Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions docs/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This document describes the observability architecture for the full-stack-fastap
```
Browser (React + OTEL Web SDK)
│ W3C traceparent headers via OTEL XHR instrumentation
│ W3C traceparent headers via OpenAPI interceptor + OTEL context
Traefik
Expand Down Expand Up @@ -58,9 +58,12 @@ This document describes the observability architecture for the full-stack-fastap

### Trace Context Propagation

Frontend-to-backend trace continuity is achieved through OpenTelemetry browser instrumentation in `frontend/src/telemetry.ts`. `XMLHttpRequestInstrumentation` automatically creates client spans for Axios requests and injects W3C `traceparent` / `tracestate` headers.
Frontend-to-backend trace continuity is intentionally split into two layers:

`WebTracerProvider` in the browser registers the default W3C TraceContext propagator, so requests stay correlated without patching generated client files under `frontend/src/client/`. The backend's FastAPI auto-instrumentation extracts those headers and creates child spans, resulting in a single trace across frontend → backend → database.
1. `frontend/src/telemetry.ts` initializes the Web SDK so browser spans exist (document load + XHR client spans).
2. `frontend/src/telemetry-interceptor.ts` registers `OpenAPI.interceptors.request` middleware that injects W3C `traceparent` / `tracestate` headers into every generated API client request.

This is an educational pattern: it teaches explicit context propagation at the client boundary while keeping generated files under `frontend/src/client/` untouched. The backend's FastAPI auto-instrumentation extracts those headers and creates child spans, producing a single trace across frontend → backend → database.
Comment thread
ncolesummers marked this conversation as resolved.
Outdated

### Metrics Model

Expand Down Expand Up @@ -278,5 +281,6 @@ def my_function():

### Frontend traces not linking to backend
- Verify `traceparent` header is present in browser DevTools Network tab
- Confirm `registerTraceContextInterceptor()` is called in `frontend/src/main.tsx`
- Check CORS allows the `traceparent` header (should be covered by `allow_headers=["*"]`)
- Ensure both frontend and backend OTEL are sending to the same Collector instance
2 changes: 2 additions & 0 deletions frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import { Toaster } from "./components/ui/sonner"
import "./index.css"
import { routeTree } from "./routeTree.gen"
import { initTelemetry } from "./telemetry"
import { registerTraceContextInterceptor } from "./telemetry-interceptor"

initTelemetry()
registerTraceContextInterceptor()

OpenAPI.BASE = import.meta.env.VITE_API_URL
OpenAPI.TOKEN = async () => {
Expand Down
54 changes: 54 additions & 0 deletions frontend/src/telemetry-interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { context, propagation } from "@opentelemetry/api"
import type { AxiosRequestConfig } from "axios"
import { OpenAPI } from "./client"

let traceContextInterceptorRegistered = false

const toHeaderRecord = (
headers: AxiosRequestConfig["headers"],
): Record<string, string> => {
if (!headers) {
return {}
}

const maybeAxiosHeaders = headers as {
toJSON?: () => Record<string, unknown>
}
const serializedHeaders =
typeof maybeAxiosHeaders.toJSON === "function"
? maybeAxiosHeaders.toJSON()
: (headers as Record<string, unknown>)

return Object.fromEntries(
Object.entries(serializedHeaders)
.filter(([, value]) => value !== undefined && value !== null)
.map(([key, value]) => [key, String(value)]),
)
}

export const traceContextInterceptor = (
config: AxiosRequestConfig,
): AxiosRequestConfig => {
const traceHeaders: Record<string, string> = {}
propagation.inject(context.active(), traceHeaders)

if (Object.keys(traceHeaders).length === 0) {
return config
}

config.headers = {
...toHeaderRecord(config.headers),
...traceHeaders,
}

return config
Comment thread
ncolesummers marked this conversation as resolved.
}

export const registerTraceContextInterceptor = (): void => {
if (traceContextInterceptorRegistered) {
return
}

OpenAPI.interceptors.request.use(traceContextInterceptor)
traceContextInterceptorRegistered = true
}
Comment thread
ncolesummers marked this conversation as resolved.