Skip to main content

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

  1. Integration Guide
  2. Event Anatomy
  3. Distributed Tracing
  4. Architecture Overview
  5. Extending Observability

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:

ConstantValueUsage
event.ComponentFlowEngineFlowEngineEvents from the flow execution engine
event.ComponentAuthHandlerAuthHandlerEvents from authentication handlers

Event Anatomy

Required Fields

  • TraceID: UUID or hex string for trace correlation.
  • EventType: Predefined constant from the event package (e.g., event.EventTypeFlowStarted).
  • Component: Your component name. Use a predefined constant from the event package where one exists.

Optional Fields

  • Status: event.StatusSuccess, event.StatusFailure, or event.StatusInProgress.
  • Data: Key-value pairs using event.DataKey constants.

Defined Event Types

Always use the predefined constants. Do not pass raw strings as event types.

Authentication events (category: observability.authentication):

ConstantValueDescription
event.EventTypeTokenIssuanceStartedTOKEN_ISSUANCE_STARTEDToken issuance begins
event.EventTypeTokenIssuedTOKEN_ISSUEDToken successfully issued
event.EventTypeTokenIssuanceFailedTOKEN_ISSUANCE_FAILEDToken issuance failed

Flow execution events (category: observability.flows):

ConstantValueDescription
event.EventTypeFlowStartedFLOW_STARTEDFlow execution begins
event.EventTypeFlowNodeExecutionStartedFLOW_NODE_EXECUTION_STARTEDA flow node begins executing
event.EventTypeFlowNodeExecutionCompletedFLOW_NODE_EXECUTION_COMPLETEDA flow node completes successfully
event.EventTypeFlowNodeExecutionFailedFLOW_NODE_EXECUTION_FAILEDA flow node fails
event.EventTypeFlowUserInputRequiredFLOW_USER_INPUT_REQUIREDFlow pauses waiting for user input
event.EventTypeFlowCompletedFLOW_COMPLETEDFlow execution succeeds
event.EventTypeFlowFailedFLOW_FAILEDFlow 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.

CategoryDescription
observability.authenticationToken issuance events
observability.authorizationAuthorization-related events
observability.flowsFlow execution events
observability.allSpecial 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:

ConstantKeyUsage
event.DataKey.UserIDuser_idAuthenticated user identifier
event.DataKey.UsernameusernameUsername
event.DataKey.ClientIDclient_idOAuth client identifier
event.DataKey.EntityIDapp_idApplication identifier

Flow execution keys:

ConstantKeyUsage
event.DataKey.ExecutionIDexecution_idFlow execution identifier
event.DataKey.FlowTypeflow_typeType of flow being executed
event.DataKey.NodeIDnode_idFlow node identifier
event.DataKey.NodeTypenode_typeFlow node type
event.DataKey.NodeStatusnode_statusExecution status of the node
event.DataKey.ExecutorNameexecutor_nameName of the executor running the node
event.DataKey.ExecutorTypeexecutor_typeType of the executor
event.DataKey.StepNumberstep_numberStep number within the flow
event.DataKey.AttemptNumberattempt_numberRetry attempt number
event.DataKey.AuthMethodauth_methodAuthentication method used
event.DataKey.RedirectToredirect_toRedirect destination
event.DataKey.FailedStepfailed_stepStep at which the flow failed

OAuth and token keys:

ConstantKeyUsage
event.DataKey.ScopescopeOAuth scopes
event.DataKey.GrantTypegrant_typeOAuth grant type

Event metadata keys:

ConstantKeyUsage
event.DataKey.MessagemessageHuman-readable message
event.DataKey.ErrorerrorError message for failures
event.DataKey.ErrorCodeerror_codeMachine-readable error code
event.DataKey.ErrorTypeerror_typeError classification
event.DataKey.DurationMsduration_msOperation duration in milliseconds
event.DataKey.LatencyUslatency_usOperation latency in microseconds
event.DataKey.TraceParenttrace_parentW3C 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

  1. Service (Service): The main entry point. Manages lifecycle and configuration.
  2. Publisher (CategoryPublisher): Acts as the event bus. Distributes events to subscribers based on categories.
  3. 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):

  1. Create a new file in backend/internal/system/observability/subscriber/.
  2. Implement SubscriberInterface:
    type SubscriberInterface interface {
    Initialize() error
    IsEnabled() bool
    GetID() string
    GetCategories() []event.EventCategory
    OnEvent(evt *event.Event) error
    Close() error
    }
  3. Register the Factory: Add an init() function to register your subscriber factory.
    func init() {
    RegisterSubscriberFactory("my-subscriber", func() SubscriberInterface {
    return NewMySubscriber()
    })
    }
  4. Add Configuration: Update backend/internal/system/config/config.go.

Further Reading

ThunderID LogoThunderID Logo

Product

DocsAPIsSDKs
© WSO2 LLC. All rights reserved.Privacy PolicyCookie Policy