stats

package
v1.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package stats tracks live per-connector counters and rolling boundary-acceptance rates for the config API and the future UI dashboard. It is independent of OTel (which serves Prometheus via internal/metrics): this registry exists for cheap, synchronous, in-process reads from HTTP handlers.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type ByteDistribution added in v1.0.2

type ByteDistribution struct {
	Offset            int     `json:"offset"`
	Samples           int64   `json:"samples"`
	Minimum           uint8   `json:"minimum"`
	Maximum           uint8   `json:"maximum"`
	MostCommon        uint8   `json:"most_common"`
	MostCommonShare   float64 `json:"most_common_share"`
	EntropyBits       float64 `json:"entropy_bits"`
	ChangedShare      float64 `json:"changed_share"`
	ChangedBitMaskHex string  `json:"changed_bit_mask_hex"`
	OtherSamples      int64   `json:"other_samples,omitempty"`
}

ByteDistribution describes one raw payload byte position. Counts stay internal; only bounded descriptive values leave the registry.

type Event

type Event struct {
	Time        time.Time `json:"time"`
	Stage       string    `json:"stage"`
	ConnectorID string    `json:"connector_id,omitempty"`
	PGN         uint32    `json:"pgn"`
	PGNName     string    `json:"pgn_name,omitempty"`
	Source      uint8     `json:"source"`
	Dest        uint8     `json:"dest"`
	Priority    uint8     `json:"priority"`
	Timestamp   time.Time `json:"timestamp"`
	Payload     string    `json:"payload,omitempty"`
	SizeBytes   int       `json:"size_bytes"`
}

type FieldDistribution added in v1.0.2

type FieldDistribution struct {
	Field               string           `json:"field"`
	Kind                string           `json:"kind"`
	Unit                string           `json:"unit,omitempty"`
	Samples             int64            `json:"samples"`
	Last                string           `json:"last"`
	Minimum             *float64         `json:"minimum,omitempty"`
	Maximum             *float64         `json:"maximum,omitempty"`
	Mean                *float64         `json:"mean,omitempty"`
	StdDev              *float64         `json:"stddev,omitempty"`
	LastNumeric         *float64         `json:"last_numeric,omitempty"`
	LastChange          *float64         `json:"last_change,omitempty"`
	P05                 *float64         `json:"p05,omitempty"`
	P50                 *float64         `json:"p50,omitempty"`
	P95                 *float64         `json:"p95,omitempty"`
	P99                 *float64         `json:"p99,omitempty"`
	LastRateOfChange    *float64         `json:"last_rate_of_change,omitempty"`
	StuckSeconds        float64          `json:"stuck_seconds,omitempty"`
	PresentMessages     int64            `json:"present_messages"`
	MissingMessages     int64            `json:"missing_messages"`
	AvailabilityPercent float64          `json:"availability_percent"`
	InvalidCount        int64            `json:"invalid_count,omitempty"`
	NovelValueCount     int64            `json:"novel_value_count,omitempty"`
	CatalogMinimum      *float64         `json:"catalog_minimum,omitempty"`
	CatalogMaximum      *float64         `json:"catalog_maximum,omitempty"`
	Values              map[string]int64 `json:"values,omitempty"`
	Other               int64            `json:"other,omitempty"`
}

FieldDistribution is the process-local distribution of one decoded field on a source/PGN/sender stream. Numeric fields expose descriptive statistics; category fields expose bounded value counts (overflow is counted in Other).

type PayloadFingerprint added in v1.0.2

type PayloadFingerprint struct {
	Fingerprint string    `json:"fingerprint"`
	Count       int64     `json:"count"`
	Share       float64   `json:"share"`
	Length      int       `json:"length"`
	LastSeen    time.Time `json:"last_seen"`
}

type RawPayloadDiagnostics added in v1.0.2

type RawPayloadDiagnostics struct {
	LastHex                 string               `json:"last_hex,omitempty"`
	LastFingerprint         string               `json:"last_fingerprint,omitempty"`
	LastLength              int                  `json:"last_length,omitempty"`
	LastHexTruncated        bool                 `json:"last_hex_truncated,omitempty"`
	RetainedByteLimit       int                  `json:"retained_byte_limit"`
	TruncatedSamples        int64                `json:"truncated_samples,omitempty"`
	LengthCounts            map[string]int64     `json:"length_counts,omitempty"`
	LengthCountOverflow     int64                `json:"length_count_overflow,omitempty"`
	DistinctPayloads        int                  `json:"distinct_payloads"`
	DistinctPayloadOverflow int64                `json:"distinct_payload_overflow,omitempty"`
	UnchangedSeconds        float64              `json:"unchanged_seconds,omitempty"`
	HammingDistanceMean     float64              `json:"hamming_distance_mean,omitempty"`
	HammingDistanceP95      float64              `json:"hamming_distance_p95,omitempty"`
	LastChangedBytes        []int                `json:"last_changed_bytes,omitempty"`
	LastChangeOutsidePrefix bool                 `json:"last_change_outside_prefix,omitempty"`
	Fingerprints            []PayloadFingerprint `json:"top_fingerprints,omitempty"`
	Bytes                   []ByteDistribution   `json:"byte_distributions,omitempty"`
	Samples                 []RawPayloadSample   `json:"recent_samples,omitempty"`
}

type RawPayloadSample added in v1.0.2

type RawPayloadSample struct {
	ObservedAt   time.Time `json:"observed_at"`
	Hex          string    `json:"hex"`
	Fingerprint  string    `json:"fingerprint"`
	Length       int       `json:"length"`
	HexTruncated bool      `json:"hex_truncated,omitempty"`
}

RawPayloadSample is a bounded recent wire-payload example. It is exposed in the UI and MCP, but never emitted as a Prometheus label.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry tracks live per-connector counters and rolling rates for the API and UI. It is independent of OTel (which serves Prometheus). A nil *Registry no-ops Record/SetQueue/Touch/Remove and returns empty from Snapshot/All, the same nil-safe convention as metrics.Set, so callers never need a nil check around instrumentation.

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty Registry using the real wall clock.

func (*Registry) All

func (r *Registry) All() map[string]Snapshot

All returns every tracked connector's current snapshot, keyed by id.

func (*Registry) AllSourcePGNMetrics added in v1.0.2

func (r *Registry) AllSourcePGNMetrics() map[string][]SourcePGNMetric

AllSourcePGNMetrics returns source metrics keyed by configured source id.

func (*Registry) AttachSourceMetricPersistence added in v1.0.2

func (r *Registry) AttachSourceMetricPersistence(ctx context.Context, db *sql.DB) error

AttachSourceMetricPersistence loads recent source lifecycle events and starts their non-blocking persistence writer.

func (*Registry) CloseSourceMetricPersistence added in v1.0.2

func (r *Registry) CloseSourceMetricPersistence(ctx context.Context) error

func (*Registry) Recent

func (r *Registry) Recent(kind, id string, limit int) []Event

func (*Registry) Record

func (r *Registry) Record(connector string, msgs, bytes int64)

Record adds boundary-accepted messages/bytes for a connector at time now.

func (*Registry) RecordConnectorEvent

func (r *Registry) RecordConnectorEvent(connector, stage string, e *msg.Envelope)

func (*Registry) RecordSink

func (r *Registry) RecordSink(sink, connector string, e *msg.Envelope)

func (*Registry) RecordSource

func (r *Registry) RecordSource(source string, e *msg.Envelope)

func (*Registry) RecordSourceDrops

func (r *Registry) RecordSourceDrops(source string, n int64)

func (*Registry) RecordStage

func (r *Registry) RecordStage(connector, stage string, n int64)

RecordStage counts a route-runtime transition without assuming that every terminal transition is confirmed delivery.

func (*Registry) Remove

func (r *Registry) Remove(connector string)

Remove drops a connector's stats and recent events (deleted connectors), including its queue-depth history ring — the whole *counters entry (totals, rate buckets, and depth ring alike) is deleted as one unit, so there's no separate ring-eviction step to keep in sync with this method.

Ordering contract: callers must ensure the connector's pipeline is fully stopped (Connector.Stop is synchronous — it blocks on the connector's internal WaitGroup) before calling Remove. Record does not distinguish a never-seen id from a just-removed one: get lazily (re)creates a fresh, zeroed *counters entry (empty depth ring included) for any id not currently in the map. So a Record (or SetQueue) call that lands after Remove — from a pipeline that is somehow still running or racing the removal — will silently resurrect the entry under the same id, just reset to zero rather than holding the pre-removal totals/history. It won't reappear with stale data, but it will reappear. Removing only after the pipeline has fully stopped is what prevents that resurrection.

func (*Registry) RemoveSink

func (r *Registry) RemoveSink(sink string)

func (*Registry) RemoveSource

func (r *Registry) RemoveSource(source string)

RemoveSource and RemoveSink drop process-local counters and recent payload events for an entity deleted from config. Disabled or hot-restarted entities deliberately keep their history; the supervisor calls these only after it observes the configured id disappear entirely.

func (*Registry) SetQueue

func (r *Registry) SetQueue(connector string, depth, bytes int64)

SetQueue records current queue depth/bytes (from the prune loop) and appends depth to the connector's depth-history ring (see depthRingSize), which Snapshot surfaces as DepthHistory for the UI sparkline. Because every call appends a history sample, SetQueue is for genuine periodic measurements only — presence registration belongs to Touch.

func (*Registry) SetQueueStats

func (r *Registry) SetQueueStats(connector string, s queue.Stats)

func (*Registry) SetRuntime

func (r *Registry) SetRuntime(connector, deliveryClass, state string, err error)

func (*Registry) SinkSnapshot

func (r *Registry) SinkSnapshot(sink string) (Snapshot, bool)

func (*Registry) Snapshot

func (r *Registry) Snapshot(connector string) (Snapshot, bool)

Snapshot returns a connector's current counters/rates and whether it has ever been recorded.

func (*Registry) SourceMetricCapacity added in v1.2.6

func (r *Registry) SourceMetricCapacity(source string) SourceMetricCapacity

func (*Registry) SourceMetricEvents added in v1.0.2

func (r *Registry) SourceMetricEvents(source string, limit int) []SourceMetricEvent

func (*Registry) SourcePGNLastPayloadsFiltered added in v1.2.3

func (r *Registry) SourcePGNLastPayloadsFiltered(source string, filter SourcePGNMetricFilter) []SourcePGNLastPayload

SourcePGNLastPayloadsFiltered returns one retained latest payload per matching sensor/PGN stream, sorted by sender address and PGN for stable MCP output. Oversized omitted bodies are reported by SourcePGNMetric's LatestPayloadTruncated/LatestPayloadTruncations fields instead.

func (*Registry) SourcePGNLastPayloadsFilteredLimit added in v1.2.6

func (r *Registry) SourcePGNLastPayloadsFilteredLimit(source string, filter SourcePGNMetricFilter, limit int) ([]SourcePGNLastPayload, bool)

SourcePGNLastPayloadsFilteredLimit bounds payload cloning before it occurs. A non-positive limit is unbounded for internal callers.

func (*Registry) SourcePGNMetrics added in v1.0.2

func (r *Registry) SourcePGNMetrics(source string) []SourcePGNMetric

SourcePGNMetrics returns every observed PGN/sender stream for one source. Results are sorted with active problems first, then by PGN and address.

func (*Registry) SourcePGNMetricsFiltered added in v1.2.3

func (r *Registry) SourcePGNMetricsFiltered(source string, filter SourcePGNMetricFilter) []SourcePGNMetric

SourcePGNMetricsFiltered returns rich metrics for matching PGN/sender streams. Filtering occurs before snapshot generation so narrow MCP and UI reads do not rebuild diagnostics for unrelated sensors.

func (*Registry) SourcePGNMetricsFilteredLimit added in v1.2.6

func (r *Registry) SourcePGNMetricsFilteredLimit(source string, filter SourcePGNMetricFilter, limit int) ([]SourcePGNMetric, bool)

SourcePGNMetricsFilteredLimit applies a response bound before deep-copying raw and decoded-field diagnostics. Problems retain the same ordering as the full snapshot, followed by PGN and sender address. A non-positive limit is unbounded for internal callers.

func (*Registry) SourcePGNMetricsForAddress added in v1.2.3

func (r *Registry) SourcePGNMetricsForAddress(source string, address uint8) []SourcePGNMetric

SourcePGNMetricsForAddress returns rich metrics for one opened device only, avoiding full diagnostic snapshots for unrelated devices.

func (*Registry) SourcePGNSummaries added in v1.2.3

func (r *Registry) SourcePGNSummaries(source string) []SourcePGNMetric

SourcePGNSummaries returns compact per-stream state for high-frequency UI refreshes. Rich decoded-field and raw-payload diagnostics remain available through SourcePGNMetrics when explicitly requested.

func (*Registry) SourceSnapshot

func (r *Registry) SourceSnapshot(source string) (Snapshot, bool)

func (*Registry) SubscribeStream added in v1.1.0

func (r *Registry) SubscribeStream(kind, id string, buffer int) (<-chan []byte, func())

SubscribeStream subscribes to future source-received or sink-sent envelopes. It is an observability tap: publishers never block and drop older preview data when a subscriber cannot keep up. Each message is the canonical three-key consumer envelope JSON.

func (*Registry) Touch

func (r *Registry) Touch(connector string)

Touch ensures a connector's entry exists (so it shows up in All()/ Snapshot immediately) and zeroes its depth/bytes gauges, WITHOUT appending to the depth-history ring. It exists for Connector.Start's synchronous presence registration: on a hot-apply restart (config edit → supervisor Stop + new Start) the registry entry survives — Remove only fires on delete — so Start seeding via SetQueue(id, 0, 0) would append a genuine 0 mid-history, drawing a dip-to-zero notch in the sparkline that looks like the queue drained and refilled when it did no such thing. History samples must only come from the prune loop's real periodic measurements (SetQueue); Touch covers the "make the connector visible now, real numbers follow within milliseconds" path.

type Snapshot

type Snapshot struct {
	TotalMessages           int64            `json:"total_messages"`
	TotalBytes              int64            `json:"total_bytes"`
	MsgPerSec               float64          `json:"msg_per_sec"`   // over last 10s window
	BytesPerSec             float64          `json:"bytes_per_sec"` // over last 10s window
	QueueDepth              int64            `json:"queue_depth"`
	QueueBytes              int64            `json:"queue_bytes"`
	RetainedDepth           int64            `json:"retained_depth"`
	RetainedBytes           int64            `json:"retained_bytes"`
	QueueCursor             int64            `json:"queue_cursor"`
	QueueTail               int64            `json:"queue_tail"`
	OldestPending           *time.Time       `json:"oldest_pending,omitempty"`
	OldestRetained          *time.Time       `json:"oldest_retained,omitempty"`
	LimitMessages           int64            `json:"limit_messages,omitempty"`
	LimitBytes              int64            `json:"limit_bytes,omitempty"`
	HeadroomMessages        int64            `json:"headroom_messages,omitempty"`
	HeadroomBytes           int64            `json:"headroom_bytes,omitempty"`
	DeliveryClass           string           `json:"delivery_class,omitempty"`
	State                   string           `json:"state,omitempty"`
	LastError               string           `json:"last_error,omitempty"`
	Drops                   int64            `json:"drops"`
	StageTotals             map[string]int64 `json:"stage_totals,omitempty"`
	StageTotalsOverflow     int64            `json:"stage_totals_overflow,omitempty"`
	PreviewDocumentsOmitted int64            `json:"preview_documents_omitted,omitempty"`

	// Source-only diagnostic capacity accounting. Exact source traffic totals
	// above continue increasing when a novel PGN/sender cannot be admitted to
	// the bounded rich-diagnostics cache.
	SourceMetricStreams         int   `json:"source_metric_streams,omitempty"`
	SourceMetricStreamLimit     int   `json:"source_metric_stream_limit,omitempty"`
	SourceMetricGlobalStreams   int   `json:"source_metric_global_streams,omitempty"`
	SourceMetricGlobalLimit     int   `json:"source_metric_global_limit,omitempty"`
	SourceMetricMessagesOmitted int64 `json:"source_metric_messages_omitted,omitempty"`
	SourceMetricStreamsExpired  int64 `json:"source_metric_streams_expired,omitempty"`
	SourceDeviceNamesOmitted    int64 `json:"source_device_names_omitted,omitempty"`

	// DepthHistory is the last depthRingSize QueueDepth readings, oldest
	// first, as recorded by successive SetQueue calls. Absent (nil/omitted
	// from JSON) for a connector that has never had SetQueue called on it —
	// additive/non-breaking: existing Snapshot consumers (the config API's
	// metrics endpoints, the connector detail/dashboard UI fragments) that
	// don't know this field simply ignore it.
	DepthHistory []int64 `json:"depth_history,omitempty"`
}

Snapshot is the point-in-time view of one connector's counters, returned by Snapshot and All. Rates are computed over the trailing 10-second window; totals never decay.

type SourceMetricCapacity added in v1.2.6

type SourceMetricCapacity struct {
	TrackedStreams       int   `json:"tracked_streams"`
	StreamLimit          int   `json:"stream_limit"`
	GlobalTrackedStreams int   `json:"global_tracked_streams"`
	GlobalStreamLimit    int   `json:"global_stream_limit"`
	MessagesOmitted      int64 `json:"messages_omitted"`
	StreamsExpired       int64 `json:"streams_expired"`
	DeviceNamesOmitted   int64 `json:"device_names_omitted"`
	PreviewDocsOmitted   int64 `json:"preview_documents_omitted"`
}

SourceMetricCapacity describes the bounded rich-diagnostics cache for one configured source. MessagesOmitted does not mean traffic loss: canonical Envelopes and exact source totals continue while only a novel stream's per-PGN diagnostic state is skipped.

type SourceMetricEvent added in v1.0.2

type SourceMetricEvent struct {
	ID            int64             `json:"id,omitempty"`
	Time          time.Time         `json:"time"`
	SourceID      string            `json:"source_id"`
	PGN           uint32            `json:"pgn"`
	SourceAddress uint8             `json:"source_address"`
	DeviceNameHex string            `json:"device_name_hex,omitempty"`
	Kind          string            `json:"kind"`
	Severity      string            `json:"severity"`
	Summary       string            `json:"summary"`
	Details       map[string]string `json:"details,omitempty"`
}

type SourcePGNLastPayload added in v1.2.3

type SourcePGNLastPayload struct {
	SourceID      string
	PGN           uint32
	PGNName       string
	SourceAddress uint8
	DeviceNameHex string
	LastSeen      time.Time
	PayloadBytes  int
	Truncated     bool
	Payload       json.RawMessage `json:"-"`
}

SourcePGNLastPayload is the single most recently observed decoded payload for one configured-source/PGN/sender stream. The registry stores only this one payload per bounded stream and omits bodies above maxRetainedDecodedPayloadBytes rather than retaining invalid JSON prefixes. Payload is intentionally excluded from generic JSON/schema reflection so an MCP seam can decode the raw JSON into its true object/array/scalar type.

type SourcePGNMetric added in v1.0.2

type SourcePGNMetric struct {
	Observed                 bool                   `json:"observed"`
	SourceID                 string                 `json:"source_id"`
	PGN                      uint32                 `json:"pgn"`
	PGNName                  string                 `json:"pgn_name,omitempty"`
	Variant                  string                 `json:"variant,omitempty"`
	Transport                string                 `json:"transport,omitempty"`
	ManufacturerCode         *uint16                `json:"manufacturer_code,omitempty"`
	DecodeStatus             string                 `json:"decode_status"`
	DecodeStatuses           map[string]int64       `json:"decode_statuses"`
	DecodeComplete           int64                  `json:"decode_complete"`
	DecodeIncomplete         int64                  `json:"decode_incomplete"`
	DecodeFallback           int64                  `json:"decode_fallback"`
	UnknownMessages          int64                  `json:"unknown_messages"`
	MissingDecodedFields     map[string]int64       `json:"missing_decoded_fields,omitempty"`
	MissingDecodedOverflow   int64                  `json:"missing_decoded_fields_overflow,omitempty"`
	SourceAddress            uint8                  `json:"source_address"`
	DeviceName               *uint64                `json:"device_name,omitempty"`
	DeviceNameHex            string                 `json:"device_name_hex,omitempty"`
	Messages                 int64                  `json:"messages"`
	DiagnosticSamples        int64                  `json:"diagnostic_samples"`
	DiagnosticTruncations    int64                  `json:"diagnostic_truncations,omitempty"`
	LatestPayloadBytes       int                    `json:"latest_payload_bytes,omitempty"`
	LatestPayloadTruncated   bool                   `json:"latest_payload_truncated,omitempty"`
	LatestPayloadTruncations int64                  `json:"latest_payload_truncations,omitempty"`
	FirstSeen                time.Time              `json:"first_seen"`
	LastSeen                 time.Time              `json:"last_seen"`
	AgeSeconds               float64                `json:"age_seconds"`
	FrequencyHz              float64                `json:"frequency_hz"`
	ExpectedPeriodSeconds    float64                `json:"expected_period_seconds"`
	ShortestPeriodSeconds    float64                `json:"shortest_period_seconds,omitempty"`
	LongestPeriodSeconds     float64                `json:"longest_period_seconds,omitempty"`
	PeriodP90Seconds         float64                `json:"period_p90_seconds,omitempty"`
	PeriodP95Seconds         float64                `json:"period_p95_seconds,omitempty"`
	PeriodP99Seconds         float64                `json:"period_p99_seconds,omitempty"`
	JitterMADSeconds         float64                `json:"jitter_mad_seconds,omitempty"`
	JitterPercent            float64                `json:"jitter_percent,omitempty"`
	BurstCount               int64                  `json:"burst_count"`
	RecentMessagesPerSec     float64                `json:"recent_messages_per_sec"`
	RecentBytesPerSec        float64                `json:"recent_bytes_per_sec"`
	EstimatedBusLoadPercent  float64                `json:"estimated_bus_load_percent"`
	TrafficSharePercent      float64                `json:"traffic_share_percent"`
	PayloadBytesLast         int64                  `json:"payload_bytes_last"`
	PayloadBytesMin          int64                  `json:"payload_bytes_min"`
	PayloadBytesMax          int64                  `json:"payload_bytes_max"`
	PayloadBytesMean         float64                `json:"payload_bytes_mean"`
	GapActive                bool                   `json:"gap_active"`
	GapRatio                 float64                `json:"gap_ratio,omitempty"`
	GapCount                 int64                  `json:"gap_count"`
	LastGapAt                *time.Time             `json:"last_gap_at,omitempty"`
	LongestGapSeconds        float64                `json:"longest_gap_seconds,omitempty"`
	Status                   string                 `json:"status"`
	DestinationCounts        map[string]int64       `json:"destination_counts"`
	PriorityCounts           map[string]int64       `json:"priority_counts"`
	IdentityChanges          int64                  `json:"identity_changes"`
	Raw                      *RawPayloadDiagnostics `json:"raw,omitempty"`
	Fields                   []FieldDistribution    `json:"fields,omitempty"`
}

SourcePGNMetric describes one distinct stream on a configured source. The CAN source address is part of the identity so two devices sending the same PGN remain independently observable when one stops transmitting.

type SourcePGNMetricFilter added in v1.2.3

type SourcePGNMetricFilter struct {
	PGN           *uint32
	SourceAddress *uint8
	DeviceNameHex string
}

SourcePGNMetricFilter limits rich metric snapshots before their field and raw diagnostics are copied and sorted. DeviceNameHex is the stable NMEA 2000 Device NAME without regard to an optional 0x prefix or hex letter case.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL