-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathobserver.go
More file actions
179 lines (155 loc) · 7.2 KB
/
Copy pathobserver.go
File metadata and controls
179 lines (155 loc) · 7.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
package chat
import "context"
// ObservationName is a closed, stable set of discrete observation points the
// runtime emits. The set mirrors the decision points the runtime already logs
// via structured slog, so a log line and a metric cannot disagree about what
// happened.
type ObservationName string
const (
// ObsDedupeHit is emitted when a duplicate Event (by Event Identity) is
// dropped.
ObsDedupeHit ObservationName = "dedupe_hit"
// ObsLockConflict is emitted when an Event hits a Lock Conflict.
ObsLockConflict ObservationName = "lock_conflict"
// ObsLockReleaseFailed is emitted when releasing a Thread Lock fails or finds
// the lease already gone.
ObsLockReleaseFailed ObservationName = "lock_release_failed"
// ObsIgnoredEvent is emitted when a verified, Accepted Event is ignored
// (non-message with no payload handler, self, or unrouted). Carries a reason
// attribute.
ObsIgnoredEvent ObservationName = "ignored_event"
// ObsHandlerError is emitted when a routed handler returns an error. Carries a
// route attribute.
ObsHandlerError ObservationName = "handler_error"
// ObsAdapterCall is an adapter-facing observation emitted around a Platform
// Adapter API call. It is reached through typed Adapter Access and
// adapter-owned Observer wiring, not the core Adapter interface.
ObsAdapterCall ObservationName = "adapter_call"
// ObsRateLimit is an adapter-facing observation emitted when an adapter
// observes platform rate limiting (ADR 0005). Adapter-owned, like
// ObsAdapterCall.
ObsRateLimit ObservationName = "rate_limit"
// ObsAdmissionRejected is emitted when the deferred-dispatch Admission
// Bound rejects a delivery (MaxDetached, MaxDetachedPerTenant, or Runtime
// Shutdown). Carries adapter and tenant attributes.
ObsAdmissionRejected ObservationName = "admission_rejected"
)
// DispatchOutcome is the closed set of terminal outcomes for one Runtime
// Dispatch, mirroring the terminal branches in dispatch.
type DispatchOutcome string
const (
OutcomeHandled DispatchOutcome = "handled"
OutcomeIgnored DispatchOutcome = "ignored"
OutcomeDroppedLockConflict DispatchOutcome = "dropped-lock-conflict"
OutcomeDuplicate DispatchOutcome = "duplicate"
OutcomeError DispatchOutcome = "error"
// OutcomePreempted is the terminal outcome of a handler that stopped because
// its Lock Lease was lost mid-run: released by another runtime instance,
// expired, or no longer refreshable.
OutcomePreempted DispatchOutcome = "preempted"
// OutcomeAdmissionRejected is the terminal outcome of a delivery the
// Admission Bound rejected before acknowledgement and before dedupe
// marking: it never became an Accepted Event and owes only the overload
// response.
OutcomeAdmissionRejected DispatchOutcome = "admission-rejected"
)
// Attribute key constants form the documented, stable, low-cardinality set.
// Thread ID, message text, and raw actor IDs are deliberately never emitted as
// default attributes to keep metric cardinality bounded and avoid leaking
// conversation content.
const (
AttrAdapter = "adapter"
AttrRoute = "route"
AttrReason = "reason"
AttrOutcome = "outcome"
AttrTenant = "tenant"
)
// Attr is a single low-cardinality observation attribute. Construct attrs with
// the helper constructors so keys stay within the documented set.
type Attr struct {
Key string
Value string
}
// AdapterAttr tags an observation with the originating adapter name.
func AdapterAttr(adapter string) Attr { return Attr{Key: AttrAdapter, Value: adapter} }
// RouteAttr tags an observation with the routing decision (new-mention,
// subscribed-message, command, interaction).
func RouteAttr(route string) Attr { return Attr{Key: AttrRoute, Value: route} }
// ReasonAttr tags an ignored-event observation with why it was ignored.
func ReasonAttr(reason string) Attr { return Attr{Key: AttrReason, Value: reason} }
// OutcomeAttr tags an observation with a terminal DispatchOutcome.
func OutcomeAttr(outcome DispatchOutcome) Attr {
return Attr{Key: AttrOutcome, Value: string(outcome)}
}
// TenantAttr tags an observation with the Platform Tenant.
func TenantAttr(tenant string) Attr { return Attr{Key: AttrTenant, Value: tenant} }
// Observer is the optional Observation Hook: a narrow, observe-only seam the
// runtime calls at the decision points it already logs. It is modeled as an
// Optional Capability with a no-op default; the core never imports OpenTelemetry,
// Prometheus, or statsd. An Observer cannot mutate routing, acknowledgement, or
// handler flow, which is what keeps it distinct from Middleware.
//
// All methods must be best-effort and must not block dispatch. The runtime wraps
// every call so a panicking or slow Observer can never fail an Accepted Event or
// alter acknowledgement.
type Observer interface {
// Event records a discrete, counter-style observation with low-cardinality
// attributes.
Event(ctx context.Context, name ObservationName, attrs ...Attr)
// Dispatch opens a span for one Runtime Dispatch and returns a DispatchSpan
// whose End records the terminal outcome and latency. Under deferred dispatch
// the span follows the Detached Work Context so Ack-Then-Work latency is
// measured to handler completion.
Dispatch(ctx context.Context, attrs ...Attr) (context.Context, DispatchSpan)
}
// DispatchSpan is the open span for one Runtime Dispatch; End closes it with the
// terminal outcome.
type DispatchSpan interface {
End(outcome DispatchOutcome, attrs ...Attr)
}
// WithObserver installs an Observer. The default is an internal no-op observer,
// so an unconfigured runtime behaves exactly as today: structured slog only,
// with no third-party metrics dependency.
func WithObserver(observer Observer) Option {
return func(cfg *config) {
cfg.observer = observer
}
}
// noopObserver is the default: it records nothing and returns a no-op span.
type noopObserver struct{}
func (noopObserver) Event(context.Context, ObservationName, ...Attr) {}
func (noopObserver) Dispatch(ctx context.Context, _ ...Attr) (context.Context, DispatchSpan) {
return ctx, noopSpan{}
}
type noopSpan struct{}
func (noopSpan) End(DispatchOutcome, ...Attr) {}
// safeEvent invokes Observer.Event best-effort, recovering from a panic so
// observation can never fail an Accepted Event or alter acknowledgement.
func (c *Chat) safeEvent(ctx context.Context, name ObservationName, attrs ...Attr) {
defer c.recoverObserver("Event")
c.observer.Event(ctx, name, attrs...)
}
// safeDispatch opens the dispatch span best-effort, falling back to the original
// context and a no-op span on panic.
func (c *Chat) safeDispatch(ctx context.Context, attrs ...Attr) (outCtx context.Context, span DispatchSpan) {
outCtx, span = ctx, noopSpan{}
defer c.recoverObserver("Dispatch")
outCtx, span = c.observer.Dispatch(ctx, attrs...)
if span == nil {
span = noopSpan{}
}
return outCtx, span
}
// safeEnd closes the dispatch span best-effort.
func (c *Chat) safeEnd(span DispatchSpan, outcome DispatchOutcome, attrs ...Attr) {
if span == nil {
return
}
defer c.recoverObserver("DispatchSpan.End")
span.End(outcome, attrs...)
}
func (c *Chat) recoverObserver(method string) {
if r := recover(); r != nil {
c.logger.Warn("chat observer panicked", "method", method, "recovered", r)
}
}