Skip to main content

Chapter 41: Monitoring & Observability

You cannot fix what you cannot see. Observability encompasses three pillars: traces (request flows), metrics (quantitative measurements), and logs (event records). This chapter sets up OpenTelemetry for full-stack observability.

OpenTelemetry Setup​

Backend (Node.js)​

// src/server/telemetry.ts
import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { Resource } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";

const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: "taskforge-api",
[ATTR_SERVICE_VERSION]: process.env.npm_package_version ?? "0.0.0",
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318/v1/traces",
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318/v1/metrics",
}),
}),
instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

// Graceful shutdown
process.on("SIGTERM", () => {
sdk.shutdown().then(() => process.exit(0));
});

Frontend (Browser)​

// src/shared/lib/telemetry.ts
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { ZoneContextManager } from "@opentelemetry/context-zone";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
import { Resource } from "@opentelemetry/resources";

export function initTelemetry() {
if (import.meta.env.PROD) {
const provider = new WebTracerProvider({
resource: new Resource({
"service.name": "taskforge-web",
}),
});

provider.addSpanProcessor(
new BatchSpanProcessor(
new OTLPTraceExporter({
url: "/api/telemetry/traces",
})
)
);

provider.register({
contextManager: new ZoneContextManager(),
});

registerInstrumentations({
instrumentations: [
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [/\/api\//],
}),
],
});
}
}

Structured Logging with Effect​

// Effect has built-in logging that integrates with OpenTelemetry
import { Effect, Logger, LogLevel } from "effect";

// Configure log level from environment
const logLevel = import.meta.env.VITE_LOG_LEVEL === "debug"
? LogLevel.Debug
: LogLevel.Info;

// In your Effect programs:
const createProject = (input: CreateProjectInput) =>
Effect.gen(function* () {
yield* Effect.log(`Creating project: ${input.name}`);

const project = yield* repo.create(input);

yield* Effect.logInfo(`Project created successfully`, {
projectId: project.id,
organizationId: input.organizationId,
});

return project;
}).pipe(
Effect.withLogSpan("createProject"),
Effect.annotateLogs({ module: "projects" }),
);

Health Checks​

// API health check endpoint
app.get("/api/health", async (req, res) => {
const checks = {
status: "ok",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
checks: {
database: await checkDatabase(),
redis: await checkRedis(),
},
};

const allHealthy = Object.values(checks.checks).every((c) => c.status === "ok");
res.status(allHealthy ? 200 : 503).json(checks);
});

async function checkDatabase() {
try {
await prisma.$queryRaw`SELECT 1`;
return { status: "ok" };
} catch (error) {
return { status: "error", message: String(error) };
}
}

Web Vitals Monitoring​

// Report Web Vitals to your analytics/monitoring service
import { onCLS, onINP, onLCP } from "web-vitals";

function reportMetric(metric: { name: string; value: number; id: string }) {
// Send to your monitoring service
navigator.sendBeacon("/api/telemetry/vitals", JSON.stringify(metric));
}

onCLS(reportMetric);
onINP(reportMetric);
onLCP(reportMetric);

Error Tracking​

// Catch unhandled errors and report them
window.addEventListener("unhandledrejection", (event) => {
reportError({
type: "unhandled_rejection",
message: event.reason?.message ?? String(event.reason),
stack: event.reason?.stack,
url: window.location.href,
});
});

window.addEventListener("error", (event) => {
reportError({
type: "uncaught_error",
message: event.message,
stack: event.error?.stack,
url: window.location.href,
line: event.lineno,
column: event.colno,
});
});

Observability Stack with Docker Compose​

# docker-compose.observability.yml
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
command: ["--config", "/etc/otelcol/config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otelcol/config.yaml
ports:
- "4317:4317" # gRPC
- "4318:4318" # HTTP

jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # Jaeger UI
environment:
COLLECTOR_OTLP_ENABLED: "true"

prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"

grafana:
image: grafana/grafana:latest
ports:
- "3001:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: admin

Summary​

  • ✅ OpenTelemetry for vendor-neutral tracing and metrics
  • ✅ Auto-instrumentation captures HTTP requests, database queries
  • ✅ Structured logging with Effect's built-in logger
  • ✅ Health checks for monitoring infrastructure dependencies
  • ✅ Web Vitals tracking for frontend performance
  • ✅ Error tracking for unhandled exceptions
  • ✅ Observability stack with Jaeger, Prometheus, Grafana via Docker