// Package protocol holds the wire types of the probe ↔ ingestion protocol (docs/PROBE-PROTOCOL.md, v1). // // Field names and JSON tags mirror the document exactly; optional fields use pointers with omitempty so that // "fields not relevant to kind" are omitted (the protocol allows omitted or null). package protocol import "time" // TimeFormat is the UTC RFC 3339 format with milliseconds used everywhere on the wire. const TimeFormat = "2006-01-02T15:04:05.000Z07:00" // FormatTime renders t as UTC RFC 3339 with milliseconds ("2026-09-12T05:10:04.123Z"). func FormatTime(t time.Time) string { return t.UTC().Format(TimeFormat) } // ParseTime accepts RFC 3339 with or without fractional seconds. func ParseTime(s string) (time.Time, error) { if t, err := time.Parse(time.RFC3339Nano, s); err == nil { return t, nil } return time.Parse(time.RFC3339, s) } // ---- GET /config ------------------------------------------------------------------------------------------- // RemoteConfig is the probe assignment returned by GET /ingest/v1/config. type RemoteConfig struct { ServerTime string `json:"server_time"` ConfigVersion string `json:"config_version"` Probe ProbeInfo `json:"probe"` Schedule Schedule `json:"schedule"` Resolvers []Resolver `json:"resolvers"` Targets []Target `json:"targets"` } // ProbeInfo describes the probe as known by the server. type ProbeInfo struct { ProbeID string `json:"probe_id"` Name string `json:"name"` Region string `json:"region"` Country string `json:"country"` City string `json:"city"` Provider string `json:"provider"` ASN int `json:"asn"` Lat *float64 `json:"lat"` Lon *float64 `json:"lon"` Enabled bool `json:"enabled"` } // Schedule carries the interval parameters. Tiers keys are strings ("1", "2", "3") as in the JSON document. type Schedule struct { Tiers map[string]int `json:"tiers"` DNSEvery int `json:"dns_every"` PingEvery int `json:"ping_every"` TracerouteEvery int `json:"traceroute_every"` BatchFlushSeconds int `json:"batch_flush_seconds"` MaxBatch int `json:"max_batch"` ConfigRefreshSeconds int `json:"config_refresh_seconds"` Boost *Boost `json:"boost,omitempty"` } // Boost multiplies the intervals of the listed targets by Factor until Until. type Boost struct { Targets []string `json:"targets"` Factor float64 `json:"factor"` Until string `json:"until"` } // Active reports whether the boost applies to targetID at time now. func (b *Boost) Active(targetID string, now time.Time) bool { if b == nil || b.Factor <= 0 { return false } until, err := ParseTime(b.Until) if err != nil || !now.Before(until) { return false } for _, t := range b.Targets { if t == targetID { return true } } return false } // Resolver is a DNS resolver to query. ID "system" (empty address) means the OS resolver. type Resolver struct { ID string `json:"id"` Address string `json:"address"` } // Target is one monitored endpoint. type Target struct { TargetID string `json:"target_id"` Name string `json:"name"` Hostname string `json:"hostname"` URL string `json:"url"` IP *string `json:"ip"` Port int `json:"port"` Category string `json:"category"` Provider string `json:"provider"` ServiceID string `json:"service_id"` Country *string `json:"country"` Region string `json:"region"` Importance int `json:"importance"` Tier int `json:"tier"` Checks []string `json:"checks"` Traceroute bool `json:"traceroute"` } // HasCheck reports whether kind is listed in Checks. func (t Target) HasCheck(kind string) bool { for _, c := range t.Checks { if c == kind { return true } } return false } // FixedIP returns the pinned IP ("" when none). func (t Target) FixedIP() string { if t.IP == nil { return "" } return *t.IP } // ---- POST /batch ------------------------------------------------------------------------------------------- // Batch is the (pre-gzip) body of POST /ingest/v1/batch. type Batch struct { ProbeID string `json:"probe_id"` AgentVersion string `json:"agent_version"` SentAt string `json:"sent_at"` Measurements []Measurement `json:"measurements"` Traceroutes []Traceroute `json:"traceroutes,omitempty"` Health *Health `json:"health,omitempty"` } // Measurement is one http / dns / ping / tcp observation. type Measurement struct { TS string `json:"ts"` TargetID string `json:"target_id"` Kind string `json:"kind"` // "http" | "dns" | "ping" | "tcp" OK bool `json:"ok"` Error string `json:"error"` // "" or one of the protocol error codes // http DNSMs *float64 `json:"dns_ms,omitempty"` TCPMs *float64 `json:"tcp_ms,omitempty"` TLSMs *float64 `json:"tls_ms,omitempty"` TTFBMs *float64 `json:"ttfb_ms,omitempty"` TotalMs *float64 `json:"total_ms,omitempty"` HTTPStatus *int `json:"http_status,omitempty"` HTTPProto string `json:"http_proto,omitempty"` TLSVersion string `json:"tls_version,omitempty"` ResolvedIP string `json:"resolved_ip,omitempty"` // dns Resolver string `json:"resolver,omitempty"` DNSRcode string `json:"dns_rcode,omitempty"` DNSAnswers []string `json:"dns_answers,omitempty"` // ping / tcp Sent *int `json:"sent,omitempty"` Received *int `json:"received,omitempty"` PacketLoss *float64 `json:"packet_loss,omitempty"` RTTMinMs *float64 `json:"rtt_min_ms,omitempty"` RTTAvgMs *float64 `json:"rtt_avg_ms,omitempty"` RTTMaxMs *float64 `json:"rtt_max_ms,omitempty"` JitterMs *float64 `json:"jitter_ms,omitempty"` } // Traceroute is one path measurement. type Traceroute struct { TS string `json:"ts"` TargetID string `json:"target_id"` DestIP string `json:"dest_ip"` Reached bool `json:"reached"` HopCount int `json:"hop_count"` TotalMs *float64 `json:"total_ms"` RouteHash string `json:"route_hash"` Hops []Hop `json:"hops"` } // Hop is one traceroute hop; IP is "*" and RTTMs nil when unanswered. type Hop struct { N int `json:"n"` IP string `json:"ip"` RTTMs *float64 `json:"rtt_ms"` } // Health is the agent self-report (at most once per minute). type Health struct { TS string `json:"ts"` AgentVersion string `json:"agent_version"` UptimeS int64 `json:"uptime_s"` Buffered int `json:"buffered"` SpoolBytes int64 `json:"spool_bytes"` MeasurementsTotal uint64 `json:"measurements_total"` ErrorsTotal uint64 `json:"errors_total"` ClockOffsetMs int64 `json:"clock_offset_ms"` RSSMb float64 `json:"rss_mb"` Goroutines int `json:"goroutines"` Capabilities []string `json:"capabilities"` OS string `json:"os"` Arch string `json:"arch"` Identity *Identity `json:"identity,omitempty"` } // Identity is the public, city-level network identity of the probe (never more precise than city). type Identity struct { PublicIP string `json:"public_ip"` ASN int `json:"asn"` Org string `json:"org"` Country string `json:"country"` City string `json:"city"` Lat *float64 `json:"lat"` Lon *float64 `json:"lon"` Source string `json:"source"` } // BatchResponse is the body returned by POST /batch. type BatchResponse struct { Accepted int `json:"accepted"` Rejected int `json:"rejected"` ConfigVersion string `json:"config_version"` ServerTime string `json:"server_time"` Boost *Boost `json:"boost,omitempty"` } // ---- GET /agent/latest ------------------------------------------------------------------------------------- // AgentLatest describes the newest agent release. type AgentLatest struct { Version string `json:"version"` Assets map[string]Asset `json:"assets"` } // Asset is one downloadable binary. type Asset struct { URL string `json:"url"` SHA256 string `json:"sha256"` } // Error codes (docs/PROBE-PROTOCOL.md § Measurement). const ( ErrDNSFail = "dns_fail" ErrDNSTimeout = "dns_timeout" ErrDNSServfail = "dns_servfail" ErrDNSNxdomain = "dns_nxdomain" ErrTCPTimeout = "tcp_timeout" ErrTCPRefused = "tcp_refused" ErrTCPReset = "tcp_reset" ErrTLSFail = "tls_fail" ErrTLSCert = "tls_cert" ErrHTTPTimeout = "http_timeout" ErrHTTP5xx = "http_5xx" ErrHTTP4xx = "http_4xx" ErrReset = "reset" ErrUnreachable = "unreachable" ErrICMPUnavailable = "icmp_unavailable" ErrOther = "other" ) // F returns a pointer to v (helper for optional float fields). func F(v float64) *float64 { return &v } // I returns a pointer to v (helper for optional int fields). func I(v int) *int { return &v } // Ms converts a duration to milliseconds with 0.1 ms resolution. func Ms(d time.Duration) float64 { return float64(d.Microseconds()) / 1000 }