Wadjit (pronounced /ˈwɒdʒɪt/, or "watch it") is a program for endpoint monitoring and analysis.
Wadjet is the ancient Egyptian goddess of protection and royal authority. She is sometimes shown as the Eye of Ra, acting as a protector of the country and the king, and her vigilant eye would watch over the land. - Wikipedia
wadjit.New()
creates a manager for an arbitrary number of watchers. The watchers monitor pre-defined endpoints according to their configuration, and feed the results back to the manager. A single Wadjit manager can hold watchers for many different tasks, as responses on the response channel are separated by watcher ID, or you may choose to create several managers as a more strict separation of concerns.
go get github.com/jkbrsn/wadjit@latest
- HTTP + WS: Monitor HTTP and WebSocket endpoints, with or without TLS.
- WS modes: One-shot messages and persistent connections for JSON-RPC.
- Batched watchers: Schedule many tasks per watcher at a fixed cadence.
- Buffered responses: Non-blocking channel with watcher IDs and metadata.
- Metrics: Access scheduler metrics via
Metrics()
.
Minimal example with one HTTP and one WS task and basic response handling:
package main
import (
"fmt"
"net/http"
"net/url"
"time"
"github.com/jkbrsn/wadjit"
)
func main() {
// Initialize manager (options available)
manager := wadjit.New()
defer manager.Close()
// Build tasks
httpTask := &wadjit.HTTPEndpoint{
Header: make(http.Header),
Method: http.MethodGet,
URL: &url.URL{Scheme: "https", Host: "httpbin.org", Path: "/get"},
}
wsTask := &wadjit.WSEndpoint{
Mode: wadjit.OneHitText,
Payload: []byte("hello"),
URL: &url.URL{Scheme: "wss", Host: "ws.postman-echo.com", Path: "/raw"},
}
// Add a watcher with a 5s cadence
watcher, err := wadjit.NewWatcher("example", 5*time.Second, wadjit.WatcherTasksToSlice(httpTask, wsTask))
if err == nil {
_ = manager.AddWatcher(watcher)
}
// Consume responses (must be read to avoid backpressure)
for resp := range manager.Responses() {
if resp.Err != nil {
fmt.Printf("%s error: %v\n", resp.WatcherID, resp.Err)
continue
}
body, err := resp.Data()
if err != nil { continue }
fmt.Printf("%s %s -> %s\n", resp.WatcherID, resp.URL, string(body))
// Access timing and header metadata
md := resp.Metadata()
fmt.Printf("latency: %v headers: %v\n", md.TimeData.Latency, md.Headers)
}
}
Need to tweak the scheduler? Wrap the constructor call: wadjit.New(wadjit.WithTaskmanMode(taskman.ModeOnDemand))
or forward taskman
options via wadjit.WithTaskmanOptions(...)
.
See a fuller runnable example in examples/example.go
and run it with:
go run ./examples
Run tests with:
make test
Other targets in the Makefile
include fmt
for formatting the code and lint
for running the linter.
These targets are also used in the GitHub CI pipeline, see .github/workflows/ci.yml
for details.
New(opts ...Option) *Wadjit
: Creates a new Wadjit instance; options can tweak the internal task manager (for exampleWithTaskmanMode
).AddWatcher(watcher *Watcher) error
: Adds a watcher to the managerAddWatchers(watchers ...*Watcher) error
: Adds multiple watchers at onceClear() error
: Stops and removes all watchers, keeps manager runningClose() error
: Stops all watchers and cleans up resourcesMetrics() taskman.TaskManagerMetrics
: Returns task scheduler metricsPauseWatcher(id string) error
: Pauses a watcher's scheduled executionRemoveWatcher(id string) error
: Removes a watcher by IDResponses() <-chan WatcherResponse
: Returns a channel for receiving responsesResumeWatcher(id string) error
: Resumes a previously paused watcherWatcherIDs() []string
: Lists IDs of active watchers
NewWatcher(id string, cadence time.Duration, tasks []WatcherTask) (*Watcher, error)
: Creates a new watcherValidate() error
: Validates the watcher configuration
For making HTTP/HTTPS requests
Wadjit's HTTP task can stay on long-lived keep-alive connections or routinely force new dials depending on the configured DNS policy. Each mode determines when the dnsPolicyManager
refreshes name resolution and flushes idle connections:
DNSRefreshDefault
mirrors Go's standardhttp.Transport
: connections stay warm until some other condition closes them.DNSRefreshStatic
bypasses DNS entirely by dialing a fixednetip.AddrPort
, making every reuse hit the same IP.DNSRefreshSingleLookup
does one resolution during initialization, caches the addresses, and keeps reusing that result indefinitely.DNSRefreshTTL
honors observed DNS TTLs (clamped by optionalTTLMin
/TTLMax
) and forces a fresh lookup once the TTL elapses. Failed refreshes reuse the cached address by default; setDisableFallback
to drop it instead. TTLs ≤ 0 are treated as “expire immediately,” ensuring the next request resolves again unless fallback keeps the previous address alive.DNSRefreshCadence
ignores TTL and instead re-lookups on a fixed cadence you supply; it still records the resolver's TTL for observability. Failed refreshes reuse the cached address unlessDisableFallback
is set.
Guard rails add safety nets on top of any mode. Configure DNSGuardRailPolicy
with a consecutive error threshold, optional rolling window, and an action:
DNSGuardRailActionFlush
drops idle connections after the threshold, ensuring the next request redials.DNSGuardRailActionForceLookup
also sets aforceLookup
flag so the next request performs a fresh DNS resolution before dialing.
Set a global default for every watcher by supplying wadjit.WithDefaultDNSPolicy(...)
when creating the Wadjit. Endpoints that do not call WithDNSPolicy
inherit this default automatically, while explicit endpoint policies still win.
Assuming a parsed target URL:
targetURL, _ := url.Parse("https://service.example.com/healthz")
-
Default keep-alive: omit
WithDNSPolicy
to keep Go's stock reuse.defaultEndpoint := wadjit.NewHTTPEndpoint(targetURL, http.MethodGet)
-
Static address: pin the transport to a literal IP:port, bypassing DNS.
staticEndpoint := wadjit.NewHTTPEndpoint( targetURL, http.MethodGet, wadjit.WithDNSPolicy(wadjit.DNSPolicy{ Mode: wadjit.DNSRefreshStatic, StaticAddr: netip.MustParseAddrPort("198.51.123.123:443"), }), )
-
Single lookup: resolve once when the watcher starts, then reuse indefinitely.
singleLookupEndpoint := wadjit.NewHTTPEndpoint( targetURL, http.MethodGet, wadjit.WithDNSPolicy(wadjit.DNSPolicy{Mode: wadjit.DNSRefreshSingleLookup}), )
-
TTL-aware refresh: honor dynamic endpoints and force fresh lookups when the TTL expires. Guard rails can force a lookup on repeated failures.
ttlEndpoint := wadjit.NewHTTPEndpoint( targetURL, http.MethodGet, wadjit.WithDNSPolicy(wadjit.DNSPolicy{ Mode: wadjit.DNSRefreshTTL, TTLMin: 5 * time.Second, TTLMax: 30 * time.Second, // DisableFallback: true, // opt out of reusing cached addresses when lookups fail GuardRail: wadjit.DNSGuardRailPolicy{ ConsecutiveErrorThreshold: 4, Window: 1 * time.Minute, Action: wadjit.DNSGuardRailActionForceLookup, }, }), )
-
Fixed cadence refresh: ignore TTL and re-dial on a clock.
cadenceEndpoint := wadjit.NewHTTPEndpoint( targetURL, http.MethodGet, wadjit.WithDNSPolicy(wadjit.DNSPolicy{ Mode: wadjit.DNSRefreshCadence, Cadence: 10 * time.Second, GuardRail: wadjit.DNSGuardRailPolicy{ ConsecutiveErrorThreshold: 2, Action: wadjit.DNSGuardRailActionFlush, }, }), )
The DNSDecisionCallback
can be used to observe decisions and state transitions.
For WebSocket connections (one-shot and persistent JSON-RPC).
Thank you for considering to contribute to this project. For contributions, please open a GitHub issue with your questions and suggestions. Before submitting an issue, have a look at the existing TODO list to see if your idea is already in the works.