feat: add event validation
Validates required envelope fields: schema, event.id, event.type, event.ts, and event.source (framework, client_id, host). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var validTypes = map[string]bool{
|
||||||
|
"session.start": true,
|
||||||
|
"session.end": true,
|
||||||
|
"run.start": true,
|
||||||
|
"run.end": true,
|
||||||
|
"span.start": true,
|
||||||
|
"span.end": true,
|
||||||
|
"error": true,
|
||||||
|
"metric.snapshot": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ValidationError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Validate(m map[string]any) error {
|
||||||
|
// Check schema
|
||||||
|
schema, ok := m["schema"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ValidationError{Field: "schema", Message: "missing or invalid"}
|
||||||
|
}
|
||||||
|
if name, _ := schema["name"].(string); name != "agentmon.event" {
|
||||||
|
return ValidationError{Field: "schema.name", Message: "must be 'agentmon.event'"}
|
||||||
|
}
|
||||||
|
if ver, _ := schema["version"].(float64); ver != 1 {
|
||||||
|
return ValidationError{Field: "schema.version", Message: "must be 1"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check event
|
||||||
|
event, ok := m["event"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ValidationError{Field: "event", Message: "missing or invalid"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if id, _ := event["id"].(string); id == "" {
|
||||||
|
return ValidationError{Field: "event.id", Message: "required"}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventType, _ := event["type"].(string)
|
||||||
|
if eventType == "" {
|
||||||
|
return ValidationError{Field: "event.type", Message: "required"}
|
||||||
|
}
|
||||||
|
if !validTypes[eventType] {
|
||||||
|
return ValidationError{Field: "event.type", Message: fmt.Sprintf("invalid type '%s'", eventType)}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event["ts"] == nil {
|
||||||
|
return ValidationError{Field: "event.ts", Message: "required"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check source
|
||||||
|
source, ok := event["source"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ValidationError{Field: "event.source", Message: "missing or invalid"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fw, _ := source["framework"].(string); fw == "" {
|
||||||
|
return ValidationError{Field: "event.source.framework", Message: "required"}
|
||||||
|
}
|
||||||
|
if cid, _ := source["client_id"].(string); cid == "" {
|
||||||
|
return ValidationError{Field: "event.source.client_id", Message: "required"}
|
||||||
|
}
|
||||||
|
if host, _ := source["host"].(string); host == "" {
|
||||||
|
return ValidationError{Field: "event.source.host", Message: "required"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidationError(err error) bool {
|
||||||
|
var ve ValidationError
|
||||||
|
return errors.As(err, &ve)
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidate_ValidEvent(t *testing.T) {
|
||||||
|
raw := `{
|
||||||
|
"schema": {"name": "agentmon.event", "version": 1},
|
||||||
|
"event": {
|
||||||
|
"id": "e1",
|
||||||
|
"type": "run.start",
|
||||||
|
"ts": "2026-01-17T00:00:00Z",
|
||||||
|
"source": {"framework": "claude-code", "client_id": "c1", "host": "myhost"}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
var m map[string]any
|
||||||
|
_ = json.Unmarshal([]byte(raw), &m)
|
||||||
|
|
||||||
|
err := Validate(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate_MissingEventID(t *testing.T) {
|
||||||
|
raw := `{
|
||||||
|
"schema": {"name": "agentmon.event", "version": 1},
|
||||||
|
"event": {
|
||||||
|
"type": "run.start",
|
||||||
|
"ts": "2026-01-17T00:00:00Z",
|
||||||
|
"source": {"framework": "claude-code", "client_id": "c1", "host": "myhost"}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
var m map[string]any
|
||||||
|
_ = json.Unmarshal([]byte(raw), &m)
|
||||||
|
|
||||||
|
err := Validate(m)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for missing event.id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate_InvalidType(t *testing.T) {
|
||||||
|
raw := `{
|
||||||
|
"schema": {"name": "agentmon.event", "version": 1},
|
||||||
|
"event": {
|
||||||
|
"id": "e1",
|
||||||
|
"type": "invalid.type",
|
||||||
|
"ts": "2026-01-17T00:00:00Z",
|
||||||
|
"source": {"framework": "claude-code", "client_id": "c1", "host": "myhost"}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
var m map[string]any
|
||||||
|
_ = json.Unmarshal([]byte(raw), &m)
|
||||||
|
|
||||||
|
err := Validate(m)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate_WrongSchemaName(t *testing.T) {
|
||||||
|
raw := `{
|
||||||
|
"schema": {"name": "wrong.schema", "version": 1},
|
||||||
|
"event": {
|
||||||
|
"id": "e1",
|
||||||
|
"type": "run.start",
|
||||||
|
"ts": "2026-01-17T00:00:00Z",
|
||||||
|
"source": {"framework": "claude-code", "client_id": "c1", "host": "myhost"}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
var m map[string]any
|
||||||
|
_ = json.Unmarshal([]byte(raw), &m)
|
||||||
|
|
||||||
|
err := Validate(m)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for wrong schema name")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user