feat(18-01): add counter cache with Redis delta computation and SNMPMetricsEvent

- Implement computeCounterDelta for Counter32/Counter64 with wraparound handling
- Sanity threshold discards deltas > 90% of max value (device reset detection)
- CounterCache uses Redis MGET/MSET pipelining for efficient state persistence
- Counter keys use "snmp:counter:{device_id}:{oid}" format with 600s TTL
- Add SNMPMetricsEvent and SNMPMetricEntry structs to bus package
- Add PublishSNMPMetrics publishing to "device.metrics.snmp_custom.{device_id}"
- Full test coverage: 10 counter tests including miniredis integration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-21 19:21:25 -05:00
parent cec0a8c6d4
commit 0697563a13
3 changed files with 337 additions and 0 deletions

View File

@@ -486,6 +486,54 @@ func (p *Publisher) PublishSessionEnd(ctx context.Context, event SessionEndEvent
return nil
}
// SNMPMetricsEvent is the payload published to NATS JetStream when custom SNMP
// metrics are collected from a device. The backend subscribes to
// "device.metrics.snmp_custom.>" to ingest these into the snmp_metrics hypertable.
type SNMPMetricsEvent struct {
DeviceID string `json:"device_id"`
TenantID string `json:"tenant_id"`
CollectedAt string `json:"collected_at"` // RFC3339
Type string `json:"type"` // always "snmp_custom"
Metrics []SNMPMetricEntry `json:"metrics"`
}
// SNMPMetricEntry is a single metric within an SNMPMetricsEvent. Numeric and
// text values are mutually exclusive; IndexValue is populated for table metrics.
type SNMPMetricEntry struct {
MetricName string `json:"metric_name"`
MetricGroup string `json:"metric_group"`
OID string `json:"oid"`
ValueNum *float64 `json:"value_numeric,omitempty"`
ValueText *string `json:"value_text,omitempty"`
IndexValue *string `json:"index_value,omitempty"`
}
// PublishSNMPMetrics publishes custom SNMP metrics to NATS JetStream.
//
// Events are published to "device.metrics.snmp_custom.{device_id}" so the
// Python metrics_subscriber can ingest them into the snmp_metrics hypertable.
func (p *Publisher) PublishSNMPMetrics(ctx context.Context, event SNMPMetricsEvent) error {
data, err := json.Marshal(event)
if err != nil {
return fmt.Errorf("marshalling SNMP metrics event: %w", err)
}
subject := fmt.Sprintf("device.metrics.snmp_custom.%s", event.DeviceID)
_, err = p.js.Publish(ctx, subject, data)
if err != nil {
return fmt.Errorf("publishing to %s: %w", subject, err)
}
slog.Debug("published SNMP metrics event",
"device_id", event.DeviceID,
"metrics_count", len(event.Metrics),
"subject", subject,
)
return nil
}
// Conn returns the raw NATS connection for use by other components
// (e.g., CmdResponder for request-reply subscriptions).
func (p *Publisher) Conn() *nats.Conn {