Observability
This guide explains how to instrument your code with observability events and provides an overview of the observability architecture. The guide is intended for developers adding new features or components to ThunderID.
Table of Contents
Integration Guide
ThunderID uses a dependency injection pattern for observability. To instrument your component:
1. Inject the Observability Service
Your component should accept ObservabilityServiceInterface in its constructor or initialization method.
import "github.com/thunder-id/thunderid/internal/system/observability"
type MyComponent struct {
obsSvc observability.ObservabilityServiceInterface
}
func NewMyComponent(obsSvc observability.ObservabilityServiceInterface) *MyComponent {
return &MyComponent{
obsSvc: obsSvc,
}
}
2. Publish Events
Use the injected service to publish events. Always check if the service is enabled (though the service handles no-ops, checking avoids unnecessary object creation).
import "github.com/thunder-id/thunderid/internal/system/observability/event"
func (c *MyComponent) DoSomething(ctx context.Context) {
// 1. Check if enabled
if c.obsSvc == nil || !c.obsSvc.IsEnabled() {
return
}
// 2. Create event
traceID := uuid.NewString() // Or get from context
evt := event.NewEvent(traceID, event.EventTypeTokenIssued, event.ComponentAuthHandler).
WithStatus(event.StatusSuccess).
WithData(event.DataKey.UserID, "user-123")
// 3. Publish
c.obsSvc.PublishEvent(evt)
}
Use the predefined component constants when setting the component name:
| Constant | Value | Usage |
|---|---|---|
event.ComponentFlowEngine | FlowEngine | Events from the flow execution engine |
event.ComponentAuthHandler | AuthHandler | Events from authentication handlers |
Event Anatomy
Required Fields
- TraceID: UUID or hex string for trace correlation.
- EventType: Predefined constant from the
eventpackage (e.g.,event.EventTypeFlowStarted). - Component: Your component name. Use a predefined constant from the
eventpackage where one exists.
Optional Fields
- Status:
event.StatusSuccess,event.StatusFailure, orevent.StatusInProgress. - Data: Key-value pairs using
event.DataKeyconstants.
Defined Event Types
Always use the predefined constants. Do not pass raw strings as event types.
Authentication events (category: observability.authentication):
| Constant | Value | Description |
|---|---|---|
event.EventTypeTokenIssuanceStarted | TOKEN_ISSUANCE_STARTED | Token issuance begins |
event.EventTypeTokenIssued | TOKEN_ISSUED | Token successfully issued |
event.EventTypeTokenIssuanceFailed | TOKEN_ISSUANCE_FAILED | Token issuance failed |
Flow execution events (category: observability.flows):
| Constant | Value | Description |
|---|---|---|
event.EventTypeFlowStarted | FLOW_STARTED | Flow execution begins |
event.EventTypeFlowNodeExecutionStarted | FLOW_NODE_EXECUTION_STARTED | A flow node begins executing |
event.EventTypeFlowNodeExecutionCompleted | FLOW_NODE_EXECUTION_COMPLETED | A flow node completes successfully |
event.EventTypeFlowNodeExecutionFailed | FLOW_NODE_EXECUTION_FAILED | A flow node fails |
event.EventTypeFlowUserInputRequired | FLOW_USER_INPUT_REQUIRED | Flow pauses waiting for user input |
event.EventTypeFlowCompleted | FLOW_COMPLETED | Flow execution succeeds |
event.EventTypeFlowFailed | FLOW_FAILED | Flow execution fails |
Event Categories
Categories control event routing. Each event type maps to exactly one category. Subscribers declare which categories they are interested in and receive only matching events.
| Category | Description |
|---|---|
observability.authentication | Token issuance events |
observability.authorization | Authorization-related events |
observability.flows | Flow execution events |
observability.all | Special category that matches all events regardless of type |
Common Data Keys
Always use predefined keys from event.DataKey for consistency. See event/datakeys.go for the complete list.
Identity keys:
| Constant | Key | Usage |
|---|---|---|
event.DataKey.UserID | user_id | Authenticated user identifier |
event.DataKey.Username | username | Username |
event.DataKey.ClientID | client_id | OAuth client identifier |
event.DataKey.EntityID | app_id | Application identifier |
Flow execution keys:
| Constant | Key | Usage |
|---|---|---|
event.DataKey.ExecutionID | execution_id | Flow execution identifier |
event.DataKey.FlowType | flow_type | Type of flow being executed |
event.DataKey.NodeID | node_id | Flow node identifier |
event.DataKey.NodeType | node_type | Flow node type |
event.DataKey.NodeStatus | node_status | Execution status of the node |
event.DataKey.ExecutorName | executor_name | Name of the executor running the node |
event.DataKey.ExecutorType | executor_type | Type of the executor |
event.DataKey.StepNumber | step_number | Step number within the flow |
event.DataKey.AttemptNumber | attempt_number | Retry attempt number |
event.DataKey.AuthMethod | auth_method | Authentication method used |
event.DataKey.RedirectTo | redirect_to | Redirect destination |
event.DataKey.FailedStep | failed_step | Step at which the flow failed |
OAuth and token keys:
| Constant | Key | Usage |
|---|---|---|
event.DataKey.Scope | scope | OAuth scopes |
event.DataKey.GrantType | grant_type | OAuth grant type |
Event metadata keys:
| Constant | Key | Usage |
|---|---|---|
event.DataKey.Message | message | Human-readable message |
event.DataKey.Error | error | Error message for failures |
event.DataKey.ErrorCode | error_code | Machine-readable error code |
event.DataKey.ErrorType | error_type | Error classification |
event.DataKey.DurationMs | duration_ms | Operation duration in milliseconds |
event.DataKey.LatencyUs | latency_us | Operation latency in microseconds |
event.DataKey.TraceParent | trace_parent | W3C traceparent header value for OTel span linking |
Distributed Tracing
Events with the same TraceID are automatically grouped into a single trace by the OpenTelemetry subscriber.
Hierarchical Tracing
To create parent-child relationships between spans, include the TraceParent key:
// Child operation
childEvt := event.NewEvent(traceID, event.EventTypeFlowNodeExecutionStarted, event.ComponentFlowEngine).
WithData(event.DataKey.TraceParent, parentSpanID)
obs.PublishEvent(childEvt)
Architecture Overview
The Observability component follows a Publisher-Subscriber pattern, decoupled from core business logic.
Core Components
- Service (
Service): The main entry point. Manages lifecycle and configuration. - Publisher (
CategoryPublisher): Acts as the event bus. Distributes events to subscribers based on categories. - Subscribers (
SubscriberInterface): Consume events (e.g., Console, File, OTel).
Directory Structure
backend/internal/system/observability/
├── service.go # Main service implementation
├── event/ # Event definitions
├── publisher/ # Publisher implementation
├── subscriber/ # Subscriber implementations
└── opentelemetry/ # OpenTelemetry configuration
Extending Observability
To add a new subscriber (e.g., to send logs to a webhook):
- Create a new file in
backend/internal/system/observability/subscriber/. - Implement
SubscriberInterface:type SubscriberInterface interface {
Initialize() error
IsEnabled() bool
GetID() string
GetCategories() []event.EventCategory
OnEvent(evt *event.Event) error
Close() error
} - Register the Factory:
Add an
init()function to register your subscriber factory.func init() {
RegisterSubscriberFactory("my-subscriber", func() SubscriberInterface {
return NewMySubscriber()
})
} - Add Configuration: Update
backend/internal/system/config/config.go.