139 lines
4.1 KiB
Go
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
|
|
}
|