Observability in TypeScript: from black box to transparent system
\n\nImagine your TypeScript application is a complex mechanism of thousands of gears. Everything works perfectly until it breaks. And then you get a message: "The site is slow." Where do you look for the problem? Database? API? Or did one of the microservices just crash?
\n\nWithout observability, you'll be guessing like a shaman with a tambourine. With observability, you see the whole picture: where performance dropped, which request caused an error, and why memory leaked.
\n\nIn this article, we'll break down the three pillars of observability — tracing, metrics, and logs — and learn how to implement them in TypeScript applications with real-world examples. You'll learn how to turn your application from a black box into a transparent system that can be diagnosed in minutes.
\n\nWhy is observability critical for TypeScript applications?
\n\nTypeScript is not just a superset of JavaScript. It's a language used to write backends for high-load systems, microservices, and even CLI tools. And the more complex the system, the more important it is to understand what's happening inside it.
\n\nTraditional logging is good, but not enough. When you have 50 microservices, logs become white noise. You need:
\n\n- \n
- Tracing — to track the path of a single request through all services. \n
- Metrics — to see that RAM is growing like yeast. \n
- Structured logs — so you don't have to find a needle in a haystack. \n
Let's implement this in a real application.
\n\nTracing in TypeScript: OpenTelemetry in practice
\n\nTracing is the life story of a single request. You see how long each step took: from an HTTP request to a database query. In TypeScript, the de facto standard is OpenTelemetry.
\n\nInstalling and configuring OpenTelemetry
\n\nnpm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-jaeger\n\nLet's create a tracing.ts file to initialize tracing:
import { NodeSDK } from '@opentelemetry/sdk-node';\nimport { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';\nimport { JaegerExporter } from '@opentelemetry/exporter-jaeger';\nimport { Resource } from '@opentelemetry/resources';\nimport { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';\n\nconst jaegerExporter = new JaegerExporter({\n endpoint: 'http://localhost:14268/api/traces',\n serviceName: 'my-typescript-app',\n});\n\nconst sdk = new NodeSDK({\n resource: new Resource({\n [SemanticResourceAttributes.SERVICE_NAME]: 'my-typescript-app',\n [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',\n }),\n traceExporter: jaegerExporter,\n instrumentations: [getNodeAutoInstrumentations()],\n});\n\nsdk.start();\n\n\nNow every HTTP request, every database call will be automatically traced. You'll see a complete call map in the Jaeger UI (or Grafana Tempo).
\n\nManual span creation
\n\nAuto-instrumentation is great, but sometimes you need to add your own labels. For example, measure the execution time of complex business logic:
\n\nimport { trace } from '@opentelemetry/api';\n\nconst tracer = trace.getTracer('business-logic');\n\nasync function processOrder(orderId: string): Promise<void> {\n const span = tracer.startSpan('process-order', {\n attributes: { 'order.id': orderId },\n });\n\n try {\n // Your order processing logic\n await validateOrder(orderId);\n await chargePayment(orderId);\n await sendConfirmation(orderId);\n\n span.setStatus({ code: 1 }); // OK\n } catch (error) {\n span.recordException(error as Error);\n span.setStatus