{"slug":"observability-opentelemetry-guide","title":"Full-Stack Observability with OpenTelemetry: From Chaos to Clarity","description":"Implementing unified observability across 120+ microservices with OpenTelemetry, Prometheus, Grafana, and SLO-based alerting","date":"2025-03-10","author":"Elaph Hilful","thumbnail":"/blog-images/observability-stack.svg","tags":["Observability","OpenTelemetry","Prometheus","Grafana","SRE"],"category":"SRE","featured":false,"readTime":6,"htmlContent":"<p>When you're operating 120+ microservices processing 50TB of telemetry data per day, traditional monitoring breaks. Alert fatigue, disconnected tools, and 15-minute detection times become existential threats to reliability. OpenTelemetry provides the unified foundation to correlate traces, metrics, and logs into actionable insights.</p>\n<p>This is the story of building an observability platform that reduced alert noise by 92% and achieved sub-30-second mean time to detection.</p>\n<p><img src=\"/blog-images/observability-stack.svg\" alt=\"Full-Stack Observability Architecture\" loading=\"lazy\" decoding=\"async\"></p>\n<h2>The Problem: Observability Sprawl</h2>\n<p>Before the unified platform, our stack looked like this:</p>\n<ul>\n<li><strong>Metrics</strong>: Prometheus (fragmented across teams)</li>\n<li><strong>Logs</strong>: ELK Stack (slow queries, high cost)</li>\n<li><strong>Traces</strong>: Jaeger (partial coverage, no correlation)</li>\n<li><strong>Alerts</strong>: PagerDuty + custom scripts (500+ alerts/day)</li>\n</ul>\n<p>The result? Engineers spent 40 hours/week on incident triage, MTTD was 15+ minutes, and alert fatigue was causing real incidents to be missed.</p>\n<h2>The Three Pillars, Unified</h2>\n<p>OpenTelemetry provides a single SDK and protocol for all three telemetry signals. Here's how we deployed it across our Go and Node.js services:</p>\n<h3>Instrumentation: Auto + Manual</h3>\n<pre role=\"region\" aria-label=\"go code block\"><code class=\"hljs language-go\" tabindex=\"0\"><span class=\"hljs-comment\">// main.go - Go service with OpenTelemetry</span>\n<span class=\"hljs-keyword\">package</span> main\n\n<span class=\"hljs-keyword\">import</span> (\n    <span class=\"hljs-string\">\"context\"</span>\n    <span class=\"hljs-string\">\"go.opentelemetry.io/otel\"</span>\n    <span class=\"hljs-string\">\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp\"</span>\n    <span class=\"hljs-string\">\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp\"</span>\n    <span class=\"hljs-string\">\"go.opentelemetry.io/otel/sdk/trace\"</span>\n    <span class=\"hljs-string\">\"go.opentelemetry.io/otel/sdk/metric\"</span>\n    <span class=\"hljs-string\">\"go.opentelemetry.io/otel/sdk/resource\"</span>\n    semconv <span class=\"hljs-string\">\"go.opentelemetry.io/otel/semconv/v1.21.0\"</span>\n)\n\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">func</span> <span class=\"hljs-title\">initTelemetry</span><span class=\"hljs-params\">(ctx context.Context)</span></span> (<span class=\"hljs-function\"><span class=\"hljs-keyword\">func</span><span class=\"hljs-params\">()</span></span>, <span class=\"hljs-type\">error</span>) {\n    res := resource.NewWithAttributes(\n        semconv.SchemaURL,\n        semconv.ServiceName(<span class=\"hljs-string\">\"payment-service\"</span>),\n        semconv.ServiceVersion(<span class=\"hljs-string\">\"1.2.0\"</span>),\n        semconv.DeploymentEnvironment(<span class=\"hljs-string\">\"production\"</span>),\n    )\n\n    <span class=\"hljs-comment\">// Trace exporter</span>\n    traceExporter, _ := otlptracehttp.New(ctx,\n        otlptracehttp.WithEndpoint(<span class=\"hljs-string\">\"otel-collector:4318\"</span>),\n        otlptracehttp.WithInsecure(),\n    )\n\n    tp := trace.NewTracerProvider(\n        trace.WithBatcher(traceExporter),\n        trace.WithResource(res),\n        trace.WithSampler(trace.ParentBased(\n            trace.TraceIDRatioBased(<span class=\"hljs-number\">0.1</span>), <span class=\"hljs-comment\">// 10% sampling</span>\n        )),\n    )\n    otel.SetTracerProvider(tp)\n\n    <span class=\"hljs-comment\">// Metric exporter</span>\n    metricExporter, _ := otlpmetrichttp.New(ctx,\n        otlpmetrichttp.WithEndpoint(<span class=\"hljs-string\">\"otel-collector:4318\"</span>),\n    )\n\n    mp := metric.NewMeterProvider(\n        metric.WithReader(metric.NewPeriodicReader(metricExporter)),\n        metric.WithResource(res),\n    )\n    otel.SetMeterProvider(mp)\n\n    <span class=\"hljs-keyword\">return</span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">func</span><span class=\"hljs-params\">()</span></span> {\n        tp.Shutdown(ctx)\n        mp.Shutdown(ctx)\n    }, <span class=\"hljs-literal\">nil</span>\n}\n</code></pre>\n<h3>OpenTelemetry Collector: The Central Hub</h3>\n<p>The Collector receives, processes, and exports telemetry to backend stores:</p>\n<pre role=\"region\" aria-label=\"yaml code block\"><code class=\"hljs language-yaml\" tabindex=\"0\"><span class=\"hljs-comment\"># otel-collector-config.yaml</span>\n<span class=\"hljs-attr\">receivers:</span>\n  <span class=\"hljs-attr\">otlp:</span>\n    <span class=\"hljs-attr\">protocols:</span>\n      <span class=\"hljs-attr\">grpc:</span>\n        <span class=\"hljs-attr\">endpoint:</span> <span class=\"hljs-number\">0.0</span><span class=\"hljs-number\">.0</span><span class=\"hljs-number\">.0</span><span class=\"hljs-string\">:4317</span>\n      <span class=\"hljs-attr\">http:</span>\n        <span class=\"hljs-attr\">endpoint:</span> <span class=\"hljs-number\">0.0</span><span class=\"hljs-number\">.0</span><span class=\"hljs-number\">.0</span><span class=\"hljs-string\">:4318</span>\n\n  <span class=\"hljs-comment\"># Scrape Prometheus endpoints from Kubernetes pods</span>\n  <span class=\"hljs-attr\">prometheus:</span>\n    <span class=\"hljs-attr\">config:</span>\n      <span class=\"hljs-attr\">scrape_configs:</span>\n        <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">job_name:</span> <span class=\"hljs-string\">\"kubernetes-pods\"</span>\n          <span class=\"hljs-attr\">kubernetes_sd_configs:</span>\n            <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">role:</span> <span class=\"hljs-string\">pod</span>\n          <span class=\"hljs-attr\">relabel_configs:</span>\n            <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">source_labels:</span> [<span class=\"hljs-string\">__meta_kubernetes_pod_annotation_prometheus_io_scrape</span>]\n              <span class=\"hljs-attr\">action:</span> <span class=\"hljs-string\">keep</span>\n              <span class=\"hljs-attr\">regex:</span> <span class=\"hljs-literal\">true</span>\n\n<span class=\"hljs-attr\">processors:</span>\n  <span class=\"hljs-attr\">batch:</span>\n    <span class=\"hljs-attr\">timeout:</span> <span class=\"hljs-string\">5s</span>\n    <span class=\"hljs-attr\">send_batch_size:</span> <span class=\"hljs-number\">1024</span>\n\n  <span class=\"hljs-comment\"># Add Kubernetes metadata to all signals</span>\n  <span class=\"hljs-attr\">k8sattributes:</span>\n    <span class=\"hljs-attr\">extract:</span>\n      <span class=\"hljs-attr\">metadata:</span>\n        <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">k8s.pod.name</span>\n        <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">k8s.namespace.name</span>\n        <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">k8s.deployment.name</span>\n        <span class=\"hljs-bullet\">-</span> <span class=\"hljs-string\">k8s.node.name</span>\n\n  <span class=\"hljs-comment\"># Tail-based sampling: keep error traces, sample normal ones</span>\n  <span class=\"hljs-attr\">tail_sampling:</span>\n    <span class=\"hljs-attr\">policies:</span>\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">errors-always</span>\n        <span class=\"hljs-attr\">type:</span> <span class=\"hljs-string\">status_code</span>\n        <span class=\"hljs-attr\">status_code:</span> { <span class=\"hljs-attr\">status_codes:</span> [<span class=\"hljs-string\">ERROR</span>] }\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">slow-traces</span>\n        <span class=\"hljs-attr\">type:</span> <span class=\"hljs-string\">latency</span>\n        <span class=\"hljs-attr\">latency:</span> { <span class=\"hljs-attr\">threshold_ms:</span> <span class=\"hljs-number\">1000</span> }\n      <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">probabilistic</span>\n        <span class=\"hljs-attr\">type:</span> <span class=\"hljs-string\">probabilistic</span>\n        <span class=\"hljs-attr\">probabilistic:</span> { <span class=\"hljs-attr\">sampling_percentage:</span> <span class=\"hljs-number\">10</span> }\n\n  <span class=\"hljs-comment\"># Memory limiter to prevent OOM</span>\n  <span class=\"hljs-attr\">memory_limiter:</span>\n    <span class=\"hljs-attr\">check_interval:</span> <span class=\"hljs-string\">1s</span>\n    <span class=\"hljs-attr\">limit_mib:</span> <span class=\"hljs-number\">2048</span>\n    <span class=\"hljs-attr\">spike_limit_mib:</span> <span class=\"hljs-number\">512</span>\n\n<span class=\"hljs-attr\">exporters:</span>\n  <span class=\"hljs-comment\"># Traces to Tempo</span>\n  <span class=\"hljs-attr\">otlp/tempo:</span>\n    <span class=\"hljs-attr\">endpoint:</span> <span class=\"hljs-string\">tempo:4317</span>\n    <span class=\"hljs-attr\">tls:</span>\n      <span class=\"hljs-attr\">insecure:</span> <span class=\"hljs-literal\">true</span>\n\n  <span class=\"hljs-comment\"># Metrics to Mimir (Prometheus-compatible)</span>\n  <span class=\"hljs-attr\">prometheusremotewrite:</span>\n    <span class=\"hljs-attr\">endpoint:</span> <span class=\"hljs-string\">http://mimir:9009/api/v1/push</span>\n    <span class=\"hljs-attr\">resource_to_telemetry_conversion:</span>\n      <span class=\"hljs-attr\">enabled:</span> <span class=\"hljs-literal\">true</span>\n\n  <span class=\"hljs-comment\"># Logs to Loki</span>\n  <span class=\"hljs-attr\">loki:</span>\n    <span class=\"hljs-attr\">endpoint:</span> <span class=\"hljs-string\">http://loki:3100/loki/api/v1/push</span>\n    <span class=\"hljs-attr\">labels:</span>\n      <span class=\"hljs-attr\">resource:</span>\n        <span class=\"hljs-attr\">service.name:</span> <span class=\"hljs-string\">\"service\"</span>\n        <span class=\"hljs-attr\">k8s.namespace.name:</span> <span class=\"hljs-string\">\"namespace\"</span>\n\n<span class=\"hljs-attr\">service:</span>\n  <span class=\"hljs-attr\">pipelines:</span>\n    <span class=\"hljs-attr\">traces:</span>\n      <span class=\"hljs-attr\">receivers:</span> [<span class=\"hljs-string\">otlp</span>]\n      <span class=\"hljs-attr\">processors:</span> [<span class=\"hljs-string\">memory_limiter</span>, <span class=\"hljs-string\">k8sattributes</span>, <span class=\"hljs-string\">tail_sampling</span>, <span class=\"hljs-string\">batch</span>]\n      <span class=\"hljs-attr\">exporters:</span> [<span class=\"hljs-string\">otlp/tempo</span>]\n    <span class=\"hljs-attr\">metrics:</span>\n      <span class=\"hljs-attr\">receivers:</span> [<span class=\"hljs-string\">otlp</span>, <span class=\"hljs-string\">prometheus</span>]\n      <span class=\"hljs-attr\">processors:</span> [<span class=\"hljs-string\">memory_limiter</span>, <span class=\"hljs-string\">k8sattributes</span>, <span class=\"hljs-string\">batch</span>]\n      <span class=\"hljs-attr\">exporters:</span> [<span class=\"hljs-string\">prometheusremotewrite</span>]\n    <span class=\"hljs-attr\">logs:</span>\n      <span class=\"hljs-attr\">receivers:</span> [<span class=\"hljs-string\">otlp</span>]\n      <span class=\"hljs-attr\">processors:</span> [<span class=\"hljs-string\">memory_limiter</span>, <span class=\"hljs-string\">k8sattributes</span>, <span class=\"hljs-string\">batch</span>]\n      <span class=\"hljs-attr\">exporters:</span> [<span class=\"hljs-string\">loki</span>]\n</code></pre>\n<h2>SLO-Based Alerting: The End of Alert Fatigue</h2>\n<p>The biggest transformation wasn't technical — it was philosophical. We moved from <strong>threshold-based alerting</strong> (CPU &gt; 80%? Alert!) to <strong>SLO-based alerting</strong> (error budget burning too fast? Alert!).</p>\n<h3>Defining SLOs</h3>\n<pre role=\"region\" aria-label=\"yaml code block\"><code class=\"hljs language-yaml\" tabindex=\"0\"><span class=\"hljs-comment\"># slo-definitions/payment-service.yaml</span>\n<span class=\"hljs-attr\">apiVersion:</span> <span class=\"hljs-string\">sloth.slok.dev/v1</span>\n<span class=\"hljs-attr\">kind:</span> <span class=\"hljs-string\">PrometheusServiceLevel</span>\n<span class=\"hljs-attr\">metadata:</span>\n  <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">payment-service-availability</span>\n<span class=\"hljs-attr\">spec:</span>\n  <span class=\"hljs-attr\">service:</span> <span class=\"hljs-string\">payment-service</span>\n  <span class=\"hljs-attr\">labels:</span>\n    <span class=\"hljs-attr\">team:</span> <span class=\"hljs-string\">payments</span>\n    <span class=\"hljs-attr\">tier:</span> <span class=\"hljs-string\">critical</span>\n\n  <span class=\"hljs-attr\">slos:</span>\n    <span class=\"hljs-bullet\">-</span> <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">availability</span>\n      <span class=\"hljs-attr\">objective:</span> <span class=\"hljs-number\">99.95</span> <span class=\"hljs-comment\"># 99.95% availability target</span>\n      <span class=\"hljs-attr\">description:</span> <span class=\"hljs-string\">\"Payment transactions complete successfully\"</span>\n      <span class=\"hljs-attr\">sli:</span>\n        <span class=\"hljs-attr\">events:</span>\n          <span class=\"hljs-attr\">error_query:</span> <span class=\"hljs-string\">|\n            sum(rate(http_requests_total{\n              service=\"payment-service\",\n              status_code=~\"5..\"\n            }[{{.window}}]))\n</span>          <span class=\"hljs-attr\">total_query:</span> <span class=\"hljs-string\">|\n            sum(rate(http_requests_total{\n              service=\"payment-service\"\n            }[{{.window}}]))\n</span>\n      <span class=\"hljs-attr\">alerting:</span>\n        <span class=\"hljs-attr\">name:</span> <span class=\"hljs-string\">PaymentAvailabilityBurnRate</span>\n        <span class=\"hljs-attr\">labels:</span>\n          <span class=\"hljs-attr\">category:</span> <span class=\"hljs-string\">availability</span>\n        <span class=\"hljs-attr\">annotations:</span>\n          <span class=\"hljs-attr\">summary:</span> <span class=\"hljs-string\">\"Payment service error budget burning too fast\"</span>\n        <span class=\"hljs-attr\">page_alert:</span>\n          <span class=\"hljs-attr\">labels:</span>\n            <span class=\"hljs-attr\">severity:</span> <span class=\"hljs-string\">critical</span>\n        <span class=\"hljs-attr\">ticket_alert:</span>\n          <span class=\"hljs-attr\">labels:</span>\n            <span class=\"hljs-attr\">severity:</span> <span class=\"hljs-string\">warning</span>\n</code></pre>\n<h3>Burn Rate Alerts in Practice</h3>\n<p>Instead of alerting on every 500 error, we alert on <strong>burn rate</strong> — how fast the error budget is being consumed:</p>\n<pre role=\"region\" aria-label=\"promql code block\"><code class=\"hljs language-promql\" tabindex=\"0\"># Multi-window burn rate alert (Google SRE book approach)\n# Page if burning 14.4x budget over 1h AND 6x over 5m\n(\n  error_budget_burn_rate:1h{service=\"payment-service\"} &gt; 14.4\n  and\n  error_budget_burn_rate:5m{service=\"payment-service\"} &gt; 14.4\n)\nor\n# Ticket if burning 3x budget over 6h AND 1x over 30m\n(\n  error_budget_burn_rate:6h{service=\"payment-service\"} &gt; 3\n  and\n  error_budget_burn_rate:30m{service=\"payment-service\"} &gt; 3\n)\n</code></pre>\n<p>This approach reduced our daily alert volume from 500+ to under 40 — a 92% reduction — while catching every real incident faster.</p>\n<h2>Distributed Tracing: Following Requests Across 120 Services</h2>\n<p>With OpenTelemetry's context propagation, a single trace follows a request across all services:</p>\n<pre role=\"region\" aria-label=\"typescript code block\"><code class=\"hljs language-typescript\" tabindex=\"0\"><span class=\"hljs-comment\">// Node.js - Adding custom spans for business logic</span>\n<span class=\"hljs-keyword\">import</span> { trace, <span class=\"hljs-title class_\">SpanStatusCode</span> } <span class=\"hljs-keyword\">from</span> <span class=\"hljs-string\">\"@opentelemetry/api\"</span>;\n\n<span class=\"hljs-keyword\">const</span> tracer = trace.<span class=\"hljs-title function_\">getTracer</span>(<span class=\"hljs-string\">\"checkout-service\"</span>);\n\n<span class=\"hljs-keyword\">async</span> <span class=\"hljs-keyword\">function</span> <span class=\"hljs-title function_\">processCheckout</span>(<span class=\"hljs-params\"><span class=\"hljs-attr\">order</span>: <span class=\"hljs-title class_\">Order</span></span>) {\n  <span class=\"hljs-keyword\">return</span> tracer.<span class=\"hljs-title function_\">startActiveSpan</span>(<span class=\"hljs-string\">\"process-checkout\"</span>, <span class=\"hljs-title function_\">async</span> (span) =&gt; {\n    span.<span class=\"hljs-title function_\">setAttribute</span>(<span class=\"hljs-string\">\"order.id\"</span>, order.<span class=\"hljs-property\">id</span>);\n    span.<span class=\"hljs-title function_\">setAttribute</span>(<span class=\"hljs-string\">\"order.total\"</span>, order.<span class=\"hljs-property\">total</span>);\n    span.<span class=\"hljs-title function_\">setAttribute</span>(<span class=\"hljs-string\">\"order.items_count\"</span>, order.<span class=\"hljs-property\">items</span>.<span class=\"hljs-property\">length</span>);\n\n    <span class=\"hljs-keyword\">try</span> {\n      <span class=\"hljs-comment\">// Each of these creates child spans automatically</span>\n      <span class=\"hljs-keyword\">const</span> inventory = <span class=\"hljs-keyword\">await</span> <span class=\"hljs-title function_\">checkInventory</span>(order.<span class=\"hljs-property\">items</span>);\n      <span class=\"hljs-keyword\">const</span> payment = <span class=\"hljs-keyword\">await</span> <span class=\"hljs-title function_\">chargePayment</span>(order.<span class=\"hljs-property\">total</span>);\n      <span class=\"hljs-keyword\">const</span> shipment = <span class=\"hljs-keyword\">await</span> <span class=\"hljs-title function_\">createShipment</span>(order);\n\n      span.<span class=\"hljs-title function_\">setStatus</span>({ <span class=\"hljs-attr\">code</span>: <span class=\"hljs-title class_\">SpanStatusCode</span>.<span class=\"hljs-property\">OK</span> });\n      <span class=\"hljs-keyword\">return</span> { inventory, payment, shipment };\n    } <span class=\"hljs-keyword\">catch</span> (error) {\n      span.<span class=\"hljs-title function_\">setStatus</span>({\n        <span class=\"hljs-attr\">code</span>: <span class=\"hljs-title class_\">SpanStatusCode</span>.<span class=\"hljs-property\">ERROR</span>,\n        <span class=\"hljs-attr\">message</span>: error.<span class=\"hljs-property\">message</span>,\n      });\n      span.<span class=\"hljs-title function_\">recordException</span>(error);\n      <span class=\"hljs-keyword\">throw</span> error;\n    } <span class=\"hljs-keyword\">finally</span> {\n      span.<span class=\"hljs-title function_\">end</span>();\n    }\n  });\n}\n</code></pre>\n<h3>Grafana: The Unified Dashboard</h3>\n<p>In Grafana, we correlate all three signals:</p>\n<ol>\n<li><strong>Dashboard</strong> shows SLO burn rate trending upward</li>\n<li><strong>Click through</strong> to see error rate spike in metrics</li>\n<li><strong>Exemplar links</strong> jump directly to the failing trace</li>\n<li><strong>Trace view</strong> shows the exact service and span that failed</li>\n<li><strong>Log link</strong> shows the structured error with full context</li>\n</ol>\n<p>All in under 30 seconds from detection to root cause.</p>\n<h2>Results: The Numbers Speak</h2>\n<table>\n<thead>\n<tr>\n<th>Metric</th>\n<th>Before</th>\n<th>After</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Mean Time to Detection</td>\n<td>15+ minutes</td>\n<td>&lt; 30 seconds</td>\n</tr>\n<tr>\n<td>Daily alert volume</td>\n<td>500+</td>\n<td>&lt; 40</td>\n</tr>\n<tr>\n<td>Hours/week on triage</td>\n<td>40</td>\n<td>4</td>\n</tr>\n<tr>\n<td>Trace coverage</td>\n<td>20% of services</td>\n<td>100%</td>\n</tr>\n<tr>\n<td>Telemetry data/day</td>\n<td>5TB (fragmented)</td>\n<td>50TB (unified)</td>\n</tr>\n<tr>\n<td>Cost per GB telemetry</td>\n<td>$2.80</td>\n<td>$0.40</td>\n</tr>\n</tbody>\n</table>\n<h2>Key Architecture Decisions</h2>\n<ol>\n<li>\n<p><strong>Collector-per-node, not per-pod</strong>: DaemonSet deployment reduces resource overhead and provides node-level visibility.</p>\n</li>\n<li>\n<p><strong>Tail-based sampling</strong>: Keep all error traces, sample normal ones. This gives 100% visibility into failures while controlling storage costs.</p>\n</li>\n<li>\n<p><strong>Grafana Mimir over vanilla Prometheus</strong>: Mimir provides long-term storage, multi-tenancy, and horizontal scaling that standalone Prometheus can't match at 50TB/day.</p>\n</li>\n<li>\n<p><strong>Structured logs, not free text</strong>: Every log line is JSON with correlation IDs. Loki queries against labels are 100x faster than full-text search.</p>\n</li>\n<li>\n<p><strong>SLOs before SLIs</strong>: Define what \"reliable\" means to users first, then instrument to measure it. Not the other way around.</p>\n</li>\n</ol>\n<h2>Getting Started</h2>\n<p>The migration path that worked for us:</p>\n<ol>\n<li><strong>Week 1-2</strong>: Deploy OTel Collector and basic Grafana stack</li>\n<li><strong>Week 3-4</strong>: Instrument top 10 critical services</li>\n<li><strong>Month 2</strong>: Define SLOs for critical services, set up burn rate alerts</li>\n<li><strong>Month 3</strong>: Roll out to all services, disable old monitoring</li>\n<li><strong>Ongoing</strong>: Refine SLOs based on user impact data</li>\n</ol>\n<p>The hardest part isn't the technology — it's convincing teams to trust SLO-based alerting over their beloved threshold alerts. Let the numbers prove it.</p>"}