Files
mqtt-5-test/internal/events/events.go
T
Ryan 71ef622b4f
Test and deploy MQTT backend / test (push) Successful in 2m8s
Test and deploy MQTT backend / deploy (push) Successful in 18s
Deploy MQTT test backend with private viewer and Gitea CI/CD
2026-10-08 14:48:53 +03:30

139 lines
4.1 KiB
Go

// Package events defines safe inspection records shared by logs and the future viewer.
package events
import (
"encoding/base64"
"encoding/json"
"time"
"unicode/utf8"
"robot-market/test-mqtt-5/internal/contracts"
)
type Direction string
const (
In Direction = "in"
Out Direction = "out"
Internal Direction = "internal"
)
type Kind string
const (
PublishAttempt Kind = "publish_attempt"
TransportAck Kind = "transport_ack"
MessageReceived Kind = "message_received"
ValidationFailed Kind = "validation_failed"
CommandAccepted Kind = "command_accepted"
CommandCompleted Kind = "command_completed"
CommandTimeout Kind = "command_timeout"
LateAck Kind = "late_ack"
DuplicateRequest Kind = "duplicate_request"
ConnectionUp Kind = "connection_up"
ConnectionDown Kind = "connection_down"
EventGap Kind = "event_gap"
)
type Properties struct {
ResponseTopic string `json:"response_topic,omitempty"`
CorrelationData []byte `json:"correlation_data,omitempty"`
MessageExpirySeconds *uint32 `json:"message_expiry_seconds,omitempty"`
}
// Event contains inspection data only. Configuration and connection credentials
// are never attached. Raw MQTT payloads require the explicit diagnostics opt-in.
type Event struct {
Scenario *ScenarioResult `json:"scenario,omitempty"`
ID uint64 `json:"id"`
ObservedAt time.Time `json:"observed_at"`
Kind Kind `json:"kind"`
Direction Direction `json:"direction"`
ClientID string `json:"client_id,omitempty"`
DeviceID string `json:"device_id,omitempty"`
Topic string `json:"topic,omitempty"`
QoS byte `json:"qos"`
Retain bool `json:"retain"`
Duplicate *bool `json:"duplicate,omitempty"`
Payload Preview `json:"payload"`
Properties Properties `json:"properties"`
RequestID string `json:"request_id,omitempty"`
Outcome string `json:"outcome,omitempty"`
LatencyMS *int64 `json:"latency_ms,omitempty"`
}
// ScenarioResult is reported test evidence, never inferred hardware compatibility.
type ScenarioResult struct {
Feature string `json:"feature"`
Environment string `json:"environment"`
Endpoint string `json:"endpoint"`
Version string `json:"version"`
Status string `json:"status"`
Observation string `json:"observation"`
Time time.Time `json:"time"`
}
type Preview struct {
Bytes int `json:"bytes"`
Text string `json:"text,omitempty"`
Omitted bool `json:"omitted"`
Truncated bool `json:"truncated"`
}
// SafePreview only renders validated contract fields. Unknown fields (including
// credentials) cause the entire payload to be omitted. Free-form error messages
// are always redacted. The original payload is never retained in this value.
func SafePreview(kind, device string, payload []byte, limit int) Preview {
p := Preview{Bytes: len(payload), Omitted: true}
if limit < 1 {
return p
}
if limit > 4096 {
limit = 4096
}
msg, err := contracts.Decode(kind, payload, device)
if err != nil {
return p
}
if ack, ok := msg.(*contracts.Ack); ok && ack.Error != nil {
ack.Error.Message = "[redacted]"
}
encoded, err := json.Marshal(msg)
if err != nil {
return p
}
if len(encoded) > limit {
encoded = encoded[:limit]
for len(encoded) > 0 && !utf8.Valid(encoded) {
encoded = encoded[:len(encoded)-1]
}
p.Truncated = true
}
p.Text = string(encoded)
p.Omitted = false
return p
}
// RawPreview is opt-in diagnostics for this isolated test harness. It displays
// received bytes without treating them as a valid contract or execution evidence.
func RawPreview(payload []byte, limit int) Preview {
p := Preview{Bytes: len(payload)}
if limit < 1 {
p.Omitted = true
return p
}
limit = min(limit, 4096)
text := string(payload)
if !utf8.Valid(payload) {
text = "[base64] " + base64.StdEncoding.EncodeToString(payload)
}
if len(text) > limit {
text = text[:limit]
for !utf8.ValidString(text) {
text = text[:len(text)-1]
}
p.Truncated = true
}
p.Text = text
return p
}