Wadjit (pronounced /ˈwɒdʒɪt/, or "watch it") is a program for endpoint monitoring and analysis.
Note: Version 0.10.x introduces breaking changes to DNS error handling. See CHANGELOG.md for migration details.
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().
The CHANGELOG.md shows recent changes and explains mitigations that might be needed when migrating from one version to the next.
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 ./examplesRun tests with:
make testOther 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:
DNSRefreshDefaultmirrors Go's standardhttp.Transport: connections stay warm until some other condition closes them.DNSRefreshStaticbypasses DNS entirely by dialing a fixednetip.AddrPort, making every reuse hit the same IP.DNSRefreshSingleLookupdoes one resolution during initialization, caches the addresses, and keeps reusing that result indefinitely.DNSRefreshTTLhonors observed DNS TTLs (clamped by optionalTTLMin/TTLMax) and forces a fresh lookup once the TTL elapses. Failed refreshes reuse the cached address by default; setDisableFallbackto drop it instead. TTLs ≤ 0 are treated as “expire immediately,” ensuring the next request resolves again unless fallback keeps the previous address alive.DNSRefreshCadenceignores 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 unlessDisableFallbackis set.
Guard rails add safety nets on top of any mode. Configure DNSGuardRailPolicy with a consecutive error threshold, optional rolling window, and an action:
DNSGuardRailActionFlushdrops idle connections after the threshold, ensuring the next request redials.DNSGuardRailActionForceLookupalso sets aforceLookupflag 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.
DNS errors are always reported in WatcherResponse.Err, even when fallback to cached addresses succeeds. This provides visibility into DNS degradation while maintaining resilience:
resp := <-manager.Responses()
if resp.Err != nil {
md := resp.Metadata()
if md.DNS != nil && md.DNS.FallbackUsed {
// DNS failed but request completed with cached address
log.Warn("DNS fallback used", "error", md.DNS.Err)
} else {
// Hard failure - request did not complete
log.Error("Request failed", resp.Err)
}
}New in 0.10.x: DNSMetadata includes FallbackUsed bool and Err error fields to distinguish between degraded (fallback) and failed states. See CHANGELOG.md for migration guidance.
Assuming a parsed target URL:
targetURL, _ := url.Parse("https://service.example.com/healthz")-
Default keep-alive: omit
WithDNSPolicyto 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.