Title here
Summary here
This guide shows developers how to integrate timeline events into platform components, connectors, and custom services.
The Timeline Service uses CloudEvents v1.0 as the standard event format, with incident management extensions for metadata and policy enforcement.
┌──────────────┐ CloudEvents ┌──────────────────┐
│ Your │────────────────────► │ Timeline │
│ Service │ (append event) │ Service │
└──────────────┘ └────────┬─────────┘
│
│ (query/export)
▼
┌──────────────────┐
│ Event Bus │
│ (notifications) │
└──────────────────┘
go get github.com/systmms/incidentsimport (
"github.com/systmms/incidents/internal/timeline"
"github.com/systmms/incidents/internal/models"
"github.com/systmms/incidents/internal/storage"
)func main() {
// Initialize database connection
db, err := storage.New("postgres://user:pass@localhost/incidents")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Create timeline service
svc := timeline.New(db)
// Use the service to append/query events
// ... (see examples below)
}func declareIncident(svc *timeline.Service, incidentID string) error {
// Create timeline event
event := &models.TimelineEvent{
IncidentID: incidentID,
EventType: models.EventTypeDeclare,
Actor: "alice@example.com",
Title: "Database Connection Pool Exhausted",
Content: "Payment API experiencing connection timeouts",
EventTime: time.Now(),
Source: "api",
Data: map[string]any{
"severity": "SEV-2",
"service": "payments-api",
"region": "us-east-1",
},
}
// Create CloudEvent wrapper
ce := &models.CloudEvent{
SpecVersion: "1.0",
ID: uuid.New().String(),
Source: "im://api",
Type: models.EventTypeDeclare,
Subject: fmt.Sprintf("incident/%s", incidentID),
Time: event.EventTime,
Data: event.Data,
Extensions: map[string]any{
models.ExtIncidentID: incidentID,
models.ExtActor: event.Actor,
},
}
// Append to timeline
ctx := context.Background()
err := svc.AppendEvent(ctx, event, ce)
if err != nil {
return fmt.Errorf("failed to append event: %w", err)
}
return nil
}Use im.origin_id extension for idempotency:
func appendAlertEvent(svc *timeline.Service, incidentID, alertID string) error {
event := &models.TimelineEvent{
IncidentID: incidentID,
EventType: models.EventTypeAlertTrigger,
Actor: "system:pagerduty",
Title: "High CPU Usage Alert",
EventTime: time.Now(),
Source: "pagerduty",
Data: map[string]any{
"alert_id": alertID,
"metric": "cpu.usage",
"value": 95.2,
},
}
ce := &models.CloudEvent{
SpecVersion: "1.0",
ID: uuid.New().String(),
Source: "im://pagerduty",
Type: models.EventTypeAlertTrigger,
Time: event.EventTime,
Extensions: map[string]any{
models.ExtIncidentID: incidentID,
models.ExtOriginID: fmt.Sprintf("pagerduty:alert:%s", alertID),
models.ExtActor: event.Actor,
},
}
ctx := context.Background()
err := svc.AppendEvent(ctx, event, ce)
// Note: Duplicate events (same origin_id) return nil error
// but do not create a new entry
return err
}Group related events across incidents:
func appendCorrelatedEvent(svc *timeline.Service, incidentID, deploymentID string) error {
ce := &models.CloudEvent{
SpecVersion: "1.0",
ID: uuid.New().String(),
Source: "im://ci-cd",
Type: "im.deployment.complete",
Time: time.Now(),
Extensions: map[string]any{
models.ExtIncidentID: incidentID,
models.ExtCorrelationKey: fmt.Sprintf("deployment:%s", deploymentID),
models.ExtActor: "system:github-actions",
},
}
event := &models.TimelineEvent{
IncidentID: incidentID,
EventType: "im.deployment.complete",
Actor: "system:github-actions",
Title: "Hotfix Deployment Completed",
EventTime: ce.Time,
Source: "ci-cd",
}
ctx := context.Background()
return svc.AppendEvent(ctx, event, ce)
}Mark events containing PII/PHI for automatic redaction:
func appendCustomerImpactEvent(svc *timeline.Service, incidentID string) error {
event := &models.TimelineEvent{
IncidentID: incidentID,
EventType: "im.incident.customer_impact",
Actor: "support@example.com",
Title: "Customer Reported Issue",
EventTime: time.Now(),
Source: "support-portal",
Data: map[string]any{
"customer_email": "customer@example.com", // Will be redacted
"customer_name": "Jane Doe", // Will be redacted
"ticket_id": "SUP-5678", // Not redacted
},
}
ce := &models.CloudEvent{
SpecVersion: "1.0",
ID: uuid.New().String(),
Source: "im://support",
Type: "im.incident.customer_impact",
Time: event.EventTime,
Extensions: map[string]any{
models.ExtIncidentID: incidentID,
models.ExtDataClass: []string{"pii"}, // Mark as PII
models.ExtVisibility: "restricted", // Restrict access
models.ExtActor: event.Actor,
},
}
ctx := context.Background()
return svc.AppendEvent(ctx, event, ce)
}func getIncidentTimeline(svc *timeline.Service, incidentID string) ([]*models.TimelineEvent, error) {
ctx := context.Background()
// Get last 100 events
events, err := svc.GetTimeline(ctx, incidentID, 100)
if err != nil {
return nil, fmt.Errorf("failed to get timeline: %w", err)
}
return events, nil
}func queryFilteredTimeline(svc *timeline.Service, incidentID string) (*models.TimelineQueryResult, error) {
// Parse time range
from, _ := time.Parse(time.RFC3339, "2025-12-10T00:00:00Z")
to, _ := time.Parse(time.RFC3339, "2025-12-10T23:59:59Z")
query := &models.TimelineQuery{
IncidentID: incidentID,
EventTypePattern: "im.incident.*", // Only incident events
FromTime: &from,
ToTime: &to,
Actor: "alice@example.com",
Limit: 50,
Order: "desc", // Newest first
}
ctx := context.Background()
result, err := svc.QueryTimeline(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to query timeline: %w", err)
}
return result, nil
}func paginatedQuery(svc *timeline.Service, incidentID string) error {
query := &models.TimelineQuery{
IncidentID: incidentID,
Limit: 50,
Order: "asc", // Oldest first
}
ctx := context.Background()
// Fetch all pages
for {
result, err := svc.QueryTimeline(ctx, query)
if err != nil {
return err
}
// Process events
for _, event := range result.Events {
fmt.Printf("[%s] %s: %s\n",
event.EventTime.Format(time.RFC3339),
event.EventType,
event.Title)
}
// Check for more results
if result.NextCursor == "" {
break
}
// Update cursor for next page
query.Cursor = result.NextCursor
}
return nil
}func queryByCorrelationKey(svc *timeline.Service, correlationKey string) ([]*models.TimelineEvent, error) {
query := &models.TimelineQuery{
CorrelationKey: correlationKey,
Limit: 100,
Order: "asc",
}
ctx := context.Background()
result, err := svc.QueryTimeline(ctx, query)
if err != nil {
return nil, err
}
return result.Events, nil
}package models
const (
// Incident lifecycle
EventTypeDeclare = "im.incident.declare"
EventTypeAcknowledge = "im.incident.acknowledge"
EventTypeEscalate = "im.incident.escalate"
EventTypeResolve = "im.incident.resolve"
EventTypeClose = "im.incident.close"
EventTypeReopen = "im.incident.reopen"
// Alerts
EventTypeAlertTrigger = "im.alert.trigger"
EventTypeAlertAck = "im.alert.acknowledge"
EventTypeAlertResolve = "im.alert.resolve"
// Collaboration
EventTypeNoteAdd = "im.note.add"
EventTypeStatusUpdate = "im.status_update.post"
EventTypeRunbook = "im.runbook.execute"
// Integration sync
EventTypeServiceNowSync = "im.servicenow.sync"
EventTypeJiraSync = "im.jira.sync"
EventTypeSlackMessage = "im.slack.message"
)Define custom event types following the im.* namespace:
const (
// Custom deployment events
EventTypeDeploymentStart = "im.deployment.start"
EventTypeDeploymentComplete = "im.deployment.complete"
EventTypeDeploymentRollback = "im.deployment.rollback"
// Custom monitoring events
EventTypeThresholdBreach = "im.monitor.threshold_breach"
EventTypeAnomalyDetected = "im.monitor.anomaly_detected"
)const (
// Required for timeline storage
ExtIncidentID = "im.incident_id" // Links to incident
ExtActor = "im.actor" // Who/what triggered
// Idempotency and grouping
ExtOriginID = "im.origin_id" // Dedupe key from source
ExtCorrelationKey = "im.correlation_key" // Cross-event grouping
ExtFingerprint = "im.fingerprint" // Auto-aggregation
// Related events
ExtRelatedIDs = "im.related_ids" // Parent/child refs
// Security and access control
ExtDataClass = "im.data_class" // ["pii", "phi", "financial"]
ExtVisibility = "im.visibility" // "public", "restricted"
ExtRedactionApplied = "im.redaction_applied" // Set by PEP
// Metadata
ExtTags = "im.tags" // Categorical tags
)// Example 1: Basic event
extensions := map[string]any{
models.ExtIncidentID: "INC-1234",
models.ExtActor: "alice@example.com",
}
// Example 2: Idempotent event from external source
extensions := map[string]any{
models.ExtIncidentID: "INC-1234",
models.ExtOriginID: "pagerduty:incident:ABCD123",
models.ExtActor: "system:pagerduty",
}
// Example 3: Correlated events
extensions := map[string]any{
models.ExtIncidentID: "INC-1234",
models.ExtCorrelationKey: "deployment:v2.5.1",
models.ExtActor: "system:github-actions",
}
// Example 4: Sensitive data with access control
extensions := map[string]any{
models.ExtIncidentID: "INC-1234",
models.ExtDataClass: []string{"pii", "financial"},
models.ExtVisibility: "confidential",
models.ExtActor: "billing@example.com",
}
// Example 5: Related events (parent/child)
extensions := map[string]any{
models.ExtIncidentID: "INC-1235",
models.ExtRelatedIDs: []string{"INC-1234"}, // Parent incident
models.ExtActor: "alice@example.com",
}type PagerDutyWebhook struct {
timelineSvc *timeline.Service
}
func (h *PagerDutyWebhook) HandleIncidentTrigger(w http.ResponseWriter, r *http.Request) {
var payload PagerDutyPayload
json.NewDecoder(r.Body).Decode(&payload)
// Map to incident ID (or create new incident)
incidentID := h.getOrCreateIncident(payload.IncidentKey)
// Create timeline event
event := &models.TimelineEvent{
IncidentID: incidentID,
EventType: models.EventTypeAlertTrigger,
Actor: "system:pagerduty",
Title: payload.IncidentTitle,
EventTime: payload.CreatedAt,
Source: "pagerduty",
Data: payload,
}
ce := &models.CloudEvent{
SpecVersion: "1.0",
ID: uuid.New().String(),
Source: "im://pagerduty",
Type: models.EventTypeAlertTrigger,
Time: payload.CreatedAt,
Extensions: map[string]any{
models.ExtOriginID: fmt.Sprintf("pd:%s", payload.IncidentID),
models.ExtIncidentID: incidentID,
models.ExtActor: "system:pagerduty",
},
}
// Append (idempotent - duplicate webhooks ignored)
err := h.timelineSvc.AppendEvent(r.Context(), event, ce)
if err != nil {
http.Error(w, "Failed to append event", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}func syncServiceNowTickets(timelineSvc *timeline.Service, snowClient *ServiceNowClient) error {
// Query ServiceNow for updated tickets
since := time.Now().Add(-5 * time.Minute)
tickets, err := snowClient.GetUpdatedTickets(since)
if err != nil {
return err
}
// Append timeline event for each update
for _, ticket := range tickets {
incidentID := mapTicketToIncident(ticket.Number)
event := &models.TimelineEvent{
IncidentID: incidentID,
EventType: "im.servicenow.sync",
Actor: fmt.Sprintf("system:servicenow:%s", ticket.UpdatedBy),
Title: ticket.ShortDescription,
EventTime: ticket.UpdatedAt,
Source: "servicenow",
Data: ticket,
}
ce := &models.CloudEvent{
SpecVersion: "1.0",
ID: uuid.New().String(),
Source: "im://servicenow",
Type: "im.servicenow.sync",
Time: ticket.UpdatedAt,
Extensions: map[string]any{
models.ExtOriginID: fmt.Sprintf("snow:%s:%s", ticket.SysID, ticket.SysModCount),
models.ExtIncidentID: incidentID,
},
}
// Idempotent append - same sys_mod_count ignored
if err := timelineSvc.AppendEvent(context.Background(), event, ce); err != nil {
log.Printf("Failed to sync ticket %s: %v", ticket.Number, err)
}
}
return nil
}func subscribeToIncidentEvents(timelineSvc *timeline.Service, eventBus *EventBus) {
// Subscribe to incident notifications
eventBus.Subscribe("im.incident.*", func(notification Event) {
// Extract event details
incidentID := notification.Subject
event := &models.TimelineEvent{
IncidentID: incidentID,
EventType: notification.Type,
Actor: notification.GetExtension(models.ExtActor).(string),
Title: notification.Summary,
EventTime: notification.Time,
Source: "event-bus",
Data: notification.Data,
}
ce := &models.CloudEvent{
SpecVersion: "1.0",
ID: notification.ID,
Source: notification.Source,
Type: notification.Type,
Time: notification.Time,
Extensions: notification.Extensions,
}
// Append to timeline
ctx := context.Background()
if err := timelineSvc.AppendEvent(ctx, event, ce); err != nil {
log.Printf("Failed to append event: %v", err)
}
})
}func TestAppendEvent_Idempotency(t *testing.T) {
// Setup
db := storage.NewTestDB(t)
svc := timeline.New(db)
incidentID := "INC-TEST-1234"
originID := "test:event:1"
event := &models.TimelineEvent{
IncidentID: incidentID,
EventType: models.EventTypeDeclare,
Actor: "test@example.com",
Title: "Test Event",
EventTime: time.Now(),
Source: "test",
}
ce := &models.CloudEvent{
SpecVersion: "1.0",
ID: uuid.New().String(),
Source: "im://test",
Type: models.EventTypeDeclare,
Time: event.EventTime,
Extensions: map[string]any{
models.ExtOriginID: originID,
models.ExtIncidentID: incidentID,
models.ExtActor: event.Actor,
},
}
ctx := context.Background()
// First append
err := svc.AppendEvent(ctx, event, ce)
require.NoError(t, err)
// Get timeline
events1, err := svc.GetTimeline(ctx, incidentID, 100)
require.NoError(t, err)
require.Len(t, events1, 1)
firstEventID := events1[0].ID
// Second append with same origin_id
err = svc.AppendEvent(ctx, event, ce)
require.NoError(t, err) // No error on duplicate
// Verify no duplicate created
events2, err := svc.GetTimeline(ctx, incidentID, 100)
require.NoError(t, err)
require.Len(t, events2, 1)
require.Equal(t, firstEventID, events2[0].ID)
}// Good: Idempotent
extensions := map[string]any{
models.ExtOriginID: "pagerduty:alert:ABC123",
}
// Bad: Duplicate events on webhook retries
extensions := map[string]any{
// No origin_id - relies only on UUID
}// Public event (default)
extensions := map[string]any{
models.ExtVisibility: "public",
}
// Restricted event (requires attribute)
extensions := map[string]any{
models.ExtVisibility: "restricted",
}
// Confidential event
extensions := map[string]any{
models.ExtVisibility: "confidential",
}// Mark PII for automatic redaction
extensions := map[string]any{
models.ExtDataClass: []string{"pii"},
}
// Mark multiple classifications
extensions := map[string]any{
models.ExtDataClass: []string{"pii", "financial"},
}// Group events from same deployment
extensions := map[string]any{
models.ExtCorrelationKey: "deployment:v2.5.1",
}
// Query all related events later
query := &models.TimelineQuery{
CorrelationKey: "deployment:v2.5.1",
}func appendEventWithRetry(svc *timeline.Service, event *models.TimelineEvent, ce *models.CloudEvent) error {
ctx := context.Background()
// Retry with exponential backoff
return retry.Do(
func() error {
return svc.AppendEvent(ctx, event, ce)
},
retry.Attempts(3),
retry.Delay(100 * time.Millisecond),
)
}