Skip to content

Pattern 05 — No Observability vs. Full OTEL Stack

The difference between a prototype and a production system is often a single question: what happened? Without structured observability, the answer is always "we don't know."


❌ Before — The Invisible System

flowchart LR
    App["Application"]
    Print["print(response)\nlogging.info(prompt)"]
    LLM["LLM API"]
    User["User"]
    Incident["🔥 Incident\nWhy did it fail?\nWhich call was slow?\nWhat was in the prompt?\nHow much did it cost?"]

    App -->|"raw prompt\n(PII in logs)"| LLM
    LLM --> App
    App --> Print
    App --> User
    User -.->|"reports broken response"| Incident
    Incident -.->|"no data to debug with"| App

    style App fill:#1e1e2e,stroke:#585b70,color:#cdd6f4
    style Print fill:#3b1219,stroke:#f38ba8,color:#f38ba8
    style LLM fill:#313244,stroke:#585b70,color:#cdd6f4
    style User fill:#313244,stroke:#585b70,color:#cdd6f4
    style Incident fill:#3b1219,stroke:#f38ba8,color:#f38ba8

What you lose without observability:

  • No span-level timing — can't identify slow providers or slow tools
  • PII in plain-text logs — compliance violation waiting to happen
  • No cost attribution — no idea which feature or tenant drives spend
  • No agent hop visibility — can't reconstruct what the agent did
  • No policy decision log — can't prove what was blocked and why
  • No drift detection — degradation is invisible until users complain

✅ After — Full Structured Observability Stack

flowchart TD
    App["Application"]

    subgraph Observability["🔵 L02 Observability — ElectriPy AI"]
        OBS["ObservabilityService\nspan lifecycle + context propagation"]
        RED["DefaultRedactor\nPII/secrets masked before export"]
        TEL["AI Telemetry\nprovider-agnostic event adapters"]
        CL["CostLedger\nthread-safe token accumulation"]
        PF["PromptFingerprint\nSHA-256 dedup + drift detection"]
    end

    subgraph SpanTree["Span Tree (one request)"]
        W["workflow.summarise\n[WORKFLOW]  45ms"]
        POL["policy.preflight\n[POLICY]  2ms  action=allow"]
        LLM_S["llm.openai.gpt-4o\n[LLM]  38ms  tokens=842"]
        TOOL["retrieval.context_fetch\n[TOOL]  5ms  docs=5"]
    end

    subgraph Export["Export Targets"]
        OTEL["OpenTelemetry Collector\nJaeger / Grafana / Honeycomb"]
        JSONL["JSONL Sink\nlocal audit log"]
        MEM["InMemoryTracer\noffline tests — no network"]
    end

    App --> OBS
    OBS --> RED
    RED -->|"redacted attrs"| W
    W --> POL
    W --> LLM_S
    W --> TOOL
    LLM_S -->|"token count"| CL
    LLM_S -->|"prompt hash"| PF
    W & POL & LLM_S & TOOL -->|"finished spans"| TEL
    TEL --> OTEL
    TEL --> JSONL
    TEL --> MEM

    style Observability fill:#0d1a2e,stroke:#89dceb,color:#cdd6f4
    style SpanTree fill:#181825,stroke:#313244,color:#cdd6f4
    style Export fill:#0d2e0d,stroke:#a6e3a1,color:#cdd6f4

ElectriPy AI observability components:

Component Role
ObservabilityService Central service — creates, parents, and closes spans
InMemoryTracer Offline testing — captures all spans without network
OpenTelemetryTracer OTEL export to Jaeger, Honeycomb, Grafana, etc.
DefaultRedactor Redacts email, phone, SSN, credit card before span export
AI Telemetry Provider-agnostic adapters for HTTP, LLM, policy, RAG events
CostLedger Thread-safe cost accumulation — slice by model, tenant, feature
PromptFingerprint SHA-256 request hashing for dedup, caching, and drift detection
from electripy.observability.observe import (
    ObservabilityService, InMemoryTracer, DefaultRedactor,
)
from electripy.ai.cost_ledger import CostLedger
from electripy.ai.prompt_fingerprint import hash_prompt

# Wire once at startup
tracer = InMemoryTracer(redactor=DefaultRedactor())   # swap → OpenTelemetryTracer in prod
svc    = ObservabilityService(tracer=tracer)
ledger = CostLedger(cost_per_1k_tokens=0.010)

# Every request becomes a structured, nested trace
with svc.start_workflow_span("ai.summarise") as wf:
    wf.set_attribute("workflow.request_id", request_id)

    with svc.start_span("policy.preflight", kind=SpanKind.POLICY) as pol:
        decision = gateway.evaluate_preflight(prompt)
        pol.set_attribute("policy.action", decision.action)

    with svc.start_llm_span(provider="openai", model="gpt-4o") as llm:
        # prompt attr is auto-redacted before export
        llm.set_attribute("gen_ai.system_prompt", system_prompt)
        response = provider.complete(request)
        llm.set_attribute("gen_ai.usage.output_tokens", response.usage.total_tokens)
        ledger.record(tokens=response.usage.total_tokens, labels={"tenant": tenant_id})

# All spans now in tracer.finished_spans (or exported to OTEL backend)
# PII never left the process boundary
# Cost captured and sliceable by any label