Instrument the Worker with OpenTelemetry traces + structured logs over OTLP, plus alertable signals, via a minimal hand-rolled OTLP/HTTP exporter that fits the TinyGo/Wasm Worker build. New internal/telemetry package (build-tag-free, host-tested): - Span/log shim: Telemetry provider, Span (attrs/status/events/end), Log (Info/Warn/Error), trace/span-id correlation, W3C-style ids from crypto/rand. - Exporter seam: MemoryExporter (in-memory, for tests) and OTLPHTTPExporter (OTLP/HTTP JSON over net/http). No go.opentelemetry.io/otel/sdk dependency: the full OTEL-Go SDK + OTLP exporters pull in a large, reflection-heavy tree (protobuf, grpc) that bloats the Wasm binary and is unreliable under TinyGo. The shim uses only stdlib already proven under this project's js/wasm target (net/http per #26, encoding/json, crypto/rand). OTLP is the wire format, so any OTLP backend can ingest it. - Behaviour-preserving by construction: instrumentation is threaded through context. Instrumented code pulls an optional *Telemetry from ctx; absent (or nil exporter) => every method is a no-op. No public signatures change (NewHandler, handler.New, publish.New/Publish, schedule.Run are untouched), so parallel work built on the current APIs keeps compiling. Instrumentation: - ingest: an "ingest.request" server span + correlated log per request, classifying accepted / rejected / rate_limited / error. Observe-only (wraps the response writer to read the status); the HTTP contract is unchanged. A 5xx (e.g. 503 storage-unavailable) sets the span to Error and emits the alertable ingest.error signal; 4xx client rejections are INFO, not alerts. - publish: a "publish.run" span with per-report "publish.report" child spans and a log per report (published/failed). A failed report/run sets Error and emits alert.type=publish.run_failed. The per-run cap-hit (folding in the #14 follow-up) is now emitted as a structured, alertable OTEL signal (alert.type=publish.cap_hit + counts), not merely a log line. - schedule: a "schedule.run" span parenting the publish run; a list/publish failure emits alert.type=schedule.run_failed. Config (OTLP endpoint TBD, issue #17): - OTEL_EXPORTER_OTLP_ENDPOINT (plain var) - base OTLP/HTTP URL; empty => telemetry disabled (Worker behaves as before). /v1/traces and /v1/logs are appended. - OTEL_EXPORTER_OTLP_HEADERS (Secrets Store secret) - auth header(s), never committed. OTEL_SERVICE_NAME (plain var) - service.name override. - worker/telemetry_wasm.go builds the exporter lazily per run and injects the provider into the request/scheduled context; wrangler.jsonc gains only these OTEL keys. Alerting: run-failure, cap-hit, and elevated-ingest-error are emitted as span status=Error and structured log records carrying alert=true + a specific alert.type, so a backend alert rule can key on them once the OTLP endpoint is chosen. Tests: host unit tests with the in-memory exporter assert the ingest spans+logs for accepted/rejected/error, the publish run span + per-report spans + the cap-hit and run-failed signals, the schedule run span + list-error alert, and the OTLP/JSON encoding + HTTP round trip (httptest, no real backend). No-op default verified. go vet ./... and go test ./... green; GOOS=js GOARCH=wasm go build ./... compiles. Closes #17 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
100 lines
2.4 KiB
Go
100 lines
2.4 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
)
|
|
|
|
// MemoryExporter is an in-memory [Exporter] for host tests: it retains every
|
|
// span and log record it is handed so tests can assert on them. It is safe for
|
|
// concurrent use and never fails. It must not be used in production (it grows
|
|
// without bound).
|
|
type MemoryExporter struct {
|
|
mu sync.Mutex
|
|
spans []SpanData
|
|
logs []LogRecord
|
|
}
|
|
|
|
// NewMemoryExporter returns an empty MemoryExporter.
|
|
func NewMemoryExporter() *MemoryExporter { return &MemoryExporter{} }
|
|
|
|
// ExportSpans records spans.
|
|
func (m *MemoryExporter) ExportSpans(_ context.Context, spans []SpanData) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.spans = append(m.spans, spans...)
|
|
return nil
|
|
}
|
|
|
|
// ExportLogs records log records.
|
|
func (m *MemoryExporter) ExportLogs(_ context.Context, logs []LogRecord) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.logs = append(m.logs, logs...)
|
|
return nil
|
|
}
|
|
|
|
// Shutdown is a no-op.
|
|
func (m *MemoryExporter) Shutdown(context.Context) error { return nil }
|
|
|
|
// Spans returns a snapshot copy of the exported spans.
|
|
func (m *MemoryExporter) Spans() []SpanData {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return append([]SpanData(nil), m.spans...)
|
|
}
|
|
|
|
// Logs returns a snapshot copy of the exported log records.
|
|
func (m *MemoryExporter) Logs() []LogRecord {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return append([]LogRecord(nil), m.logs...)
|
|
}
|
|
|
|
// Reset clears all captured spans and logs.
|
|
func (m *MemoryExporter) Reset() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.spans = nil
|
|
m.logs = nil
|
|
}
|
|
|
|
// SpanByName returns the first captured span with the given name and whether one
|
|
// was found. A convenience for assertions.
|
|
func (m *MemoryExporter) SpanByName(name string) (SpanData, bool) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for _, s := range m.spans {
|
|
if s.Name == name {
|
|
return s, true
|
|
}
|
|
}
|
|
return SpanData{}, false
|
|
}
|
|
|
|
// SpansByName returns all captured spans with the given name, in export order.
|
|
func (m *MemoryExporter) SpansByName(name string) []SpanData {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []SpanData
|
|
for _, s := range m.spans {
|
|
if s.Name == name {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Attr returns the value of attribute key among attrs and whether it was
|
|
// present. It is a helper for tests asserting on span/log attributes.
|
|
func Attr(attrs []KeyValue, key string) (any, bool) {
|
|
for _, kv := range attrs {
|
|
if kv.Key == key {
|
|
return kv.Value, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
var _ Exporter = (*MemoryExporter)(nil)
|