<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ #prometheus - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ #prometheus - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 07 Sep 2026 23:54:05 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/prometheus/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Claude Code Observability with OpenTelemetry ]]>
                </title>
                <description>
                    <![CDATA[ Agentic coding tools like Claude Code, OpenAI Codex, Google Antigravity, and Cursor have become ubiquitous for everyday software development. As agentic systems mature, much of the work developers hav ]]>
                </description>
                <link>https://www.freecodecamp.org/news/claude-code-observability-with-opentelemetry/</link>
                <guid isPermaLink="false">6a9a0db7c7c0575bd6526dd2</guid>
                
                    <category>
                        <![CDATA[ OpenTelemetry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ claude ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #prometheus ]]>
                    </category>
                
                    <category>
                        <![CDATA[ distributed tracing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ generative ai ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Puneet Singh ]]>
                </dc:creator>
                <pubDate>Fri, 04 Sep 2026 00:15:51 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/2a729ee5-e1b9-4198-91cd-251b9c12867f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Agentic coding tools like <a href="https://claude.com/claude-code">Claude Code</a>, <a href="https://openai.com/codex">OpenAI Codex</a>, <a href="https://antigravity.google">Google Antigravity</a>, and <a href="https://cursor.com">Cursor</a> have become ubiquitous for everyday software development.</p>
<p>As agentic systems mature, much of the work developers have them do is delegated, one subagent at a time. Many teams are also exploring and using a shared, multi-tenant Agentic Infrastructure, where cost isn't tied to a single owner. That's where Observability becomes key to monitoring infrastructure costs.</p>
<p>In this guide, you'll learn how observability works, then enable Claude Code's built-in telemetry, run a backend to collect it, and read the metrics, logs, and traces it emits. This will help you start tracking your team's costs more effectively, and it'll only improve as emitted telemetry matures and correlates more cleanly with your sessions.</p>
<p><strong>Note</strong>: In its current state, the emitted telemetry from Claude Code provides no attributes that allow a reliable map to named sessions. Usage can be tracked using session_id, but it's still clumsy in a longer session mixing multiple prompts/skills.</p>
<p>This guide is scoped to <code>Claude Code</code>'s telemetry for metrics, logs, and tracing. Note that it applies to Linux and macOS only.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-observability-with-opentelemetry">Observability with OpenTelemetry</a></p>
<ul>
<li><a href="#heading-telemetry-data">Telemetry Data</a></li>
</ul>
</li>
<li><p><a href="#heading-instrumenting-claude-code">Instrumenting Claude Code</a></p>
<ul>
<li><p><a href="#heading-pull-vs-push-how-telemetry-leaves-an-app">Pull vs Push: How Telemetry Leaves an App</a></p>
</li>
<li><p><a href="#heading-when-to-run-a-collector">When to run a Collector</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-setup">Setup</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-exploring-telemetry">Exploring Telemetry</a></p>
<ul>
<li><p><a href="#heading-metrics">Metrics</a></p>
</li>
<li><p><a href="#heading-logs">Logs</a></p>
</li>
<li><p><a href="#heading-tracing">Tracing</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a href="#heading-references">References</a></p>
</li>
</ul>
<h2 id="heading-observability-with-opentelemetry">Observability with OpenTelemetry</h2>
<p>Observability is the ability to answer questions about a system's runtime behavior from the data it emits. You do this without looking into its internals, attaching a debugger, reading source code, or manually trying to reproduce the behavior.</p>
<p>Here, a system's runtime behavior means what's externally visible. You can ask questions like:</p>
<ul>
<li><p>How much time 95% of all requests take.</p>
</li>
<li><p>What the failure rate is across all requests received.</p>
</li>
<li><p>What the cache hit ratio is for the in-memory cache the service uses.</p>
</li>
<li><p>The difference between the configured and deployed replica counts for a service.</p>
</li>
</ul>
<p>For Claude Code, the inaccessible inner workings are: how it manages context, how work is divided across multiple LLM calls, and how subagents are orchestrated. But you can read the emitted telemetry from Claude code to answer questions like:</p>
<ul>
<li><p>How much a dev or a team spent over a day, week, or month.</p>
</li>
<li><p>How that usage is distributed across the supported models and effort levels.</p>
</li>
<li><p>How many tokens are spent per dollar, and how much that varies by type (input, output, cacheRead, cacheCreation).</p>
</li>
<li><p>When a compaction event kicked in, and by how much it reduced the context's token usage.</p>
</li>
</ul>
<p>Only an instrumented system can answer these questions. Instrumentation is a piece of code added by the developer or built into the tool that records a program's runtime behavior and emits it as telemetry. For example, a measurement like <code>this request spent 100 tokens</code>.</p>
<p>The telemetry data helps avoid silent failures by providing a well-structured data trail of the system's behavior over time. For example, here's a chart from <a href="https://github.blog/news-insights/company-news/the-august-17-outage-and-the-work-ahead/">GitHub's August 17, 2026 outage</a> postmortem explaining a rise in GitHub Actions runs over time from ~30M to ~110M:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/fe9ec420-2137-4a21-93aa-00255638bc32.png" alt="Github Actions Growth" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-telemetry-data">Telemetry Data</h3>
<p>The emitted telemetry consists of three categories of data:</p>
<ul>
<li><p><a href="https://opentelemetry.io/docs/concepts/signals/metrics/"><strong>Metrics</strong></a>: Numeric measurements aggregated over a time window, like queries per second (QPS).</p>
</li>
<li><p><a href="https://opentelemetry.io/docs/concepts/signals/logs/"><strong>Logs</strong></a>: A detailed record of an individual event, with a timestamp. For example, a compaction event in Claude Code.</p>
</li>
<li><p><a href="https://opentelemetry.io/docs/concepts/signals/traces/"><strong>Traces</strong></a>: The path of one request through the system, split by time into nested requests. For example, an order-placement request on an ecommerce website, showing which internal services it calls to complete the request.</p>
</li>
</ul>
<p>OpenTelemetry (<a href="https://opentelemetry.io/">https://opentelemetry.io/</a>) is an observability framework that helps you generate, collect, and export these signals to a backend, which handles storage, querying, and visualization. Keeping that split makes it tool/vendor agnostic where the backend can be open-source or proprietary. It provides instrumentation SDKs for multiple <a href="https://opentelemetry.io/docs/languages/">programming languages</a>.</p>
<h2 id="heading-instrumenting-claude-code">Instrumenting Claude Code</h2>
<p>Instrumentation code usually runs alongside the application, at the points best for measuring: a middleware with a request arriving or a response going out, or a token being counted.</p>
<p>It is plugged into the application in two ways:</p>
<ol>
<li><p>A Shared Instrumentation Library: Applications using an open source framework can add an instrumentation library as a dependency and link it with the application's lifecycle methods. OpenTelemetry publishes instrumentation libraries for many frameworks (Example: <a href="https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/out-of-the-box-instrumentation/">Spring Framework</a>).</p>
</li>
<li><p>Customized Implementation by Application Developers: Telemetry data emitters are added directly in the codebase using the OpenTelemetry SDK. For a closed-source product, the code is private, but it can still emit telemetry data compatible with OpenTelemetry standards.</p>
</li>
</ol>
<h3 id="heading-example-http-instrumentation">Example: HTTP Instrumentation</h3>
<p>Middleware in HTTP handling is a common codepath for all requests, which is why it's chosen for application-wide settings like authentication. The same reason makes it a good place for instrumentation code: wrap the handler once, measure every request.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/e0699b5b-5459-4a28-befb-87dce792f4e9.png" alt="HTTP Instrumentation example" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The diagram shows where instrumentation sits relative to the request path.</p>
<ul>
<li><p>A client request passes through an HTTP middleware before reaching the application's request handler and downstream calls. The middleware uses SDK constructs to time the handler and record duration, status, and a count.</p>
</li>
<li><p>The SDK then buffers those measurements and pushes OTLP to the Collector on a background thread, off the request path.</p>
</li>
</ul>
<p>Claude Code is the second case, where both the core app and its instrumentation module are provided by <a href="https://code.claude.com/docs/en/monitoring-usage">Anthropic</a>. The code that measures token usage, cost, and tool calls is built in and emits OpenTelemetry over <a href="https://opentelemetry.io/docs/specs/otlp/">OTLP</a>. As a Claude Code user, you only need to enable the telemetry and prepare a backend to receive and analyze it.</p>
<p><strong>Note</strong>: <a href="https://opentelemetry.io/docs/specs/otlp/">OTLP</a> is a telemetry data delivery protocol designed in the scope of the OpenTelemetry project. This guide assumes end-to-end compatibility with OTLP. Wiring up incompatible telemetry or using incompatible backend components can give unpredictable results and is out of scope.</p>
<p>To collect, store, and read the telemetry, you'll need the following components:</p>
<ul>
<li><p><a href="https://opentelemetry.io/docs/collector/">OpenTelemetry Collector</a>: A vendor-agnostic implementation of how to receive, process, and export telemetry data. This is optional but great to have for personal setup. Must have for a production use case.</p>
</li>
<li><p><a href="https://www.jaegertracing.io/">Jaeger</a>: Distributed tracing backend, released as an open source tool by Uber.</p>
</li>
<li><p><a href="https://prometheus.io/">Prometheus</a>: Collects and stores metrics as Timeseries Data.</p>
</li>
<li><p><a href="https://grafana.com/oss/loki/">Loki</a>: Scalable Log Aggregation system by Grafana.</p>
</li>
<li><p><a href="https://grafana.com/oss/grafana/">Grafana</a>: for UI visualization of logs, metrics, and traces.</p>
</li>
</ul>
<h3 id="heading-pull-vs-push-how-telemetry-leaves-an-app">Pull vs Push: How Telemetry Leaves an App</h3>
<p>Telemetry leaves an application in one of two ways:</p>
<p><strong>Pull (Scrape)</strong>: The app exposes its current metrics on an HTTP endpoint, and a scraper (Prometheus) reads that endpoint periodically. Every running instance needs its own port, and the scraper must know all of those addresses ahead of time. The app is passive: the scraper drives the data movement. This suits long-lived processes with stable addresses.</p>
<p>In OpenTelemetry, pull-based scraping is configured with <code>OTEL_METRICS_EXPORTER=prometheus</code> (see the <a href="https://opentelemetry.io/docs/languages/sdk-configuration/general/">SDK environment variables</a> for the accepted exporter values).</p>
<p>By convention, the app makes its metrics available at <code>http://localhost:9464/metrics</code>. This exporter handles metrics only.</p>
<p><strong>Push (OTLP)</strong>: The app sends its telemetry to a receiving endpoint on a regular interval. This is more flexible: any number of processes can push to the same endpoint with no registration ahead of time, so apps can start and stop freely even as their addresses change.</p>
<p>In OpenTelemetry, push is configured with <code>OTEL_METRICS_EXPORTER=otlp</code>, which ships over the <a href="https://opentelemetry.io/docs/specs/otel/protocol/exporter/">OTLP exporter</a>.</p>
<p>The push model can carry metrics, logs, and traces.</p>
<h3 id="heading-when-to-run-a-collector">When to Run a Collector</h3>
<p>A Collector is deployed when the existing stack isn't enough to handle the system's growing scale and complexity. It helps in the following ways:</p>
<ul>
<li><p><strong>One export config</strong>: every producer points at the Collector instead of each carrying its own per-backend exporter setup.</p>
</li>
<li><p><strong>Outbound-only connections</strong>: enterprise networks often block the inbound connections a pull-based scraper needs. With a Collector, the app pushes out to it and it pushes onward, so nothing has to accept inbound traffic.</p>
</li>
<li><p><strong>Fan-out and translation</strong>: the Collector can convert telemetry into a vendor's storage format and send the same signal to more than one backend.</p>
</li>
<li><p><strong>Buffering</strong>: if a backend goes down, the Collector holds the data and retries, absorbing transient failures.</p>
</li>
<li><p><strong>Processing</strong>: it can apply <a href="https://opentelemetry.io/docs/collector/configuration/#processors">processors</a> before data leaves for storage, such as redacting attributes.</p>
</li>
</ul>
<p><strong>Note</strong>: This guide runs a push-based configuration with a Collector even though the setup is single-user. The stack has three backends that store and query data differently, and letting the Collector receive Claude Code's OTLP once and route each signal to the right place is simpler than wiring the app to all three. That's why the tool list calls it optional for personal use but a must-have in production: its value grows with the number of producers and backends.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Each section in this guide links relevant docs, but you'll make faster progress if the tools and query languages specified below are already familiar:</p>
<p><strong>You'll need</strong></p>
<ul>
<li><p>Latest version of <a href="https://code.claude.com/docs/en/setup">Claude Code</a></p>
</li>
<li><p><a href="https://docs.docker.com/engine/install/">Docker Engine</a> (29.4.1+) with <a href="https://docs.docker.com/compose/">Docker Compose</a>.</p>
<ul>
<li>Capacity to run 5 Containers (4-core CPU, 8GB RAM, 15GB Disk)</li>
</ul>
</li>
<li><p>A Claude Plan that includes <a href="https://code.claude.com/docs/en/monitoring-usage#traces-beta">Enhanced Telemetry beta</a> (Pro+ / Max)</p>
</li>
<li><p><a href="https://git-scm.com/">Git</a></p>
</li>
<li><p>Ensure these ports are free at localhost:</p>
<ul>
<li><p><code>3000</code> (Grafana)</p>
</li>
<li><p><code>3100</code> (Loki)</p>
</li>
<li><p><code>4317</code>/<code>4318</code> (OTel Collector OTLP gRPC/HTTP)</p>
</li>
<li><p><code>9090</code> (Prometheus)</p>
</li>
<li><p><code>16686</code> (Jaeger UI)</p>
</li>
</ul>
</li>
</ul>
<p><strong>Knowledge that would help</strong></p>
<ul>
<li><p><a href="https://docs.docker.com/reference/cli/docker/compose/">Docker Compose</a>: bringing up containers defined in compose file, reading container status and logs by <code>docker compose ...</code> commands.</p>
</li>
<li><p><a href="https://www.gnu.org/software/bash/manual/">Bash</a> and Config files: Setting environment variables, editing <a href="https://www.json.org/json-en.html">JSON</a> files.</p>
</li>
<li><p><a href="https://prometheus.io/docs/prometheus/latest/querying/basics/">PromQL</a> (Prometheus): How to use counters/gauges, range selectors, and <code>sum</code> / <code>increase</code> / <code>rate</code> / <code>by</code> (label) grouping.</p>
</li>
<li><p>Grafana: <a href="https://grafana.com/docs/grafana/latest/explore/">Explore</a> a datasource, build <a href="https://grafana.com/docs/grafana/latest/panels-visualizations/">dashboards</a> with stat and timeseries panels. Panel <a href="https://grafana.com/docs/grafana/latest/panels-visualizations/query-transform-data/transform-data/">transformations</a> and <a href="https://grafana.com/docs/grafana/latest/visualizations/dashboards/variables/global-variables/">Global variables</a>.</p>
</li>
<li><p><a href="https://grafana.com/docs/loki/latest/query/">LogQL</a> (Loki): stream selectors, logfmt, and label_format.</p>
</li>
<li><p>Jaeger and tracing: the <a href="https://opentelemetry.io/docs/concepts/signals/traces/">trace/span model</a> (parent-child spans, span count, duration) and the <a href="https://www.jaegertracing.io/docs/latest/frontend-ui/">Jaeger UI's tag search</a>.</p>
</li>
<li><p>Claude Code's execution model: sessions, <a href="https://code.claude.com/docs/en/sub-agents">subagents</a>, skills, tools, and context compaction.</p>
</li>
<li><p><a href="https://code.claude.com/docs/en/costs">Claude billing basics</a>: tokens and the <a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching">prompt-caching tiers</a>.</p>
</li>
</ul>
<h3 id="heading-setup">Setup</h3>
<p>The test observability stack is deployed using Docker Compose. For telemetry export to work, Claude Code must be able to reach Collector's OTLP endpoint which is localhost:4317 (gRPC) or localhost:4318(HTTP) when running on same machine. All backend services run in a container with their own Docker volume for persistence.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/d486a15e-1b71-490a-8f5f-9489ffe0edab.png" alt="Instrumentation Setup" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The diagram shows how the telemetry components we're using are linked.</p>
<ul>
<li><p>Apart from Claude Code, every component runs in a container managed by Docker Compose.</p>
</li>
<li><p>Multiple instances of Claude Code running on any machine (host or cloud VM) should be able to export telemetry to the Collector as long as those machines have connectivity to it (ports 4317/4318 of the host running the Collector container are reachable).</p>
</li>
</ul>
<p>Moving from top to bottom:</p>
<ul>
<li><p>Claude Code exports all three signals over OTLP to the Collector.</p>
</li>
<li><p>The Collector then splits them by type, pushing traces to Jaeger and logs to Loki, while exposing metrics on port 8889 for Prometheus to scrape.</p>
</li>
<li><p>Jaeger, Prometheus, and Loki each persist to their own Docker volume.</p>
</li>
<li><p>Grafana queries all three as the single dashboard layer.</p>
</li>
</ul>
<p>The goal here is a live stream of telemetry from Claude Code that gets stored in a backend and can be queried on demand, during a session or long after. Two things must be in place:</p>
<ul>
<li><p>Enable telemetry in Claude Code. The instrumentation is built in but emits nothing until telemetry is enabled and its OTLP exporter points at the Collector.</p>
</li>
<li><p>Run the observability backend. The Collector processes each signal, forwarding it to Prometheus, Loki, and Jaeger for storing and serving queries.</p>
</li>
</ul>
<p>You'll start the backend first, so the telemetry has somewhere to go.</p>
<h4 id="heading-start-the-observability-backend">Start the Observability Backend</h4>
<p>Before enabling telemetry in Claude Code, ensure the stack is up to collect, process, and read the data. The code for the test observability backend lives in this <a href="https://github.com/ps-mir/otel-dev-stack">Github repo</a>.</p>
<p>The repo has the following structure:</p>
<pre><code class="language-bash">.
├── README.md
└── compose
    ├── docker-compose.yml # Docker config for 5 containers in the observability stack. Applies pinned image versions, port mappings and named volumes for each service.
    ├── grafana
    │&nbsp;&nbsp; └── provisioning
    │&nbsp;&nbsp;     ├── alerting
    │&nbsp;&nbsp;     ├── dashboards
    │&nbsp;&nbsp;     ├── datasources # datasources(Prometheus, Loki, Jaeger) and dashboards. Empty initially.
    │&nbsp;&nbsp;     └── plugins
    ├── jaeger-config.yaml # Jaeger v2, badger (local-file) storage for traces. Ties to the user: root TIP below.
    ├── loki-config.yaml # single-binary Loki, filesystem storage. Near default settings.
    ├── otel-collector-config.yaml # receive/process/export pipeline: OTLP in on 4317/4318, traces out to Jaeger, logs to Loki, metrics exposed on :8889 for Prometheus.
    └── prometheus.yml # a single scrape job against the Collector's :8889, 30s interval.
</code></pre>
<p>You'll only need <code>docker compose</code> command to start the containers. It reads <a href="https://github.com/ps-mir/otel-dev-stack/blob/690d485fb89491fcd940b550377d9a2cc2dcc084/compose/docker-compose.yml">docker-compose.yml</a> and starts the containers, linking them to their respective config files.</p>
<h4 id="heading-connectivity-between-containers">Connectivity between containers:</h4>
<p>All containers start within the same Docker network, which allows them to communicate using container names directly. For example, <a href="https://github.com/ps-mir/otel-dev-stack/blob/690d485fb89491fcd940b550377d9a2cc2dcc084/compose/otel-collector-config.yaml">collector's exporter config</a> uses container names:</p>
<pre><code class="language-yaml">exporters:
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
  prometheus:
    endpoint: 0.0.0.0:8889
  otlphttp/loki:
    endpoint: http://loki:3100/otlp
</code></pre>
<p>Note that it doesn't contain Prometheus config, since Prometheus ends up scraping it from the collector as configured in <a href="https://github.com/ps-mir/otel-dev-stack/blob/690d485fb89491fcd940b550377d9a2cc2dcc084/compose/prometheus.yml">prometheus.yml</a>:</p>
<pre><code class="language-yaml">global:
  scrape_interval: 30s

scrape_configs:
  - job_name: otel-collector
    static_configs:
      - targets: ["otel-collector:8889"]
</code></pre>
<p>Start the containers using Docker compose:</p>
<pre><code class="language-bash">git clone https://github.com/ps-mir/otel-dev-stack.git
cd otel-dev-stack/compose
docker compose up -d

# Output
 ✔ Volume compose_loki_data                          Created                                                                                                                                          0.0s
 ✔ Volume compose_grafana_data                       Created                                                                                                                                          0.0s
 ✔ Volume compose_prometheus_data                    Created                                                                                                                                          0.0s
 ✔ Volume compose_jaeger_data                        Created                                                                                                                                          0.0s
 ✔ Network compose_default                           Created                                                                                                                                          0.1s
 ✔ Container compose-prometheus-1                    Started                                                                                                                                          4.1s
 ✔ Container compose-loki-1                          Started                                                                                                                                          4.2s
 ✔ Container compose-jaeger-1                        Started                                                                                                                                          4.3s
 ✔ Container compose-otel-collector-1                Started                                                                                                                                          3.3s
 ✔ Container compose-grafana-1                       Started                                                                                                                                          2.7s
</code></pre>
<p>Check container status:</p>
<pre><code class="language-bash"># all five services should show "Up"
docker compose ps

# Output
NAME                       IMAGE                                              COMMAND                  SERVICE          CREATED         STATUS         PORTS
compose-grafana-1          grafana/grafana:13.2.0                            "/run.sh"                grafana          3 minutes ago   Up 3 minutes   0.0.0.0:3000-&gt;3000/tcp, [::]:3000-&gt;3000/tcp
compose-jaeger-1           cr.jaegertracing.io/jaegertracing/jaeger:2.20.0   "/go/bin/jaeger --co…"   jaeger           3 minutes ago   Up 3 minutes   0.0.0.0:16686-&gt;16686/tcp, [::]:16686-&gt;16686/tcp
compose-loki-1             grafana/loki:3.7.6                                "/usr/bin/loki -conf…"   loki             3 minutes ago   Up 3 minutes   0.0.0.0:3100-&gt;3100/tcp, [::]:3100-&gt;3100/tcp
compose-otel-collector-1   otel/opentelemetry-collector-contrib:0.159.0      "/otelcol-contrib --…"   otel-collector   3 minutes ago   Up 3 minutes   0.0.0.0:4317-4318-&gt;4317-4318/tcp, [::]:4317-4318-&gt;4317-4318/tcp, 55679/tcp
compose-prometheus-1       prom/prometheus:v3.11.2                           "/bin/prometheus --c…"   prometheus       3 minutes ago   Up 3 minutes   0.0.0.0:9090-&gt;9090/tcp, [::]:9090-&gt;9090/tcp
</code></pre>
<p><strong>TIP</strong>: Jaeger runs as <code>user: root</code> (Compose file) to create the badger dir. Not doing so causes a failure: <code>mkdir /badger/key: permission denied</code>. Jaeger itself doesn't need root permission, but Docker volumes are owned by <code>root:root</code> on first mount.</p>
<h4 id="heading-enable-telemetry">Enable Telemetry</h4>
<p>OpenTelemetry instrumentation, once added to an application, stays disabled until specific configuration enables it.</p>
<p>There are two ways to enable telemetry in Claude Code:</p>
<p>1. Environment Variables</p>
<p>Setting specific environment variables enables telemetry generation. Beyond the standard <code>OTEL_*</code> variables, Claude Code defines its own <code>CLAUDE_CODE_*</code> variables.</p>
<p>This guide uses the following settings:</p>
<pre><code class="language-bash"># master switch: when unset or 0, Claude Code produces no telemetry at all
export CLAUDE_CODE_ENABLE_TELEMETRY=1

# opt into the beta enhanced-telemetry attributes and events (extra session and tool detail)
export CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1

# per-signal exporter selection; "otlp" ships the signal over OTLP.
# other accepted values are "console" (print locally), "prometheus" (metrics only), and "none" (drop the signal)
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_TRACES_EXPORTER=otlp

# OTLP transport: "grpc" talks to the collector's 4317 port; "http/protobuf" would use 4318
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc

# one endpoint for all three signals: the collector's OTLP listener on the local machine
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

# how often metrics are flushed, in milliseconds; the default is 60000 (60s),
# shortened here so a manual check sees fresh data without a long wait
export OTEL_METRIC_EXPORT_INTERVAL=5000

# emit cumulative counters instead of delta (see "Aggregation Temporality" below)
export OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative
</code></pre>
<p>Environment variables, however, are process-wide and can affect more than Claude Code. For example,</p>
<ul>
<li><p>Accidentally enabling instrumentation in other applications.</p>
</li>
<li><p>Interfering with OpenTelemetry code/tests if you're developing your own instrumentation or working on any OpenTelemetry SDK.</p>
</li>
</ul>
<p>2. Claude Code <code>settings.json</code></p>
<p>OpenTelemetry defines <a href="https://opentelemetry.io/docs/languages/sdk-configuration/declarative-configuration/">Declarative Config</a>, a YAML based configuration to enable telemetry, but Claude Code doesn't support it. But it lets you set the same env variables in <code>~/.claude/settings.json</code>. That isn't declarative config, but it's better than shell environment variables because it applies only to Claude Code. An example:</p>
<pre><code class="language-json">{
  "effortLevel": "medium",
  "tui": "fullscreen",
  "env": {
     "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
     "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
     "OTEL_METRICS_EXPORTER": "otlp",
     "OTEL_LOGS_EXPORTER": "otlp",
     "OTEL_TRACES_EXPORTER": "otlp",
     "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
     "OTEL_EXPORTER_OTLP_ENDPOINT": "http://localhost:4317",
     "OTEL_METRIC_EXPORT_INTERVAL": "5000",
     "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE": "cumulative"
  }
}
</code></pre>
<p>Only the <code>env</code> block matters for telemetry. <code>effortLevel</code> and <code>tui</code> are unrelated settings you may already have. The variables match the annotated list above.</p>
<h4 id="heading-aggregation-temporality">Aggregation Temporality</h4>
<p>Prometheus <a href="https://prometheus.io/docs/concepts/metric_types/#counter">counter</a> type metrics only increase over time. Their raw value isn't useful, so you read them through per-second growth (<code>rate()</code>) or total growth over a time window (<code>increase()</code>).</p>
<p>Aggregation temporality decides what number a counter reports on each telemetry export: the change since the previous export(Delta), or the running total since the process started(Cumulative).</p>
<p>A short example. Say Claude Code spends tokens over four 5-second export intervals:</p>
<table>
<thead>
<tr>
<th>Export at</th>
<th>Tokens since last export</th>
<th>Delta value sent</th>
<th>Cumulative value sent</th>
</tr>
</thead>
<tbody><tr>
<td>0s (start)</td>
<td>--</td>
<td>--</td>
<td>0</td>
</tr>
<tr>
<td>5s</td>
<td>100</td>
<td>100</td>
<td>100</td>
</tr>
<tr>
<td>10s</td>
<td>0</td>
<td>0</td>
<td>100</td>
</tr>
<tr>
<td>15s</td>
<td>250</td>
<td>250</td>
<td>350</td>
</tr>
<tr>
<td>20s</td>
<td>50</td>
<td>50</td>
<td>400</td>
</tr>
</tbody></table>
<p>By default, Claude Code emits metrics with <code>AggregationTemporality: Delta</code>. This can be inspected and confirmed from the Collector's container logs using the command:</p>
<pre><code class="language-bash"># Command only works from directory containing docker-compose.yml
docker compose logs otel-collector
</code></pre>
<p><strong>Note</strong>: To enable detailed logs in the Collector, you need to add the <code>debug</code> exporter to the <a href="https://github.com/ps-mir/otel-dev-stack/blob/690d485fb89491fcd940b550377d9a2cc2dcc084/compose/otel-collector-config.yaml">Collector config</a>.</p>
<pre><code class="language-yaml">service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/jaeger, debug]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus, debug]
</code></pre>
<p>Then restart the container:</p>
<pre><code class="language-bash"># Command only works from directory containing docker-compose.yml
docker compose up -d --force-recreate otel-collector
</code></pre>
<p>Log output with <code>AggregationTemporality: Delta</code>:</p>
<pre><code class="language-bash">otel-collector-1  | Descriptor:
otel-collector-1  |      -&gt; Name: claude_code.active_time.total
otel-collector-1  |      -&gt; Description: Total active time in seconds
otel-collector-1  |      -&gt; Unit: s
otel-collector-1  |      -&gt; DataType: Sum
otel-collector-1  |      -&gt; IsMonotonic: true
otel-collector-1  |      -&gt; AggregationTemporality: Delta &lt;---
otel-collector-1  | NumberDataPoints #0
</code></pre>
<p>Delta doesn't work well with Prometheus functions like <code>rate()</code>/<code>increase()</code>, since they expect cumulative values.</p>
<p>Setting <code>OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative</code> switches the exported metrics from delta to cumulative temporality. It has been added to both the env and JSON config used in this guide.</p>
<p>As before, you need to restart the Collector container after any config change for it to take effect.</p>
<h2 id="heading-exploring-telemetry">Exploring Telemetry</h2>
<p>With telemetry flowing, you can start querying it. Metrics, logs, and traces each answer a different kind of question about Claude Code usage, so the three sections below are largely independent.</p>
<p>The data behind them comes from two places.</p>
<ul>
<li><p>The Metrics and Logs sections query whatever Claude Code usage has accumulated in the backend, so your panels will show your own sessions and the numbers won't match the screenshots. Give it a few real sessions before expecting much to show.</p>
</li>
<li><p>The Tracing section instead walks a single deliberate run, a custom skill summarizing a batch of meetings, described in enough detail to follow along. You don't need to reproduce it.</p>
</li>
</ul>
<h3 id="heading-metrics">Metrics</h3>
<p>Metrics are the aggregate, time-windowed view of Claude Code usage. Example: total cost, token volume, and how each trends and breaks down by attributes like <code>model</code>, <code>effort</code>, and token <code>type</code>. Use them to watch spend and spot shifts in consumption.</p>
<p>Each metric is a numeric measurement recorded over time, a <a href="https://prometheus.io/docs/concepts/data_model/">time series</a> of timestamped values you can plot or aggregate. Claude Code's metrics are running totals (counters), so a query reports the change over a chosen window rather than the raw value. See <a href="#heading-aggregation-temporality">Aggregation Temporality</a> above for how that works.</p>
<p><a href="https://prometheus.io/docs/introduction/overview/">Prometheus</a> is the metrics backend we're using here. It scrapes the Collector, stores the series, and answers queries written in <a href="https://prometheus.io/docs/prometheus/latest/querying/basics/">PromQL</a>. Grafana reads the same data for dashboards at <code>localhost:3000</code>. The full list of metrics and their attributes is in the <a href="https://code.claude.com/docs/en/monitoring-usage">Claude Code monitoring docs</a>.</p>
<p>Open Prometheus in your browser (localhost:9090), type <code>claude</code> in the query field, and you should see the supported metrics:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/8a4703dc-8b10-47b5-95ca-2cc383ab052a.png" alt="Available Claude Code metrics of counter type in Prometheus." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>For each metric below, you'll explore it first using PromQL, and then use the same query to add it to the Grafana dashboard as a panel.</p>
<h4 id="heading-total-usd-spent">Total USD Spent</h4>
<p><code>claude_code_cost_usage_USD_total</code> represents cumulative usage cost, in USD, tracked per session. It's useful for controlling budgets and spotting sudden spikes in usage.</p>
<p>This is a client-side estimate based on token counts priced at Anthropic's per-model, per-type rates and accumulated. It's completely normal for it to exceed your Claude Code plan's subscription cost.</p>
<p><strong>Note</strong>: This metric is more critical if you're paying per raw API call. A subscription gives you a usage allowance with increased but bounded rate limits.</p>
<p>Test the following query in Prometheus first (<code>localhost:9090/query</code>):</p>
<pre><code class="language-promql">sum(increase(claude_code_cost_usage_USD_total[10m]))
</code></pre>
<p><code>increase(...[10m])</code> gives the counter's growth over the last 10 minutes. <code>sum(...)</code> with no <code>by</code> clause collapses the per-attribute series (<code>model</code>, <code>effort</code>, and others) into one number.</p>
<p>For a Grafana panel, swap the fixed <code>[10m]</code> window for the <a href="https://grafana.com/docs/grafana/latest/visualizations/dashboards/variables/global-variables/"><code>$__range</code></a> built-in variable so the value follows the dashboard's time picker:</p>
<pre><code class="language-promql">sum(increase(claude_code_cost_usage_USD_total[$__range]))
</code></pre>
<p>To add it as a panel, open Explore, select Prometheus as the data source, and run the query. The result depends on how much you've used Claude Code in the window.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/99c0e78a-323b-412b-8952-685359644ad6.png" alt="Grafana USD Total Stat - Metrics explorer view of the query." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>On adding to the dashboard you should get more Panel Options. Select the <code>Stat</code> panel:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/d0e45c4a-f37e-4eb5-aed0-5929efe8a8fb.png" alt="Total USD Stat Panel" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>This panel needs to be added to the Grafana dashboard.</p>
<h4 id="heading-total-token-usage">Total Token Usage</h4>
<p><code>claude_code_token_usage_tokens_total</code> is the cumulative token count, with the same counter shape as the USD cost metric. Read a raw series in Prometheus first to see which labels you can aggregate by. A single series looks like:</p>
<pre><code class="language-text">claude_code_token_usage_tokens_total{effort="high", exported_job="claude-code", instance="otel-collector:8889", job="otel-collector", model="claude-sonnet-5", otel_scope_name="com.anthropic.claude_code", otel_scope_version="2.1.252", query_source="auxiliary", session_id="e0b9795b-4da3-4171-8fa6-a2866bf44d86", terminal_type="ssh-session", type="cacheCreation"}    213730
</code></pre>
<p>The trailing number is the counter value. Key attributes you'll be working with are <code>type</code>, <code>model</code>, and <code>effort</code>. The <a href="#heading-token-usage-by-type">Token Usage by Type</a> chart below groups on <code>type</code>.</p>
<p>The window total is the same query shape as <a href="#heading-total-usd-spent">Total USD Spent</a>, with the token counter:</p>
<pre><code class="language-promql">sum(increase(claude_code_token_usage_tokens_total[$__range]))
</code></pre>
<h4 id="heading-tokens-per-usd">Tokens Per USD</h4>
<p>Unlike the previous two metrics, this is a derived figure, calculated over a time window as Total Tokens / Total Cost.</p>
<pre><code class="language-promql">sum(increase(claude_code_token_usage_tokens_total[$__range])) / sum(increase(claude_code_cost_usage_USD_total[$__range]))
</code></pre>
<p>This one number collapses every attribute combination into a single value. Each distinct combination, for example model A at medium effort versus model B at high effort, is its own time series, and the query sums across all of them.</p>
<p>To analyze a specific combination, run the same ratio split by an attribute and compare.</p>
<pre><code class="language-promql">sum by (model) (increase(claude_code_token_usage_tokens_total[$__range]))
  / sum by (model) (increase(claude_code_cost_usage_USD_total[$__range]))
</code></pre>
<p>Swap <code>model</code> for <code>effort</code> or <code>type</code>; the raw series above lists the rest of the labels.</p>
<p>Overall Result:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/71b728df-e4c9-41bd-9b0b-cc0e0bade04f.png" alt="Stat Panel Aggregated Metrics" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Stats Panel(6hr window): Total USD Spent ($4.28), Total Tokens Spent (3.02M), and Tokens Per USD (707k).</p>
<h4 id="heading-token-usage-by-type">Token Usage by Type</h4>
<p>You'll see how <code>claude_code_token_usage_tokens_total</code> changes over time, broken down by <code>type</code>. A single stat hides the shape, so use a time-series panel.</p>
<p>The <code>type</code> attribute has four values, which differ a lot in cost:</p>
<ul>
<li><p><code>cacheRead</code>: tokens served from an existing cache entry. They dominate token spend in a long session, and are cheaper than the <a href="https://platform.claude.com/docs/en/about-claude/pricing#prompt-caching">baseline rate</a>.</p>
</li>
<li><p><code>cacheCreation</code>: tokens written into the prompt cache on the first prefix load. Costly.</p>
</li>
<li><p><code>input</code>: new, uncached prompt tokens.</p>
</li>
<li><p><code>output</code>: model-generated tokens.</p>
</li>
</ul>
<p>To also see the total, use two queries: the per-type breakdown and the un-split total for reference.</p>
<pre><code class="language-promql"># per-type breakdown
sum by (type) (increase(claude_code_token_usage_tokens_total[$__rate_interval]))
# total
sum(increase(claude_code_token_usage_tokens_total[$__rate_interval]))
</code></pre>
<p><code>__rate_interval</code> is Grafana's per-step window for time-series panels, the counterpart to the <code>__range</code> used for the stat panels above.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/8f5fabfa-3cb9-4bcb-bd50-6e95c216bb47.png" alt="Graphana Explore Timeseries" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The two queries running in Explore, before saving them as a panel.</p>
<p>After adding to the dashboard:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/e3cda31c-4741-4710-8bbf-de878a608d0e.png" alt="Running token usage by type" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Each spike is a burst of Claude Code activity, the flat stretches are idle time. Hovering a point splits the total into the four types: here the total is about 1.0M tokens, of which <code>cacheRead</code> is about 925k (roughly 92%), the rest cacheCreation, output, and input.</p>
<p><strong>TIP</strong>: Token consumption is dominated by <code>cacheRead</code>, which is also the cheapest type.</p>
<h4 id="heading-token-usage-by-model-and-effort">Token Usage by Model and Effort</h4>
<p>This is the concrete version of the breakdown suggested under Tokens Per USD: which <code>model</code> and <code>effort</code> pairs are actually consuming tokens.</p>
<pre><code class="language-promql">sum by (model, effort) (
  increase(claude_code_token_usage_tokens_total[$__rate_interval])
)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/47ee6a86-79a3-4bd2-9fff-7a2b64c77a7f.png" alt="Running token usage by model and effort" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Here the token counter is grouped by its <code>model</code> and <code>effort</code> attributes. In this window every series is <code>claude-sonnet-5</code> at either <code>medium</code> or <code>high</code> effort, and one burst of <code>medium</code> effort near 18:13 reaches about 2.6M tokens. The number of unique groupings depends on the cardinality of the chosen attributes.</p>
<h3 id="heading-logs">Logs</h3>
<p>A log record is a timestamped event with its full field set attached. You query logs when you want that specific event and the context around it: what happened, when, and with which values.</p>
<p>Metrics are the pre-aggregated form of the same activity. Anything that means counting, summing, or taking percentiles across many records belongs in a metric. If you're aggregating log output downstream, that data should have been a metric from the start.</p>
<p>Logs are the right tool for:</p>
<ol>
<li><p>Per-event context: the full detail of one occurrence, not a rolled-up number.</p>
</li>
<li><p>Discrete or irregular events: a compaction firing, a session start, or an API error.</p>
</li>
<li><p>Post-incident forensics: reading raw records back while debugging after the fact.</p>
</li>
<li><p>Trace correlation: a log line carrying a trace and span ID drops you into the request it came from.</p>
</li>
</ol>
<p>The log backend we're using here is <a href="https://grafana.com/oss/loki/">Loki</a>, queried with <a href="https://grafana.com/docs/loki/latest/query/">LogQL</a>. Running logs through a backend like this buys you:</p>
<ol>
<li><p>Structured fields: filter and compute on named keys instead of regex over text.</p>
</li>
<li><p>Field indexing: label lookups return without scanning every line.</p>
</li>
<li><p>Trace and span correlation: pivot from a log to its trace, or pull every log for one trace.</p>
</li>
<li><p>Time-bounded queries: each query is scoped to a window, keeping the scan cheap.</p>
</li>
</ol>
<p>Grafana reads Loki for dashboards, the same as it does for Prometheus.</p>
<h4 id="heading-compaction-event">Compaction Event</h4>
<p>Compaction is Claude Code trimming its own context when it grows too large. Each compaction emits a log event (<code>event_name="compaction"</code>) carrying the token counts before and after (<code>pre_tokens</code>, <code>post_tokens</code>) and the <code>span_id</code> it happened under, so a query over those events shows how often it fires and how much it reclaims each time.</p>
<p>In Grafana Explore, select Loki as the data source and paste the LogQL below. It selects the compaction events, parses their fields with <a href="https://grafana.com/docs/loki/latest/query/log_queries/"><code>logfmt</code></a>, and derives a per-event reduction percentage with <a href="https://grafana.com/docs/loki/latest/query/template_functions/"><code>label_format</code></a>. The fields only exist once compaction has actually happened, so trigger a few first.</p>
<pre><code class="language-logql">{service_name="claude-code"} | event_name="compaction"
  | logfmt
  | label_format reduction_pct=`{{ printf "%.1f" (mulf (divf (subf .pre_tokens .post_tokens) .pre_tokens) 100) }}`
</code></pre>
<p>Deriving <code>reduction_pct</code> for each record is fine here because it stays per-event. A running average across compactions would belong in a metric.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/a001cb54-bbeb-4a6c-931c-41ac6b7f7640.png" alt=" LogQL query for compaction event against Loki data source." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The <code>label_format</code> line adds a <code>reduction_pct</code> label. To show it as a table, switch the panel to Table view and add three Grafana transformations:</p>
<ol>
<li><p>Extract fields from the labels object.</p>
</li>
<li><p>Filter fields by name to keep Time, pre_tokens, post_tokens, reduction_pct, and span_id.</p>
</li>
<li><p>Convert field type to turn pre_tokens, post_tokens, and reduction_pct into numbers.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/921de4bb-1f19-48de-8447-200fd2d1f875.png" alt="Compaction events with pre/post token counts and the derived reduction percentage." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-tracing">Tracing</h3>
<p><a href="https://opentelemetry.io/docs/concepts/signals/traces/">Tracing</a> provides a detailed picture of the full path a request takes through an application, from start to completion. Some fundamental concepts behind tracing:</p>
<ul>
<li><p><strong>Span</strong>: a timed operation representing a unit of work. Building block for traces. All trace data is recorded as a sequence of spans, and each has a type, its operation name:</p>
<ul>
<li><p><code>claude_code.interaction</code>: one prompt and everything Claude Code does to answer it. Normally the root span, so one interaction is effectively one trace.</p>
</li>
<li><p><code>claude_code.llm_request</code>: a single model call inside an interaction.</p>
</li>
<li><p><code>claude_code.tool</code>: a single tool call inside an interaction (<code>Bash</code>, <code>Write</code>, <code>Agent</code>, and so on).</p>
</li>
</ul>
</li>
<li><p><strong>Trace</strong>: a tree of spans representing a request path from start to completion.</p>
</li>
<li><p><strong>Session</strong>: one Claude Code run, identified by <code>session.id</code>. It can result in many interactions and traces.</p>
</li>
<li><p><strong>Subagent</strong>: a nested Claude Code instance started by the <a href="https://code.claude.com/docs/en/sub-agents"><code>Agent</code> tool</a>, running its own interactions.</p>
</li>
</ul>
<p><a href="https://www.jaegertracing.io/">Jaeger</a> is the tracing backend used here. The Collector forwards spans to it over OTLP. Jaeger stores them and lets you <a href="https://www.jaegertracing.io/docs/latest/frontend-ui/">search traces</a> by service and span tags and inspect each one as a span tree. Everything below uses its UI at <code>localhost:16686</code>.</p>
<h4 id="heading-generating-traces">Generating Traces</h4>
<p>To generate trace data, this guide will use a test prompt to spawn agents and prepare some text. This prompt was tested with Sonnet 5 at medium effort.</p>
<p>You can paste the prompt directly into Claude Code:</p>
<pre><code class="language-text">Spawn 4 subagents in parallel, one per topic below. Each subagent researches its topic from your own knowledge and returns a ~150-word summary with 3 key points. Do not have them read files or run commands.
Topics:
1. How TCP congestion control works
2. The CAP theorem
3. How DNS resolution works
4. What a Bloom filter is

Once all 4 return, combine the summaries into one markdown document and write it to summary.md
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/ace08f8f-ff14-4e1e-aefa-2c23aea9ec5c.png" alt="Snapshot when running the prompt." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p><strong>Note</strong>: Ask Claude Code for the session_id in the same session after the prompt finishes. This will be used to find related traces in Jaeger.</p>
<h4 id="heading-trace-by-session-id">Trace By Session ID</h4>
<img src="https://cdn.hashnode.com/uploads/covers/69607e708806706b5c49c7af/bdbee8ee-99e0-4d2e-add7-b1689193cb4a.png" alt="Finding traces by session_id." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The search filters by <code>service = claude-code</code> and the tag <code>session.id=&lt;id&gt;</code>. It returns 6 traces, all rooted at <code>claude_code.interaction</code>, with span counts from 1 to 20 and durations from about 1 second to 33 seconds.</p>
<p>The list alone doesn't say which trace did what. Going through them by hand, or scripting it against the trace API for a real session, gives the following:</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Trace Name</th>
<th>Spans</th>
<th>Duration</th>
<th>llm_calls</th>
<th>tools</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>claude_code.interaction</td>
<td>1</td>
<td>1.4s</td>
<td>0</td>
<td>–</td>
</tr>
<tr>
<td>2</td>
<td>claude_code.interaction</td>
<td>3</td>
<td>4.6s</td>
<td>2</td>
<td>–</td>
</tr>
<tr>
<td>3</td>
<td>claude_code.interaction</td>
<td>1</td>
<td>5.4s</td>
<td>0</td>
<td>–</td>
</tr>
<tr>
<td>4</td>
<td>claude_code.interaction</td>
<td>1</td>
<td>2.5s</td>
<td>0</td>
<td>–</td>
</tr>
<tr>
<td>5</td>
<td>claude_code.interaction</td>
<td>20</td>
<td>15.7s</td>
<td>7</td>
<td>Agent(x4)</td>
</tr>
<tr>
<td>6</td>
<td>claude_code.interaction</td>
<td>15</td>
<td>32.5s</td>
<td>5</td>
<td>ScheduleWakeup(x2), Write(x1)</td>
</tr>
</tbody></table>
<p>A few observations:</p>
<ul>
<li><p>Half the traces are noise. Traces 1, 3, and 4 are single-span interactions with no model call or tool, an idle session being pinged. Trace 2 is a brief exchange. Only Traces 5 and 6 are the run.</p>
</li>
<li><p>The parallel dispatch is a single interaction. Trace 5 fires all four <code>Agent</code> calls inside one <code>claude_code.interaction</code>. Their nested model calls (7 to 10 seconds each) overlap, so the interaction finishes in about 16 seconds despite roughly 35 seconds of combined subagent LLM time.</p>
</li>
<li><p>Each subagent's model call is nested under its <code>Agent</code> span and carries an <code>agent_id</code>, so you can tell the four apart.</p>
</li>
<li><p><code>agent_id</code> is opaque. There's no <code>agent.name</code> or <code>skill.name</code>. The trace tells you four subagents ran and how long each took, not which topic each was given.</p>
</li>
<li><p>Spans carry token counts but no USD cost. Each <code>claude_code.llm_request</code> has <code>input_tokens</code>, <code>output_tokens</code>, <code>cache_read_tokens</code>, and <code>cache_creation_tokens</code>, but no USD figure.</p>
</li>
<li><p>The write is a separate, later interaction. Trace 6 has no <code>Agent</code> spans: one <code>claude_code.llm_request</code> of about 23 seconds produces the combined markdown, then a short <code>Write</code>. The two <code>ScheduleWakeup</code> spans are background coordination.</p>
</li>
</ul>
<p><strong>TIP</strong>: From the trace you get the four subagent calls, each with an <code>agent_id</code>, token counts, and timing, but no span says which topic a subagent was handed. In contrast, Metrics can provide attribution by <code>model</code>, <code>effort</code>, and skill.</p>
<p><strong>CAUTION</strong>: <code>user_prompt</code> is redacted by default on interaction spans. OTEL_LOG_USER_PROMPTS=1 disables this and logs raw prompt text. Avoid enabling it in multi-user/tenant environments since it exposes prompt content to anyone with access to the telemetry backend.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This guide was an end-to-end walkthrough of observability in Claude Code, enabling its telemetry and collecting each of the three signals in a local backend for analysis.</p>
<p>Metrics give you the ability to dissect cumulative cost and usage readings by attribute over a chosen time period. That matters most in a shared or multi-tenant setup, where cost isn't tied to a single owner and someone still has to account for it.</p>
<p>Logs are records of individual events, useful for digging into exactly what changed during one, like compaction.</p>
<p>Traces show how one prompt expands into subagents and model calls, with timing and token counts on each. That's the starting point for debugging or tightening a complex or multi-agent prompt, though the spans don't yet record which prompt or skill drove a given call.</p>
<p>Some of this telemetry is behind the Enhanced Telemetry beta, so span names and attributes can still change, and gaps like per-call attribution may close as it matures. It's worth re-checking the <a href="https://code.claude.com/docs/en/monitoring-usage">monitoring docs</a> as the surface settles.</p>
<h2 id="heading-references">References</h2>
<ul>
<li><p><a href="https://platform.claude.com/docs/en/about-claude/pricing#prompt-caching">Claude Pricing: Prompt Caching</a></p>
</li>
<li><p><a href="https://code.claude.com/docs/en/monitoring-usage">Claude Code: Monitoring Usage</a></p>
</li>
<li><p><a href="https://prometheus.io/docs/concepts/metric_types/#counter">Prometheus: Counter Metric Type</a></p>
</li>
<li><p><a href="https://www.jaegertracing.io/docs/latest/frontend-ui/">Jaeger: Finding Traces</a></p>
</li>
<li><p><a href="https://grafana.com/docs/loki/latest/query/log_queries/">Loki: LogQL Log Queries</a></p>
</li>
<li><p><a href="https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/stat/">Grafana: Stat Panel</a></p>
</li>
<li><p><a href="https://opentelemetry.io/docs/collector/configuration/">OpenTelemetry Collector: Configuration</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Convert Prometheus Histograms to OTLP with the OpenTelemetry Collector ]]>
                </title>
                <description>
                    <![CDATA[ Modern applications often expose metrics at a /metrics endpoint using the Prometheus format. Among these metrics, histograms are particularly useful. They show how often values fall into different ran ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-convert-prometheus-histogramsotlp-with-the-opentelemetry-collector/</link>
                <guid isPermaLink="false">6a8c4969642222471a04f943</guid>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloud native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #prometheus ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Devops articles ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Purity Udeh ]]>
                </dc:creator>
                <pubDate>Sat, 22 Aug 2026 03:00:00 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/3dccb28f-d024-4c7a-9ac7-353594572842.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Modern applications often expose metrics at a <code>/metrics</code> endpoint using the Prometheus format.</p>
<p>Among these metrics, histograms are particularly useful. They show how often values fall into different ranges, like HTTP request durations, database query times, or queue processing latencies.</p>
<p>Unlike simple averages, histograms show the full picture: you can see how many requests are fast, how many are slow, and where the occasional outliers occur that might be silently degrading the user experience.</p>
<p>In payment systems, for instance, a sudden spike in transactions can expose hidden bottlenecks. Most of the requests might complete quickly, but a small percentage of slow transactions can ripple through the system, impacting retries, failures, and overall throughput. Histograms help identify these issues early by showing how values are distributed and highlighting outliers that averages obscure.</p>
<p>But not all backends understand Prometheus metrics natively. Many modern observability platforms prefer <strong>OTLP (OpenTelemetry Protocol)</strong>. Forwarding Prometheus metrics without converting them can lead to incomplete or misinterpreted data. That’s why we need a pipline to scrape, transform, and export histograms into OTLP so that your observability pipeline remains consistent and actionable.</p>
<p>In this article, we'll use the OpenTelemetry Collector to scrape Prometheus histograms from application <code>/metrics</code> endpoints, map them to the OpenTelemetry Histogram data model, and export them to our observability backend using OTLP. The Collector acts as a bridge that preserves data fidelity while ensuring compatibility with your monitoring platform.</p>
<p>To make this concrete, we'll use a small FastAPI application that simulates payment transactions. It exposes two Prometheus metrics: <code>payment_transaction_duration_seconds</code> (a histogram tracking how long each transaction takes) and <code>payment_transactions_total</code> (a counter of completed transactions). You can follow along with your own instrumented application, as anything exposing Prometheus metrics at <code>/metrics</code> will work the same way. This is the metric we'll follow from the application all the way to the observability backend.</p>
<h2 id="heading-what-well-cover">What We'll Cover:</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-1-how-to-scrape-metrics-with-prometheus-receiver"><strong>1. How to Scrape Metrics with Prometheus Receiver</strong></a></p>
</li>
<li><p><a href="#heading-2-transforming-prometheus-histograms"><strong>2. Transforming Prometheus Histograms</strong></a></p>
</li>
<li><p><a href="#heading-3-exporting-metrics-via-otlp"><strong>3. Exporting Metrics via OTLP</strong></a></p>
</li>
<li><p><a href="#heading-4-putting-the-pipeline-together"><strong>4. Putting the Pipeline Together</strong></a></p>
</li>
<li><p><a href="#heading-5-running-the-opentelemetry-collector"><strong>5. Running the OpenTelemetry Collector</strong></a></p>
<ul>
<li><p><a href="#heading-51-set-up-signoz-cloud"><strong>5.1 Set Up SigNoz Cloud</strong></a></p>
</li>
<li><p><a href="#heading-52-start-the-fastapi-application"><strong>5.2 Start the FastAPI Application</strong></a></p>
</li>
<li><p><a href="#heading-53-start-the-collector"><strong>5.3 Start the Collector</strong></a></p>
</li>
<li><p><a href="#heading-54-generate-test-transactions"><strong>5.4 Generate Test Transactions</strong></a></p>
</li>
<li><p><a href="#heading-55-confirm-backend-receipt"><strong>5.5 Confirm Backend Receipt</strong></a></p>
</li>
<li><p><a href="#heading-56-troubleshooting"><strong>5.6 Troubleshooting</strong></a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion"><strong>Conclusion</strong></a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you have:</p>
<ul>
<li><p>Docker installed</p>
</li>
<li><p>An application exposing Prometheus metrics through a <code>/metrics</code> endpoint</p>
</li>
<li><p>An OTLP-compatible observability backend</p>
</li>
<li><p>Basic knowledge of Prometheus metrics</p>
</li>
<li><p>Basic knowledge of YAML</p>
</li>
<li><p>Basic familiarity with Docker and OpenTelemetry</p>
</li>
</ul>
<p>You don't need advanced OpenTelemetry knowledge to follow this tutorial. I'll walk through the Prometheus histogram and show what happens to it as it moves through the OpenTelemetry Collector.</p>
<h2 id="heading-1-how-to-scrape-metrics-with-prometheus-receiver">1. How to Scrape Metrics with Prometheus Receiver</h2>
<p>First, we'll collect metrics from the application. The demo application exposes its Prometheus metrics at the <code>/metrics</code> endpoint, and the Prometheus receiver periodically scrapes this endpoint and ingests the metrics into the observability pipeline. You can also inspect <code>/metrics</code> directly to see the data before the Collector reads it.</p>
<p>Configuration setup example:</p>
<pre><code class="language-yaml">receivers:
  prometheus:
    config:
      scrape_configs:
        - job_name: payment-demo
          scrape_interval: 15s
          static_configs:
            - targets: ["payment-api:8080"]
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/611e0999c4783a33f5e25171/5e891138-0408-41ee-9ae9-a68ac056aa81.png" alt="FastAPI /metrics output showing the payment transaction duration histogram in Prometheus format" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>What's happening behind the scenes here:</p>
<p><code>scrape_interval</code> controls how often the Collector scrapes the target. Here, we're using 15 seconds; adjust it based on how frequently you need metric updates and the load your application can handle.</p>
<p>In high-throughput systems like payment platforms or real-time processing services, the scrape interval becomes particularly important. Setting it too long may cause you to miss short-lived performance issues or transient errors, while setting it too short risks overwhelming the service with scraping requests or generating excessive network traffic.</p>
<p><code>targets</code> lists the specific endpoints that expose Prometheus metrics. You can add multiple targets when you need to scrape more than one service.</p>
<p>Finally, <code>job_name</code> is an identifier that helps group metrics logically, making them easier to manage and analyze once they reach your backend.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770317251340/2853e8ee-b3d9-49c4-b931-3194eb2ea7d2.png" alt="Diagram showing an OpenTelemetry Collector discovering metrics targets, assigning targets, querying endpoints, and scraping metrics." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-2-transforming-prometheus-histograms">2. Transforming Prometheus Histograms</h2>
<p>Once the metrics are scraped, the next step is transformation. Prometheus exposes histograms as multiple time series:</p>
<ul>
<li><p><code>_bucket</code> shows how many requests fall below a certain duration.</p>
</li>
<li><p><code>_sum</code> is the total of all observed durations.</p>
</li>
<li><p><code>_count</code> is the number of observations.</p>
</li>
</ul>
<p>The <code>payment_transaction_duration_seconds</code> histogram records how long each payment transaction takes. Prometheus exposes it as <code>_bucket</code>, <code>_count</code>, and <code>_sum</code> series. When the Prometheus receiver collects these metrics, it converts them into the OpenTelemetry Histogram data model, which can then be exported through OTLP while preserving the information needed to analyze transaction latency and calculate percentiles.</p>
<p>Without histograms, averages obscure latency distributions. If most requests complete in 50ms but 5% take 2+ seconds, the average of 150ms masks that serious performance issue. Histograms capture the complete picture by recording how many observations fall into each latency bucket.</p>
<p>Keeping the distribution allows you to identify changes in latency and investigate issues such as slow database queries, overloaded services, or delays from downstream dependencies.</p>
<h3 id="heading-actual-prometheus-histogram">Actual Prometheus Histogram</h3>
<p>This is how your actual <code>/metrics</code> output would look, for example:</p>
<img src="https://cdn.hashnode.com/uploads/covers/611e0999c4783a33f5e25171/8d1e4857-7d39-4cb1-99f7-5ce184adb59c.png" alt="Prometheus /metrics output showing the payment transaction duration histogram as _bucket time series with different latency boundaries" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>And here's the actual OpenTelemetry Histogram representation:</p>
<img src="https://cdn.hashnode.com/uploads/covers/611e0999c4783a33f5e25171/ef856e3e-99d5-42af-9f39-3eb88d752505.png" alt="OpenTelemetry Collector output showing the payment_transaction_duration_seconds metric as a Histogram with bucket boundaries and counts." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The output above shows the same transaction-duration data represented as an OpenTelemetry Histogram. Instead of three separate Prometheus series, the Collector now has one histogram containing the count, sum, explicit bucket boundaries, and bucket counts.</p>
<h2 id="heading-3-exporting-metrics-via-otlp">3. Exporting Metrics via OTLP</h2>
<p>Once the metrics have been scraped and processed, the Collector sends them to your observability backend using OTLP. The OTLP exporter sends the processed metrics, including histograms, counters, and gauges, to the backend.</p>
<pre><code class="language-yaml">exporters:
&nbsp; otlp:
&nbsp; &nbsp; endpoint: "otlp.backend.example.com:4317"
&nbsp; &nbsp; tls:
&nbsp; &nbsp; &nbsp; insecure: false
</code></pre>
<p><code>endpoint</code> specifies the backend address for receiving OTLP metrics. This typically points to a central observability platform that aggregates metrics from multiple services across your infrastructure.</p>
<p><code>tls</code> ensures secure data transmission between the collector and your backend. Set <code>insecure: true</code> only when intentionally connecting to an endpoint that does not use TLS, such as some local development setups</p>
<h2 id="heading-4-putting-the-pipeline-together">4. Putting the Pipeline Together</h2>
<p>We've looked at each part of the pipeline individually. Now let's connect them and follow a metric from the application all the way to the backend.</p>
<pre><code class="language-yaml">service:
  pipelines:
    metrics:
      receivers: [prometheus]
      exporters: [otlp]
</code></pre>
<p>This is the complete path our <code>payment_transaction_duration_seconds</code> metric follows, from the FastAPI application to the observability backend. The diagram above shows this flow. The next section walks through actually running it and connecting to SigNoz</p>
<p><strong>Pipeline flow:</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/611e0999c4783a33f5e25171/ba4d6362-0e7a-4194-ba89-6fb3b4687489.png" alt="Pipeline flow architecture" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The above architecture diagram shows a FastAPI application exposing metrics through /metrics, the OpenTelemetry Collector scraping them with the Prometheus receiver, processing the metrics, and exporting them through OTLP to an observability backend.</p>
<h2 id="heading-5-running-the-opentelemetry-collector">5. Running the OpenTelemetry Collector</h2>
<p>We'll run the complete pipeline and verify that the transaction metrics make it from the application to SigNoz. Follow these steps (which I'll walk you through in detail below):</p>
<ol>
<li><p><strong>Set up SigNoz Cloud:</strong> Configure the endpoint and ingestion key that the Collector will use.</p>
</li>
<li><p><strong>Start the FastAPI application:</strong> The application exposes the Prometheus metrics at <code>/metrics</code>.</p>
</li>
<li><p><strong>Start the OpenTelemetry Collector:</strong> The Collector begins scraping the application's <code>/metrics</code> endpoint using the Prometheus receiver.</p>
</li>
<li><p><strong>Generate test transactions:</strong> Send requests to the application to create transaction-duration measurements.</p>
</li>
<li><p><strong>Verify the Collector and backend:</strong> Check the Collector logs to confirm that the pipeline is running, then open SigNoz and verify that the <code>payment_transaction_duration_seconds</code> metric has arrived.</p>
</li>
<li><p>Troubleshooting</p>
</li>
</ol>
<h3 id="heading-51-set-up-signoz-cloud">5.1 Set Up SigNoz Cloud</h3>
<p>SigNoz provides the observability backend that will receive the metrics exported by the OpenTelemetry Collector. For this demo, we'll use SigNoz Cloud as the observability backend, so there's nothing to install locally.</p>
<p>First, sign up for a free account at <a href="http://signoz.io">signoz.io</a>. In the SigNoz Cloud dashboard, go to <strong>Settings → Ingestion Keys</strong>. The page shows your Ingestion URL, Region, and Ingestion Key.</p>
<p>Add these to a <code>.env</code> file in your project directory:</p>
<pre><code class="language-shell">SIGNOZ_INGESTION_KEY=your-real-key
SIGNOZ_OTLP_ENDPOINT=ingest.&lt;your-region&gt;.signoz.cloud:443
</code></pre>
<p>Treat the ingestion key like a password and never commit it or share it publicly.</p>
<p>Reference those variables in your Collector configuration:</p>
<pre><code class="language-shell">exporters:
  otlp:
    endpoint: "${SIGNOZ_OTLP_ENDPOINT}"
    tls:
      insecure: false
    headers:
      signoz-ingestion-key: "${SIGNOZ_INGESTION_KEY}"
</code></pre>
<p>Then add <code>.env</code> to your <code>.gitignore</code> so the key never gets committed:</p>
<pre><code class="language-plaintext">echo ".env" &gt;&gt; .gitignore
</code></pre>
<h3 id="heading-52-start-the-fastapi-application">5.2 Start the FastAPI Application.</h3>
<p>Start the application on port 8080:</p>
<pre><code class="language-shell">uvicorn app.main:app --reload --port 8080
</code></pre>
<p>Verify that the application is running:</p>
<pre><code class="language-shell">curl http://localhost:8080/
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/611e0999c4783a33f5e25171/b27bcf94-e5da-43e6-b51f-f17fe06d90d9.png" alt="FastAPI payment demo running locally at 127.0.0.1:8080, displaying a JSON response confirming that the payment transaction demo is running" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Now inspect the Prometheus metrics:</p>
<pre><code class="language-shell">curl -Ls http://localhost:8080/metrics
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/611e0999c4783a33f5e25171/10c4e1d5-d117-419d-8557-f94884f0bad8.png" alt="Browser showing the FastAPI payment demo running successfully at localhost:8080" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The <code>/metrics</code> endpoint exposes the application's Prometheus metrics, including the <code>payment_transaction_duration_seconds</code> histogram that the Collector will scrape.</p>
<h3 id="heading-53-start-the-collector">5.3 Start the Collector</h3>
<p>With the application running and the Collector configuration in place, start the Collector:</p>
<pre><code class="language-shell">docker compose up --build
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/611e0999c4783a33f5e25171/3ef283ca-bea7-4fd3-82b5-9684c3b5dea9.png" alt="Docker Compose logs showing the payment API running and the Collector successfully scraping its /metrics endpoint." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>Check the Collector logs to confirm that the pipeline is running and that metrics are being processed.</p>
<p>This builds the <code>payment-api</code> image, starts both containers on the shared <code>telemetry</code> network, and the Collector immediately begins scraping <code>/metrics</code> from the application using the Prometheus receiver, reading your SigNoz ingestion key and endpoint from <code>.env</code> automatically.</p>
<h3 id="heading-54-generate-test-transactions">5.4 Generate Test Transactions</h3>
<p>Generate transactions with different processing times:</p>
<pre><code class="language-shell">curl -X POST "http://localhost:8080/transactions?delay_ms=50"

curl -X POST "http://localhost:8080/transactions?delay_ms=250"

curl -X POST "http://localhost:8080/transactions?delay_ms=2500"
</code></pre>
<p>Generate additional requests if you want more observations in the histogram.</p>
<p>These requests create transaction-duration observations that are recorded by the <code>payment_transaction_duration_seconds</code> histogram. The Collector picks up the updated metric during its next scrape.</p>
<h3 id="heading-55-confirm-backend-receipt">5.5 Confirm Backend Receipt</h3>
<p>Open SigNoz Cloud and search for:</p>
<pre><code class="language-plaintext">payment_transaction_duration_seconds
</code></pre>
<p>The metric should be represented as a <strong>histogram</strong>, with its bucket distribution <code>_bucket</code>, <code>_sum</code>, and <code>_count</code> available to the backend rather than appearing as unrelated Prometheus series.</p>
<img src="https://cdn.hashnode.com/uploads/covers/611e0999c4783a33f5e25171/e4ea5b60-b7d4-4435-a427-d738e45ad19b.png" alt="SigNoz Metrics Explorer showing payment_transaction_duration_seconds as a histogram with associated bucket, count, and sum data after being exported through the OpenTelemetry Collector." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-56-troubleshooting">5.6 Troubleshooting</h3>
<p>If metrics aren't flowing correctly, start by checking the log:</p>
<pre><code class="language-bash">docker logs &lt;collector-container&gt;
</code></pre>
<p>Look for connection errors, authentication failures, failed scrape attempts, or configuration errors.</p>
<p>Also verify that:</p>
<ul>
<li><p>The FastAPI application is running.</p>
</li>
<li><p><code>/metrics</code> is accessible.</p>
</li>
<li><p>The Collector can reach the application.</p>
</li>
<li><p>The SigNoz endpoint and ingestion key are correct.</p>
</li>
<li><p>The <code>.env</code> variables are available to the Collector.</p>
</li>
<li><p>The receiver and exporter names match the pipeline configuration.</p>
</li>
</ul>
<p>Use the <code>--dry-run</code> flag if available to validate before deployment.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Prometheus histograms provide a useful view of transaction latency by showing how observations are distributed across different buckets rather than reducing them to a single average.</p>
<p>In this tutorial, we followed <code>payment_transaction_duration_seconds</code> from a FastAPI application's <code>/metrics</code> endpoint through the OpenTelemetry Collector and into SigNoz.</p>
<p>The Prometheus receiver mapped the <code>_bucket</code>, <code>_count</code>, and <code>_sum</code> series into the OpenTelemetry Histogram data model, preserving the distribution of transaction durations for analysis in the backend.</p>
<p>This allows the same metric to move from a Prometheus-instrumented application into an OTLP-based observability platform without manually reconstructing the histogram.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Debug CI/CD Pipelines: A Handbook on Troubleshooting with Observability Tools ]]>
                </title>
                <description>
                    <![CDATA[ Observability is a game-changer for CI/CD pipelines, and it’s one of the most exciting aspects of DevOps. When I started working with CI/CD systems, I assumed the hardest part would be building the pipeline. But with increasingly complex setups, the ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-debug-cicd-pipelines-handbook/</link>
                <guid isPermaLink="false">6850a9eb7255997ee3d47265</guid>
                
                    <category>
                        <![CDATA[ Devops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #prometheus ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Grafana ]]>
                    </category>
                
                    <category>
                        <![CDATA[ promql ]]>
                    </category>
                
                    <category>
                        <![CDATA[ loki ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Opaluwa Emidowojo ]]>
                </dc:creator>
                <pubDate>Mon, 16 Jun 2025 23:34:03 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748620971355/d4893ec5-8016-491e-9626-15d971f0c885.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Observability is a game-changer for CI/CD pipelines, and it’s one of the most exciting aspects of DevOps. When I started working with CI/CD systems, I assumed the hardest part would be building the pipeline. But with increasingly complex setups, the real challenge is debugging failures, like builds crashing or tests failing only in production.</p>
<p>Observability tools, such as logs, metrics, and traces, provide the visibility you need to pinpoint issues quickly. In this handbook, we’ll explore free and open-source tools you can use to make your CI/CD pipelines more reliable. We’ll use practical steps to troubleshoot like a pro – no enterprise licenses required.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-observability-is-important">Why Observability is Important</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-install-and-configure-grafana-loki-on-budget-infrastructure">How to Install and Configure Grafana Loki on Budget Infrastructure</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-implement-an-elk-stack-alternative-for-pipeline-observability">How to Implement an ELK Stack Alternative for Pipeline Observability</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-a-unified-logging-strategy-across-pipeline-components">How to Create a Unified Logging Strategy Across Pipeline Components</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-query-and-analyze-logs-for-effective-troubleshooting">How to Query and Analyze Logs for Effective Troubleshooting</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-prometheus-metrics-alongside-your-logs">How to Set Up Prometheus Metrics Alongside Your Logs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-grafana-dashboards-that-combine-metrics-and-logs">How to Create Grafana Dashboards That Combine Metrics and Logs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-use-exemplars-to-jump-from-metrics-to-relevant-logs">How to Use Exemplars to Jump from Metrics to Relevant Logs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-diagnose-and-fix-common-cicd-problems">How to Diagnose and Fix Common CI/CD Problems</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-implement-advanced-debugging-techniques">How to Implement Advanced Debugging Techniques</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-conduct-effective-postmortems-using-logs">How to Conduct Effective Postmortems Using Logs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-optimize-log-storage-and-management">How to Optimize Log Storage and Management</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>There are some things you should know and have to get the most out of this handbook:</p>
<h4 id="heading-technical-knowledge">Technical Knowledge:</h4>
<ul>
<li><p>Basic understanding of <a target="_blank" href="https://www.freecodecamp.org/news/what-is-ci-cd/">CI/CD pipelines</a> (for example, build, test, deploy stages).</p>
</li>
<li><p>Familiarity with <a target="_blank" href="https://www.freecodecamp.org/news/helpful-linux-commands-you-should-know/">Linux/Unix commands</a> (for example, <code>mkdir</code>, <code>grep</code>, <code>curl</code>).</p>
</li>
<li><p>Comfortable with <a target="_blank" href="https://www.freecodecamp.org/news/the-docker-handbook/">Docker basics</a> (for example, <code>docker run</code>, <code>docker-compose up</code>).</p>
</li>
<li><p>Optional: Awareness of <a target="_blank" href="https://www.freecodecamp.org/news/observability-in-cloud-native-applications/">observability concepts</a> (logs, metrics, traces) or YAML configuration.</p>
</li>
</ul>
<h4 id="heading-software-and-tools">Software and Tools:</h4>
<ul>
<li><p><strong>Docker and Docker Compose</strong>: Installed and running (verify with <code>docker --version</code> and <code>docker-compose --version</code>).</p>
</li>
<li><p><strong>CI/CD Platform</strong>: Access to GitHub Actions, Jenkins, or GitLab CI with a sample pipeline that generates logs.</p>
</li>
<li><p><strong>Text Editor</strong>: For editing YAML files (for example, VS Code, Nano).</p>
</li>
<li><p><strong>Web Browser</strong>: To access tool UIs (for example, Grafana on port 3000, Kibana on 5601).</p>
</li>
<li><p>Optional: <code>curl</code> for testing log forwarding, Git for version control.</p>
</li>
</ul>
<h4 id="heading-hardware-and-infrastructure">Hardware and Infrastructure:</h4>
<ul>
<li><p>Machine with:</p>
<ul>
<li><p>OS: Linux, Windows (with WSL2), or macOS.</p>
</li>
<li><p>4GB RAM (8GB recommended), 20GB free disk space.</p>
</li>
<li><p>Stable internet and ability to open ports (for example, 3100 for Loki, 9200 for Elasticsearch).</p>
</li>
</ul>
</li>
<li><p>Optional: Cloud provider access (for example, AWS, GCP) for scalable setups.</p>
</li>
</ul>
<h4 id="heading-access-and-permissions">Access and Permissions:</h4>
<ul>
<li><p>Admin access to install Docker and configure CI/CD tools.</p>
</li>
<li><p>Permissions to modify pipeline configs (for example, <code>.github/workflows</code>, <code>.gitlab-ci.yml</code>).</p>
</li>
<li><p>Optional: Container registry access (for example, Docker Hub) for custom images.</p>
</li>
</ul>
<h2 id="heading-why-observability-is-important"><strong>Why Observability is Important</strong></h2>
<p>Modern CI/CD pipelines are no longer linear scripts – they are now complex, distributed systems involving multiple tools, environments, and infrastructure layers. One job runs on GitHub Actions, another deploys via Jenkins, and a third builds Docker images in a Kubernetes cluster.</p>
<p>So when something breaks, you’re left chasing logs across tools, guessing where the issue originated, and wasting hours trying to reproduce it.</p>
<p>And worse still, traditional debugging tools often stop at the surface, only showing failed jobs without the context of <em>why</em> they failed or <em>where</em> in the system the fault actually lies.</p>
<p>Observability flips the script. Instead of hunting through disconnected logs or rerunning failed builds blindly, observability gives you <strong>insight</strong>, not just data. By combining structured logs, metrics, and traces, you can:</p>
<ul>
<li><p>Reconstruct exactly what happened in a pipeline failure</p>
</li>
<li><p>Trace a failure across CI agents, deployment steps, and containers</p>
</li>
<li><p>Visualize patterns and anomalies before they become outages</p>
</li>
</ul>
<p>More importantly, observability helps you <strong>move from reactive debugging to proactive prevention</strong>.</p>
<p>Here’s what you’ll learn about and accomplish in this guide:</p>
<ul>
<li><p>Set up cost-effective observability using Grafana Loki, lightweight ELK, and OpenTelemetry</p>
</li>
<li><p>Create a unified logging strategy to connect your pipeline</p>
</li>
<li><p>Write precise queries to quickly pinpoint root causes, correlate logs, metrics, and traces for comprehensive debugging</p>
</li>
<li><p>Troubleshoot CI/CD issues like build failures, flaky tests, and container crashes</p>
</li>
<li><p>Build custom dashboards and automated diagnostic tools</p>
</li>
<li><p>Promote observability through documentation and post-mortems</p>
</li>
</ul>
<p>Whether you're a solo developer or part of a DevOps team, this guide will transform your chaotic CI/CD pipelines into clear, reliable, and observable systems.</p>
<h3 id="heading-how-to-choose-the-right-observability-tool-for-cicd"><strong>How to Choose the Right Observability Tool for CI/CD</strong></h3>
<p>Here’s a quick comparison of Grafana Loki, Lightweight ELK, and Vector for CI/CD observability:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Tool</strong></td><td><strong>Resource Usage</strong></td><td><strong>Setup Complexity</strong></td><td><strong>Best For</strong></td><td><strong>CI/CD Fit</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Grafana Loki</strong></td><td>Low (lightweight)</td><td>Easy (Docker-based)</td><td>Small teams, budget infra</td><td>Simple pipelines, JSON logs, Grafana users</td></tr>
<tr>
<td><strong>Lightweight ELK</strong></td><td>High (Elasticsearch-heavy)</td><td>Moderate (multi-container)</td><td>Teams needing advanced search/visualization</td><td>Complex pipelines, rich querying needs</td></tr>
<tr>
<td><strong>Vector</strong></td><td>Very low</td><td>Easy (single binary)</td><td>Resource-constrained setups</td><td>Minimal setups, log forwarding</td></tr>
</tbody>
</table>
</div><p>How to choose:</p>
<ul>
<li><p><strong>Loki</strong>: Ideal for startups or solo devs with limited resources. Integrates well with Prometheus/Grafana.</p>
</li>
<li><p><strong>ELK</strong>: Best for teams needing Kibana’s advanced visualizations or handling large log volumes.</p>
</li>
<li><p><strong>Vector</strong>: Great for lightweight log forwarding in distributed CI/CD setups.</p>
</li>
</ul>
<p><strong>Grafana Loki</strong> is a log aggregation system like ELK, but it's more lightweight, and it’s ideal for CI/CD pipelines with limited infrastructure.</p>
<h2 id="heading-how-to-install-and-configure-grafana-loki-on-budget-infrastructure">How to Install and Configure Grafana Loki on Budget Infrastructure</h2>
<h3 id="heading-option-a-quick-docker-setup-recommended-for-budget-infra">🛠 Option A: Quick Docker Setup (Recommended for Budget Infra)</h3>
<ol>
<li><p><strong>Create a directory for configuration:</strong></p>
<pre><code class="lang-bash"> mkdir -p ~/loki-setup &amp;&amp; <span class="hljs-built_in">cd</span> ~/loki-setup
</code></pre>
</li>
<li><p><strong>Create a</strong> <code>docker-compose.yml</code>:</p>
<pre><code class="lang-yaml"> <span class="hljs-comment"># Defines a Docker Compose setup for Grafana Loki and Promtail to aggregate and scrape logs efficiently.</span>
 <span class="hljs-attr">version:</span> <span class="hljs-string">"3"</span>

 <span class="hljs-attr">services:</span>
   <span class="hljs-attr">loki:</span>
     <span class="hljs-attr">image:</span> <span class="hljs-string">grafana/loki:2.9.4</span>  <span class="hljs-comment"># Uses Loki version 2.9.4 for lightweight log aggregation.</span>
     <span class="hljs-attr">ports:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">"3100:3100"</span>  <span class="hljs-comment"># Exposes Loki’s HTTP API port for log ingestion and queries.</span>
     <span class="hljs-attr">command:</span> <span class="hljs-string">-config.file=/etc/loki/loki-config.yaml</span>  <span class="hljs-comment"># Specifies the configuration file for Loki.</span>
     <span class="hljs-attr">volumes:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">./loki-config.yaml:/etc/loki/loki-config.yaml</span>  <span class="hljs-comment"># Mounts the local config file into the container.</span>

   <span class="hljs-attr">promtail:</span>
     <span class="hljs-attr">image:</span> <span class="hljs-string">grafana/promtail:2.9.4</span>  <span class="hljs-comment"># Uses Promtail version 2.9.4 to scrape and forward logs to Loki.</span>
     <span class="hljs-attr">volumes:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">/var/log:/var/log</span>  <span class="hljs-comment"># Mounts the host’s log directory for Promtail to scrape.</span>
       <span class="hljs-bullet">-</span> <span class="hljs-string">./promtail-config.yaml:/etc/promtail/promtail-config.yaml</span>  <span class="hljs-comment"># Mounts the Promtail config file.</span>
     <span class="hljs-attr">command:</span> <span class="hljs-string">-config.file=/etc/promtail/promtail-config.yaml</span>  <span class="hljs-comment"># Specifies the configuration file for Promtail.</span>
</code></pre>
</li>
<li><p><strong>Create a basic</strong> <code>loki-config.yaml</code>:</p>
<pre><code class="lang-yaml"> <span class="hljs-comment"># Configures Grafana Loki for lightweight log storage and querying in a CI/CD environment.</span>
 <span class="hljs-attr">auth_enabled:</span> <span class="hljs-literal">false</span>  <span class="hljs-comment"># Disables authentication for simplicity (not recommended for production).</span>

 <span class="hljs-attr">server:</span>
   <span class="hljs-attr">http_listen_port:</span> <span class="hljs-number">3100</span>  <span class="hljs-comment"># Sets the port for Loki’s HTTP API.</span>

 <span class="hljs-attr">ingester:</span>
   <span class="hljs-attr">lifecycler:</span>
     <span class="hljs-attr">ring:</span>
       <span class="hljs-attr">kvstore:</span>
         <span class="hljs-attr">store:</span> <span class="hljs-string">inmemory</span>  <span class="hljs-comment"># Uses in-memory storage for the ring, suitable for small setups.</span>
       <span class="hljs-attr">replication_factor:</span> <span class="hljs-number">1</span>  <span class="hljs-comment"># Sets single replica for minimal resource use.</span>
   <span class="hljs-attr">chunk_idle_period:</span> <span class="hljs-string">3m</span>  <span class="hljs-comment"># Flushes chunks to storage after 3 minutes of inactivity.</span>
   <span class="hljs-attr">max_chunk_age:</span> <span class="hljs-string">1h</span>  <span class="hljs-comment"># Retires chunks after 1 hour to balance storage and query performance.</span>

 <span class="hljs-attr">schema_config:</span>
   <span class="hljs-attr">configs:</span>
     <span class="hljs-bullet">-</span> <span class="hljs-attr">from:</span> <span class="hljs-number">2023-01-01</span>  <span class="hljs-comment"># Defines the schema start date.</span>
       <span class="hljs-attr">store:</span> <span class="hljs-string">boltdb-shipper</span>  <span class="hljs-comment"># Uses BoltDB for indexing logs.</span>
       <span class="hljs-attr">object_store:</span> <span class="hljs-string">filesystem</span>  <span class="hljs-comment"># Stores logs on the local filesystem.</span>
       <span class="hljs-attr">schema:</span> <span class="hljs-string">v11</span>  <span class="hljs-comment"># Specifies schema version for log storage.</span>
       <span class="hljs-attr">index:</span>
         <span class="hljs-attr">prefix:</span> <span class="hljs-string">index_</span>  <span class="hljs-comment"># Prefix for index files.</span>
         <span class="hljs-attr">period:</span> <span class="hljs-string">24h</span>  <span class="hljs-comment"># Rotates indexes daily.</span>

 <span class="hljs-attr">storage_config:</span>
   <span class="hljs-attr">boltdb_shipper:</span>
     <span class="hljs-attr">active_index_directory:</span> <span class="hljs-string">/tmp/loki/index</span>  <span class="hljs-comment"># Directory for active index files.</span>
     <span class="hljs-attr">cache_location:</span> <span class="hljs-string">/tmp/loki/boltdb-cache</span>  <span class="hljs-comment"># Cache location for BoltDB.</span>
   <span class="hljs-attr">filesystem:</span>
     <span class="hljs-attr">directory:</span> <span class="hljs-string">/tmp/loki/chunks</span>  <span class="hljs-comment"># Directory for storing log chunks.</span>

 <span class="hljs-attr">limits_config:</span>
   <span class="hljs-attr">enforce_metric_name:</span> <span class="hljs-literal">false</span>  <span class="hljs-comment"># Disables strict metric name enforcement for flexibility.</span>
</code></pre>
</li>
<li><p><strong>Create a basic</strong> <code>promtail-config.yaml</code>:</p>
<pre><code class="lang-yaml"> <span class="hljs-comment"># Configures Promtail to scrape system logs and forward them to Loki.</span>
 <span class="hljs-attr">server:</span>
   <span class="hljs-attr">http_listen_port:</span> <span class="hljs-number">9080</span>  <span class="hljs-comment"># Sets Promtail’s HTTP port for metrics and health checks.</span>
   <span class="hljs-attr">grpc_listen_port:</span> <span class="hljs-number">0</span>  <span class="hljs-comment"># Disables gRPC to reduce resource usage.</span>

 <span class="hljs-attr">positions:</span>
   <span class="hljs-attr">filename:</span> <span class="hljs-string">/tmp/positions.yaml</span>  <span class="hljs-comment"># Stores the position of scraped logs to resume after restarts.</span>

 <span class="hljs-attr">clients:</span>
   <span class="hljs-bullet">-</span> <span class="hljs-attr">url:</span> <span class="hljs-string">http://loki:3100/loki/api/v1/push</span>  <span class="hljs-comment"># Specifies the Loki endpoint for log ingestion.</span>

 <span class="hljs-attr">scrape_configs:</span>
   <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">system</span>  <span class="hljs-comment"># Defines a scraping job for system logs.</span>
     <span class="hljs-attr">static_configs:</span>
       <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span>
           <span class="hljs-bullet">-</span> <span class="hljs-string">localhost</span>  <span class="hljs-comment"># Targets the local host for log collection.</span>
         <span class="hljs-attr">labels:</span>
           <span class="hljs-attr">job:</span> <span class="hljs-string">varlogs</span>  <span class="hljs-comment"># Labels logs for easy querying in Loki.</span>
           <span class="hljs-attr">__path__:</span> <span class="hljs-string">/var/log/*.log</span>  <span class="hljs-comment"># Scrapes all log files in /var/log directory.</span>
</code></pre>
</li>
<li><p><strong>Run it:</strong></p>
<pre><code class="lang-bash"> <span class="hljs-comment"># Starts the Loki and Promtail containers in detached mode for background operation.</span>
 docker-compose up -d
</code></pre>
</li>
</ol>
<p>✨ This brings up Loki and Promtail with minimal resources, no authentication, and logs scraping from <code>/var/log</code>.</p>
<h4 id="heading-troubleshooting-loki-setup-issues">Troubleshooting Loki Setup Issues</h4>
<p>If Loki or Promtail fails to start, one of the following may be the issue:</p>
<ol>
<li><p><strong>Container crashes</strong>: Check logs with <code>docker logs loki</code> or <code>docker logs promtail</code>. Look for errors like <em>“out of memory”</em> or <em>“port already in use.”</em></p>
<ul>
<li>Fix: Increase memory (for example, <code>docker-compose.yml</code> resource limits) or change ports (e.g., <code>3101:3100</code>).</li>
</ul>
</li>
<li><p><strong>Logs not ingested</strong>: Verify Promtail is scraping the correct path (<code>/var/log/ci/*.log</code>) using <code>docker exec promtail cat /etc/promtail/promtail-config.yaml</code></p>
<ul>
<li>Fix: Update <code>__path__</code> in <code>promtail-config.yaml</code> to match your CI/CD log directory.</li>
</ul>
</li>
<li><p><strong>Resource Constraints</strong>: Monitor resource usage with <code>docker stats</code> or <code>top</code> on the host.</p>
<ul>
<li>Fix: Ensure your machine has at least 4GB RAM and 20GB disk space, as specified in the prerequisites.</li>
</ul>
</li>
</ol>
<h3 id="heading-configuration-for-cicd-logging">Configuration for CI/CD Logging</h3>
<p>To adapt for CI/CD logs, you should:</p>
<h4 id="heading-1-configure-your-cicd-tools-to-write-logs-to-disk">1. Configure your CI/CD tools to write logs to disk:</h4>
<p>For example, GitHub Actions with a custom runner can write logs to <code>/var/log/gha/*.log</code>.</p>
<p>Update Promtail:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Configures Promtail to scrape logs from GitHub Actions runners for CI/CD observability.</span>
<span class="hljs-attr">scrape_configs:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">github_actions</span>  <span class="hljs-comment"># Defines a scraping job for GitHub Actions logs.</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">'localhost'</span>]  <span class="hljs-comment"># Targets the local host where the runner writes logs.</span>
        <span class="hljs-attr">labels:</span>
          <span class="hljs-attr">job:</span> <span class="hljs-string">gha</span>  <span class="hljs-comment"># Labels logs for identification in Loki queries.</span>
          <span class="hljs-attr">__path__:</span> <span class="hljs-string">/var/log/gha/*.log</span>  <span class="hljs-comment"># Scrapes logs from the specified directory.</span>
</code></pre>
<h4 id="heading-2-use-structured-logging-json">2. Use structured logging (JSON):</h4>
<p>Make sure your CI/CD tools or scripts output logs in structured format:</p>
<p>Example:</p>
<pre><code class="lang-json"># Example of a structured JSON log for CI/CD pipelines, enabling easy parsing and querying.
{
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2025-05-10T13:00:00Z"</span>,  # UTC timestamp for log entry.
  <span class="hljs-attr">"level"</span>: <span class="hljs-string">"error"</span>,  # Log level to indicate severity.
  <span class="hljs-attr">"job"</span>: <span class="hljs-string">"deploy"</span>,  # Identifies the CI/CD job (e.g., deploy stage).
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Image pull failed"</span>  # Descriptive message for the error.
}
</code></pre>
<p>This helps when querying with LogQL.</p>
<h3 id="heading-how-to-connect-ci-agents-to-loki">How to Connect CI Agents to Loki</h3>
<p>This section explains three different ways to get your CI pipeline logs into Loki for monitoring and analysis:</p>
<h4 id="heading-option-1-local-setup">Option 1 – Local setup:</h4>
<p>Your CI agents write log files to disk, and Promtail (running on the same machine) reads those files and sends them to Loki.</p>
<h4 id="heading-option-2-using-docker-logging-driver-docker-containers">Option 2 – Using Docker logging driver (Docker containers):</h4>
<p>If your CI agents run in Docker containers, you install a special Loki plugin that automatically captures all container output and sends it directly to Loki without needing separate log files.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Installs the Loki Docker logging driver to send container logs directly to Loki.</span>
docker plugin install grafana/loki-docker-driver:latest --<span class="hljs-built_in">alias</span> loki --grant-all-permissions
</code></pre>
<p>Then run your agent container:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Runs a CI agent container with the Loki logging driver to forward logs.</span>
docker run --log-driver=loki \
  --log-opt loki-url=<span class="hljs-string">"http://&lt;your-loki-host&gt;:3100/loki/api/v1/push"</span> \
  my-ci-agent-image
</code></pre>
<h4 id="heading-option-3-remote-setup">Option 3 – Remote setup:</h4>
<p>If you can't install Promtail locally, you can use a log forwarding tool like <a target="_blank" href="https://fluentbit.io/">Fluent Bit</a> or <a target="_blank" href="https://vector.dev/">Vector</a> to collect logs and push them to Loki over the network.</p>
<p><strong>The goal:</strong> Regardless of which option you choose, you’ll end up with all your CI pipeline logs centralized in Loki, where you can search through them, create dashboards in Grafana, and set up alerts when things go wrong.</p>
<p>It essentially gives you flexibility to integrate log collection based on your infrastructure setup – whether you prefer local agents, Docker plugins, or remote forwarding.</p>
<h2 id="heading-how-to-implement-an-elk-stack-alternative-for-pipeline-observability">How to Implement an ELK Stack Alternative for Pipeline Observability</h2>
<p>When full ELK (Elasticsearch, Logstash, Kibana) is too heavy for your infrastructure, you can go with lightweight setups that achieve similar observability at a lower cost and resource usage.</p>
<h3 id="heading-how-to-install-lightweight-versions-of-elasticsearch-logstash-and-kibana">How to Install Lightweight Versions of Elasticsearch, Logstash, and Kibana</h3>
<p>Goal: Stand up a minimal yet functional ELK stack for debugging CI/CD pipelines.</p>
<h4 id="heading-1-use-docker-to-spin-up-lightweight-containers">1. Use Docker to spin up lightweight containers</h4>
<p>Create a <code>docker-compose.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Defines a Docker Compose setup for a lightweight ELK stack to aggregate and visualize CI/CD logs.</span>
<span class="hljs-attr">version:</span> <span class="hljs-string">'3.7'</span>

<span class="hljs-attr">services:</span>
  <span class="hljs-attr">elasticsearch:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">docker.elastic.co/elasticsearch/elasticsearch:7.17.0</span>  <span class="hljs-comment"># Uses Elasticsearch 7.17.0.</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">elasticsearch</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">discovery.type=single-node</span>  <span class="hljs-comment"># Runs Elasticsearch in single-node mode for simplicity.</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">xpack.security.enabled=false</span>  <span class="hljs-comment"># Disables security features for lightweight setup.</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"9200:9200"</span>  <span class="hljs-comment"># Exposes Elasticsearch’s HTTP API port.</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">esdata:/usr/share/elasticsearch/data</span>  <span class="hljs-comment"># Persists Elasticsearch data.</span>

  <span class="hljs-attr">logstash:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">docker.elastic.co/logstash/logstash:7.17.0</span>  <span class="hljs-comment"># Uses Logstash 7.17.0.</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">logstash</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5044:5044"</span>  <span class="hljs-comment"># Port for receiving logs from Beats.</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"9600:9600"</span>  <span class="hljs-comment"># Port for Logstash monitoring.</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">./logstash.conf:/usr/share/logstash/pipeline/logstash.conf</span>  <span class="hljs-comment"># Mounts Logstash config file.</span>

  <span class="hljs-attr">kibana:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">docker.elastic.co/kibana/kibana:7.17.0</span>  <span class="hljs-comment"># Uses Kibana 7.17.0 for visualization.</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">kibana</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">ELASTICSEARCH_HOSTS=http://elasticsearch:9200</span>  <span class="hljs-comment"># Links Kibana to Elasticsearch.</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"5601:5601"</span>  <span class="hljs-comment"># Exposes Kibana’s web UI port.</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">esdata:</span>  <span class="hljs-comment"># Defines a volume for persisting Elasticsearch data.</span>
</code></pre>
<h4 id="heading-2-minimal-logstash-pipeline-configuration-logstashconf">2. Minimal Logstash pipeline configuration (logstash.conf)</h4>
<pre><code class="lang-javascript"><span class="hljs-comment">// Configures Logstash to process and forward CI/CD logs to Elasticsearch.</span>
input {
  beats {
    <span class="hljs-function"><span class="hljs-params">port</span> =&gt;</span> <span class="hljs-number">5044</span>  <span class="hljs-comment">// Listens for logs from Filebeat on port 5044.</span>
  }
}

filter {
  json {
    <span class="hljs-function"><span class="hljs-params">source</span> =&gt;</span> <span class="hljs-string">"message"</span>  <span class="hljs-comment">// Parses JSON-formatted log messages for structured data.</span>
  }
}

output {
  elasticsearch {
    <span class="hljs-function"><span class="hljs-params">hosts</span> =&gt;</span> [<span class="hljs-string">"http://elasticsearch:9200"</span>]  <span class="hljs-comment">// Sends processed logs to Elasticsearch.</span>
    index =&gt; <span class="hljs-string">"ci-logs-%{+YYYY.MM.dd}"</span>  <span class="hljs-comment">// Stores logs in daily indexes (e.g., ci-logs-2025.05.14).</span>
  }
}
</code></pre>
<h4 id="heading-troubleshooting-elk-setup-issues">Troubleshooting ELK Setup Issues</h4>
<p>If Elasticsearch, Logstash, or Kibana fails to start, one of the following might be the issue:</p>
<ol>
<li><p><strong>Container crashes</strong>: Check logs with <code>docker logs elasticsearch</code>, <code>docker logs logstash</code>, or <code>docker logs kibana</code>. Look for errors like <em>“insufficient disk space”</em> or <em>“port conflict”</em> (for example, 9200, 5601).</p>
<ul>
<li>Fix: Free up disk space (ensure at least 20GB available) or change ports in <code>docker-compose.yml</code> (for example, <code>9201:9200</code>).</li>
</ul>
</li>
<li><p><strong>Logs not ingested</strong>: Verify Logstash is receiving data from Filebeat or Vector using <code>docker logs logstash</code>. Check the <code>logstash.conf</code> input port (for example, 5044).</p>
<ul>
<li>Fix: Ensure Filebeat or Vector is configured to send to the correct Logstash endpoint (e.g., <code>localhost:5044</code>) and update if needed.</li>
</ul>
</li>
<li><p><strong>Resource constraints</strong>: Monitor resource usage with Docker stats or top on the host.</p>
<ul>
<li>Fix: Allocate at least 8GB RAM and 30GB disk space, as Elasticsearch requires more resources than Loki. Adjust memory limits in <code>docker-compose.yml</code> if necessary.</li>
</ul>
</li>
</ol>
<h3 id="heading-how-to-configure-log-shippers-for-different-cicd-components">How to Configure Log Shippers for Different CI/CD Components</h3>
<p>Goal: Get logs from your pipeline into Logstash or Elasticsearch.</p>
<h4 id="heading-option-1-use-filebeat-lightweight-log-shipper">Option 1: Use Filebeat (lightweight log shipper)</h4>
<p>Install <a target="_blank" href="https://www.elastic.co/beats/filebeat">Filebeat</a> on your CI/CD hosts (GitHub runner, Jenkins node, GitLab runner, and so on).</p>
<p>Filebeat config snippet (filebeat.yml):</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Configures Filebeat to collect CI/CD logs and forward them to Logstash.</span>
<span class="hljs-attr">filebeat.inputs:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">log</span>  <span class="hljs-comment"># Specifies log file input.</span>
    <span class="hljs-attr">enabled:</span> <span class="hljs-literal">true</span>  <span class="hljs-comment"># Enables the input.</span>
    <span class="hljs-attr">paths:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">/var/log/ci/*.log</span>  <span class="hljs-comment"># Scrapes logs from the specified CI log directory.</span>

<span class="hljs-attr">output.logstash:</span>
  <span class="hljs-attr">hosts:</span> [<span class="hljs-string">"localhost:5044"</span>]  <span class="hljs-comment"># Forwards logs to Logstash on port 5044.</span>
</code></pre>
<p>Then run:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Runs Filebeat with the specified configuration file for log collection.</span>
filebeat -e -c filebeat.yml
</code></pre>
<h4 id="heading-option-2-use-vectordev-as-a-more-resource-efficient-alternative-to-filebeat">Option 2: Use Vector.dev as a more resource-efficient alternative to Filebeat</h4>
<p>Vector configuration (vector.toml):</p>
<pre><code class="lang-toml"><span class="hljs-comment"># Configures Vector to collect, parse, and forward CI/CD logs to Elasticsearch efficiently.</span>
<span class="hljs-section">[sources.ci_logs]</span>
  <span class="hljs-attr">type</span> = <span class="hljs-string">"file"</span>  <span class="hljs-comment"># Specifies file-based log collection.</span>
  <span class="hljs-attr">include</span> = [<span class="hljs-string">"/var/log/ci/*.log"</span>]  <span class="hljs-comment"># Targets CI log files.</span>

<span class="hljs-section">[transforms.json_parser]</span>
  <span class="hljs-attr">type</span> = <span class="hljs-string">"remap"</span>  <span class="hljs-comment"># Uses remap transform to parse logs.</span>
  <span class="hljs-attr">inputs</span> = [<span class="hljs-string">"ci_logs"</span>]  <span class="hljs-comment"># Processes logs from the ci_logs source.</span>
  <span class="hljs-attr">source</span> = <span class="hljs-string">'''
  . = parse_json!(.message)  # Parses JSON log messages into structured data.
  '''</span>

<span class="hljs-section">[sinks.to_elasticsearch]</span>
  <span class="hljs-attr">type</span> = <span class="hljs-string">"elasticsearch"</span>  <span class="hljs-comment"># Sends logs to Elasticsearch.</span>
  <span class="hljs-attr">inputs</span> = [<span class="hljs-string">"json_parser"</span>]  <span class="hljs-comment"># Uses parsed logs from the json_parser transform.</span>
  <span class="hljs-attr">endpoint</span> = <span class="hljs-string">"http://localhost:9200"</span>  <span class="hljs-comment"># Specifies the Elasticsearch endpoint.</span>
  <span class="hljs-attr">index</span> = <span class="hljs-string">"ci-logs"</span>  <span class="hljs-comment"># Stores logs in the ci-logs index.</span>
</code></pre>
<p>Run:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Runs Vector with the specified configuration file for log processing.</span>
vector -c vector.toml
</code></pre>
<h3 id="heading-how-to-set-up-index-patterns-and-basic-visualizations">How to Set Up Index Patterns and Basic Visualizations</h3>
<p>Goal: Make CI/CD logs queryable and visual in Kibana.</p>
<h4 id="heading-1-open-kibana-httplocalhost5601httplocalhost5601">1. Open Kibana (<a target="_blank" href="http://localhost:5601/">http://localhost:5601</a>)</h4>
<ul>
<li><p>Go to <strong>Stack Management → Index Patterns</strong></p>
</li>
<li><p>Create a new pattern: <code>ci-logs-*</code></p>
</li>
<li><p>Choose a time field like <code>@timestamp</code></p>
</li>
</ul>
<h4 id="heading-2-visualizations-for-common-cicd-use-cases">2. Visualizations for Common CI/CD Use Cases</h4>
<ul>
<li><p><strong>Bar charts</strong>: Number of failed vs passed builds per day</p>
</li>
<li><p><strong>Pie chart</strong>: Top error types or most frequent failing test names</p>
</li>
<li><p><strong>Line chart</strong>: Duration of builds over time (if duration is logged)</p>
</li>
</ul>
<h4 id="heading-3-saved-searches-amp-dashboards">3. Saved Searches &amp; Dashboards</h4>
<p>You can save a search like this:</p>
<pre><code class="lang-javascript">message: <span class="hljs-string">"error"</span> AND job_name: <span class="hljs-string">"build"</span>
</code></pre>
<p>You can also combine visualizations into a CI/CD Health Dashboard.</p>
<h2 id="heading-how-to-create-a-unified-logging-strategy-across-pipeline-components">How to Create a Unified Logging Strategy Across Pipeline Components</h2>
<p>Creating a unified logging strategy across your CI/CD pipeline components ensures that logs are consistent, traceable, and easy to correlate. This helps you quickly debug issues, monitor system health, and trace requests across different tools and services. Let’s discuss some key practices for achieving a unified logging strategy:</p>
<h3 id="heading-implementing-consistent-log-formats-across-different-tools">Implementing Consistent Log Formats Across Different Tools</h3>
<p>Consistent log formats are important for various reasons. First of all, a standardized log format enables easier querying, searching, and visualization. It also helps with correlation of logs from different services. And consistency also ensures that all logs provide necessary details like timestamp, log level, and request context.</p>
<p>There are also some best practices you should follow when formatting logs:</p>
<p><strong>JSON Format</strong> is highly recommended as it’s structured, machine-readable, and compatible with many observability tools (for example, Loki, Elasticsearch, Grafana).</p>
<p>There are also some key fields you should include:</p>
<ul>
<li><p><code>timestamp</code>: The time the log entry was created (preferably in UTC).</p>
</li>
<li><p><code>log_level</code>: Indicate whether the log is an <code>INFO</code>, <code>ERROR</code>, <code>DEBUG</code>, and so on.</p>
</li>
<li><p><code>service</code>: The service or component generating the log.</p>
</li>
<li><p><code>message</code>: A concise description of the event or error.</p>
</li>
<li><p><code>correlation_id</code>: A unique identifier for requests to trace logs across systems.</p>
</li>
</ul>
<p>Here’s an example of a consistent log in JSON format:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2025-05-10T12:34:56Z"</span>,
  <span class="hljs-attr">"log_level"</span>: <span class="hljs-string">"ERROR"</span>,
  <span class="hljs-attr">"service"</span>: <span class="hljs-string">"ci_cd_pipeline"</span>,
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Build failed due to missing dependency"</span>,
  <span class="hljs-attr">"correlation_id"</span>: <span class="hljs-string">"1234567890abcdef"</span>
}
</code></pre>
<h3 id="heading-how-to-set-up-log-forwarding-from-github-actions-jenkins-or-gitlab">How to Set Up Log Forwarding from GitHub Actions, Jenkins, or GitLab</h3>
<p>Log forwarding refers to shipping logs from your CI/CD pipelines to a central spot for easy tracking. It’s helpful because it lets you spot issues fast and debug without digging through scattered files.</p>
<p>For GitHub Actions, you can configure workflows to write logs to a file or send them directly to a log aggregation tool like Loki. In Jenkins, you can use pipeline scripts to forward logs to a log server or file system. Similarly, for GitHub CI, you can add scripts in <code>.gitlab-ci.yml</code> to forward logs to a centralized endpoint.</p>
<p><strong>Using Actions for Outputting Logs:</strong><br>You can store logs in files and then forward them to a logging system (like Loki or Elasticsearch).<br>Here’s an example in a GitHub Action workflow:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Defines a GitHub Actions workflow to run tests and forward logs for observability.</span>
<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>  <span class="hljs-comment"># Uses an Ubuntu runner.</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">repository</span>  <span class="hljs-comment"># Checks out the repository code.</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">tests</span> <span class="hljs-string">and</span> <span class="hljs-string">log</span> <span class="hljs-string">output</span>  <span class="hljs-comment"># Runs tests and saves output to a log file.</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          echo "Starting tests..."
          npm test | tee test.log  # Captures test output to test.log.
          # Forwards the log file to a Loki endpoint via HTTP POST.
          curl -X POST -F 'file=@test.log' http://your-loki-endpoint</span>
</code></pre>
<p><strong>Log Forwarding with Promtail:</strong><br>If you are using Grafana Loki for log aggregation, set up Promtail to scrape the logs from the GitHub Actions runner.</p>
<h4 id="heading-jenkins">Jenkins:</h4>
<p>Jenkins logs can be forwarded to external systems (like Elasticsearch or Loki) by using log shippers or plugins.</p>
<p><strong>You can use the Logstash Plugin</strong> to forward Jenkins logs to an ELK stack or other systems:</p>
<ul>
<li><p>Install the Logstash plugin on Jenkins.</p>
</li>
<li><p>Configure the plugin to forward logs to an Elasticsearch server or a logging system of choice.</p>
</li>
<li><p>In Jenkins, add log forwarding configurations:</p>
</li>
</ul>
<pre><code class="lang-javascript">pipeline {
  agent any
  stages {
    stage(<span class="hljs-string">'Build'</span>) {
      steps {
        script {
          <span class="hljs-comment">// Example of forwarding logs to a log server</span>
          sh <span class="hljs-string">'echo "Build successful" | curl -X POST -d @- http://your-log-server'</span>
        }
      }
    }
  }
}
</code></pre>
<p><strong>Forward to Loki:</strong><br>Jenkins supports the <code>loki</code> logging driver for containers if running Jenkins in Docker. You can send logs directly to Loki using this driver:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Runs a Jenkins container with the Loki logging driver to send logs directly to Loki.</span>
docker run --log-driver=loki --log-opt loki-url=http://loki:3100 jenkins/jenkins:lts
</code></pre>
<h4 id="heading-gitlab">GitLab:</h4>
<p>GitLab CI allows logs to be forwarded to external systems for centralized collection and analysis.</p>
<p><strong>Use GitLab CI/CD to Output Logs</strong>:<br>Example in <code>.gitlab-ci.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Defines a GitLab CI/CD pipeline to run a build and forward logs to Loki.</span>
<span class="hljs-attr">stages:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">build</span>
<span class="hljs-attr">build:</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">echo</span> <span class="hljs-string">"Starting the build"</span> <span class="hljs-string">|</span> <span class="hljs-string">tee</span> <span class="hljs-string">build.log</span>  <span class="hljs-comment"># Saves build output to build.log.</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">curl</span> <span class="hljs-string">-X</span> <span class="hljs-string">POST</span> <span class="hljs-string">-d</span> <span class="hljs-string">@build.log</span> <span class="hljs-string">http://your-loki-endpoint</span>  <span class="hljs-comment"># Forwards the log to Loki.</span>
</code></pre>
<p><strong>GitLab Runners</strong>:<br>Configure GitLab runners to forward logs to an external service like Loki or Elasticsearch using <code>log-driver</code> settings or the <code>fluentd</code> log shipper.</p>
<h3 id="heading-how-to-add-correlation-ids-to-trace-requests-through-the-system">How to Add Correlation IDs to Trace Requests Through the System</h3>
<h4 id="heading-why-correlation-ids-are-important">Why Correlation IDs Are Important:</h4>
<p>Correlation IDs allow you to trace a single request as it travels through different services and tools, enabling end-to-end visibility and troubleshooting.</p>
<p>They are critical for debugging distributed systems, especially when different services (for example, CI tool, deployment tool, API service) are involved.</p>
<h4 id="heading-how-to-add-correlation-ids">How to Add Correlation IDs:</h4>
<p>You can use a UUID (Universally Unique Identifier) or a GUID (Globally Unique Identifier) to generate a unique ID for each request.</p>
<p>If you are using microservices or multiple services in the pipeline, just make sure that the same ID is propagated across each service.</p>
<p>Many logging libraries (for example, <code>winston</code> for Node.js, <code>log4j</code> for Java) support automatic correlation ID generation and logging.</p>
<p>Here’s an example in Node.js (using <code>winston</code>):</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Sets up Winston for structured logging with correlation IDs in a CI/CD pipeline.</span>
<span class="hljs-keyword">const</span> { createLogger, transports, format } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'winston'</span>);
<span class="hljs-keyword">const</span> { printf } = format;

<span class="hljs-comment">// Creates a logger with a custom format including correlation IDs.</span>
<span class="hljs-keyword">const</span> logger = createLogger({
  <span class="hljs-attr">format</span>: printf(<span class="hljs-function">(<span class="hljs-params">{ level, message, timestamp }</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">${timestamp}</span> [<span class="hljs-subst">${level}</span>] <span class="hljs-subst">${message}</span> correlation_id=<span class="hljs-subst">${generateCorrelationId()}</span>`</span>;
  }),
  <span class="hljs-attr">transports</span>: [
    <span class="hljs-keyword">new</span> transports.Console(),  <span class="hljs-comment">// Outputs logs to the console.</span>
  ],
});

<span class="hljs-comment">// Generates a random correlation ID for tracing requests.</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateCorrelationId</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.random().toString(<span class="hljs-number">36</span>).substring(<span class="hljs-number">2</span>, <span class="hljs-number">15</span>);
}

<span class="hljs-comment">// Logs a sample message.</span>
logger.info(<span class="hljs-string">'Pipeline execution started'</span>);
</code></pre>
<h4 id="heading-how-to-propagate-correlation-ids-between-services">How to Propagate Correlation IDs Between Services:</h4>
<p>In CI/CD tools, you can configure your pipeline to inject the correlation ID into logs. For example, in GitHub Actions, you can generate a correlation ID in the <code>env</code> section and propagate it in each job:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># Defines a GitHub Actions workflow that includes a correlation ID for log tracing.</span>
<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>  <span class="hljs-comment"># Uses an Ubuntu runner.</span>
    <span class="hljs-attr">env:</span>
      <span class="hljs-attr">CORRELATION_ID:</span> <span class="hljs-string">${{</span> <span class="hljs-string">github.run_id</span> <span class="hljs-string">}}</span>  <span class="hljs-comment"># Uses the GitHub run ID as a correlation ID.</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Checkout</span> <span class="hljs-string">repository</span>  <span class="hljs-comment"># Checks out the repository code.</span>
        <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v2</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Log</span> <span class="hljs-string">build</span> <span class="hljs-string">start</span> <span class="hljs-string">with</span> <span class="hljs-string">correlation</span> <span class="hljs-string">ID</span>  <span class="hljs-comment"># Logs the build start with the correlation ID.</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">echo</span> <span class="hljs-string">"Build started with Correlation ID: $CORRELATION_ID"</span>
</code></pre>
<h4 id="heading-include-correlation-ids-in-all-logs">Include Correlation IDs in All Logs:</h4>
<p>You’ll want to make sure that logs from all components in the pipeline (GitHub Actions, Jenkins, GitLab, deployment tools, and so on) include the correlation ID as part of the log message. This allows you to trace the logs of a single request or pipeline run across different services.</p>
<h4 id="heading-visualize-your-log-flow">Visualize Your Log Flow</h4>
<p>You can create a diagram showing how logs move from your CI/CD tool (for example, GitHub Actions) to Promtail/Vector, then to Loki/Elasticsearch, and finally to Grafana/Kibana for visualization. Use tools like <a target="_blank" href="http://Draw.io">Draw.io</a> to map your pipeline’s observability flow</p>
<h2 id="heading-how-to-query-and-analyze-logs-for-effective-troubleshooting">How to Query and Analyze Logs for Effective Troubleshooting</h2>
<p>In this section, you’ll learn how to use LogQL (Loki's query language) to cut through the noise and find the specific logs that matter. Whether you're hunting down a mysterious build failure or tracking deployment issues across multiple services, these query patterns always help.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748224707087/d348accc-0ef8-4ebb-9cb9-49995404b0ec.png" alt="Bar chart showing CI/CD build results from May 20-26, 2025. Blue bars represent successful builds ranging from 39-52 per day, while red bars show failed builds ranging from 1-9 per day. The chart demonstrates consistently high success rates with low failure rates throughout the week, with May 23 showing the highest failure count at 9 builds." class="image--center mx-auto" width="1468" height="866" loading="lazy"></p>
<p>This bar chart illustrates the CI/CD build performance from May 20 to May 26, 2025. It compares the number of successful builds (in blue) to failed builds (in pink) each day. Successful builds consistently range between 40 and 50, while failed builds peak at 10 on May 23, with other days showing 2 to 8 failures. This indicates a generally stable pipeline with occasional issues.</p>
<h3 id="heading-how-to-write-advanced-logql-queries-to-pinpoint-cicd-issues">How to Write Advanced LogQL Queries to Pinpoint CI/CD Issues</h3>
<p>LogQL is Grafana Loki's query language, designed for querying logs with a syntax similar to Prometheus’s PromQL. It enables efficient log searches and is particularly useful in troubleshooting CI/CD issues.</p>
<h4 id="heading-basic-logql-syntax">Basic LogQL Syntax:</h4>
<p><strong>1. Log Streams:</strong></p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>, level=<span class="hljs-string">"error"</span>}
</code></pre>
<p>This query retrieves logs where the <code>job</code> label is <code>ci_cd</code> and the <code>level</code> label is <code>error</code>.</p>
<p><strong>2. Log Filters:</strong></p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>} |= <span class="hljs-string">"build failed"</span>
</code></pre>
<p>The <code>|=</code> operator filters logs to include only those that contain the specified string, for example "build failed".</p>
<p><strong>3. Regular Expressions:</strong></p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>} |~ <span class="hljs-string">"error.*timeout"</span>
</code></pre>
<p>This uses the <code>|~</code> operator to filter logs using a regular expression. In this case, it finds logs that contain an "error" followed by "timeout".</p>
<h4 id="heading-advanced-logql-queries-for-cicd-issues">Advanced LogQL Queries for CI/CD Issues:</h4>
<p><strong>1. Filter Logs for Specific Build Failures:</strong></p>
<p>If your pipeline uses a specific label for build names:</p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>, build=<span class="hljs-string">"build123"</span>} |= <span class="hljs-string">"failure"</span>
</code></pre>
<p>This finds logs related to the <code>build123</code> job that contain the word "failure".</p>
<p><strong>2. Using Time Range and Grouping:</strong></p>
<p>To find error logs in the last 15 minutes:</p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>, level=<span class="hljs-string">"error"</span>} | <span class="hljs-string">"build failed"</span> | range(start=<span class="hljs-string">"15m"</span>)
</code></pre>
<p>To group logs by job and error type:</p>
<pre><code class="lang-javascript">sum by (job) (count_over_time({job=<span class="hljs-string">"ci_cd"</span>, level=<span class="hljs-string">"error"</span>}[<span class="hljs-number">5</span>m]))
</code></pre>
<p>This will return the count of error logs per job, grouped by job name, over the last 5 minutes.</p>
<h3 id="heading-how-to-create-pipeline-specific-queries-for-common-failure-patterns">How to Create Pipeline-Specific Queries for Common Failure Patterns</h3>
<h4 id="heading-common-failure-patterns-in-cicd-pipelines">Common Failure Patterns in CI/CD Pipelines:</h4>
<p><strong>1. Build Failures:</strong></p>
<p>If your CI system logs contain build errors, you can identify them with:</p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>, level=<span class="hljs-string">"error"</span>} |= <span class="hljs-string">"build failed"</span>
</code></pre>
<p>You can extend this to filter by specific steps or stages, for example, “test failed”, or “compilation error”.</p>
<p><strong>2. Test Failures:</strong></p>
<p>Logs from your test runner (for example, Jest, Mocha, JUnit) can contain specific failure messages:</p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>, stage=<span class="hljs-string">"test"</span>} |= <span class="hljs-string">"test failed"</span>
</code></pre>
<p><strong>3. Dependency Issues:</strong></p>
<p>If your pipeline is failing due to missing or conflicting dependencies, look for <code>npm</code>, <code>maven</code>, or <code>docker</code> related errors:</p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>, image=<span class="hljs-string">"node"</span>} |= <span class="hljs-string">"npm ERR!"</span>
</code></pre>
<p>Or for Maven-related issues:</p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>, image=<span class="hljs-string">"maven"</span>} |= <span class="hljs-string">"[ERROR]"</span>
</code></pre>
<p><strong>4. Resource Constraints (for example, Out of Memory):</strong></p>
<p>If you experience resource constraints, you might see logs like "OutOfMemoryError":</p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>, level=<span class="hljs-string">"error"</span>} |= <span class="hljs-string">"OutOfMemoryError"</span>
</code></pre>
<p><strong>Example of combining filters:</strong></p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>, level=<span class="hljs-string">"error"</span>} |= <span class="hljs-string">"build failed"</span> |~ <span class="hljs-string">"timeout|dependency"</span> | range(start=<span class="hljs-string">"1h"</span>)
</code></pre>
<p>This combines log filters for "build failed", matching any logs with the terms "timeout" or "dependency", from the last hour.</p>
<h3 id="heading-how-to-set-up-alert-rules-based-on-log-patterns">How to Set Up Alert Rules Based on Log Patterns</h3>
<p>Alerts help detect recurring issues proactively. They notify you when a specific pattern appears in your logs, allowing you to take quick action.</p>
<h4 id="heading-steps-for-setting-up-alerts"><strong>Steps for Setting Up Alerts:</strong></h4>
<p><strong>1. Create a Query for the Alert:</strong></p>
<p>First, define the log pattern you want to monitor. For example, an alert for build failures:</p>
<pre><code class="lang-javascript">{job=<span class="hljs-string">"ci_cd"</span>, level=<span class="hljs-string">"error"</span>} |= <span class="hljs-string">"build failed"</span>
</code></pre>
<p><strong>2. Create an Alert in Grafana:</strong></p>
<p>Follow these steps to set up Grafana alerts:</p>
<ul>
<li><p>Go to your Grafana dashboard.</p>
</li>
<li><p>Choose the panel you want to set the alert on (or create a new panel for this purpose).</p>
</li>
<li><p>In the panel, click the <strong>Alert</strong> tab.</p>
</li>
<li><p>Set the <strong>Query</strong> field to your LogQL query, such as the one above.</p>
</li>
<li><p>Under <strong>Conditions</strong>, define when the alert should trigger, e.g., if the error occurs more than <code>3</code> times within <code>5 minutes</code>.</p>
</li>
</ul>
<p><strong>3. Alert Settings:</strong></p>
<p>Now you’ll want to set up the alert evaluation interval and conditions for triggering the alert (e.g., if the query returns results above a certain threshold).</p>
<p><strong>Here’s an example:</strong> Trigger an alert if the number of errors exceeds 5 within 5 minutes:</p>
<pre><code class="lang-javascript">count_over_time({job=<span class="hljs-string">"ci_cd"</span>, level=<span class="hljs-string">"error"</span>} |= <span class="hljs-string">"build failed"</span>[<span class="hljs-number">5</span>m]) &gt; <span class="hljs-number">5</span>
</code></pre>
<p><strong>4. Set Alert Notifications:</strong></p>
<p>You can choose where you want the alert to be sent (like to Slack, email, or PagerDuty). And Grafana can be integrated with these systems to send real-time alerts to the right team members.</p>
<p><strong>Example alert query for test failures:</strong></p>
<pre><code class="lang-javascript">count_over_time({job=<span class="hljs-string">"ci_cd"</span>, stage=<span class="hljs-string">"test"</span>} |= <span class="hljs-string">"test failed"</span>[<span class="hljs-number">5</span>m]) &gt; <span class="hljs-number">3</span>
</code></pre>
<p>This query triggers an alert if more than 3 test failures are logged within the last 5 minutes.</p>
<h3 id="heading-kibana-query-language-deep-dive-for-cicd-contexts">Kibana Query Language Deep Dive for CI/CD Contexts</h3>
<p>Kibana Query Language (KQL) is a powerful tool for searching and filtering logs within Elasticsearch, and it becomes especially useful for debugging CI/CD pipelines.</p>
<h4 id="heading-basic-query-syntax">Basic Query Syntax:</h4>
<ul>
<li><p><strong>Field:</strong></p>
<pre><code class="lang-javascript">  textCopyEditfieldname:value
</code></pre>
<p>  Example: <code>status: "failure"</code></p>
</li>
<li><p><strong>Wildcard:</strong> Use <code>*</code> to match any number of characters:</p>
<pre><code class="lang-javascript">  textCopyEditmessage: <span class="hljs-string">"test*"</span>
</code></pre>
</li>
<li><p><strong>Range Queries:</strong> To search for logs within a specific time frame:</p>
<pre><code class="lang-javascript">  textCopyEdittimestamp:[<span class="hljs-number">2023</span><span class="hljs-number">-05</span><span class="hljs-number">-01</span> TO <span class="hljs-number">2023</span><span class="hljs-number">-05</span><span class="hljs-number">-15</span>]
</code></pre>
</li>
<li><p><strong>Boolean Queries:</strong> Combine queries using <code>AND</code>, <code>OR</code>, and <code>NOT</code>:</p>
<pre><code class="lang-javascript">  textCopyEditstatus: <span class="hljs-string">"failure"</span> AND build_id: <span class="hljs-string">"12345"</span>
</code></pre>
</li>
</ul>
<h4 id="heading-time-based-queries">Time-Based Queries:</h4>
<p>Since CI/CD logs are often tied to time-sensitive operations (builds, deployments), KQL allows you to filter logs by time:</p>
<pre><code class="lang-javascript">textCopyEdit@timestamp:[now<span class="hljs-number">-1</span>d TO now]
</code></pre>
<h4 id="heading-nested-queries-for-complex-pipelines">Nested Queries (For Complex Pipelines):</h4>
<p>CI/CD logs can have nested or multi-level structures (for example, logs within containers). You can query these nested fields:</p>
<pre><code class="lang-javascript">textCopyEditpipeline.logs.message: <span class="hljs-string">"build failed"</span>
</code></pre>
<h4 id="heading-aggregations-and-grouping">Aggregations and Grouping:</h4>
<p>You can aggregate logs based on certain fields to identify trends or recurring issues:</p>
<pre><code class="lang-javascript">textCopyEditterms aggregation on <span class="hljs-string">"status"</span> field
</code></pre>
<p>This helps identify the most common failure statuses in your pipeline.</p>
<h4 id="heading-field-specific-filtering">Field-Specific Filtering:</h4>
<p>When debugging specific components like a build tool or deployment step, you can filter by those component-specific fields:</p>
<pre><code class="lang-javascript">textCopyEditbuild_tool: <span class="hljs-string">"Jenkins"</span> AND status: <span class="hljs-string">"failure"</span>
</code></pre>
<h4 id="heading-creating-saved-searches-for-recurring-issues">Creating Saved Searches for Recurring Issues</h4>
<p>Once you’ve built queries that help you identify common issues in your CI/CD pipeline, you can save them in Kibana for future use.</p>
<p><strong>1. Create a Saved Search:</strong></p>
<p>Run your desired query in the Kibana Discover tab. Click on the “Save” button and give it a meaningful name, such as "Failed Builds - Last Week". You can add filters and customize the time range to match your typical issue patterns.</p>
<p><strong>2. Use Filters to Pinpoint Recurring Problems:</strong></p>
<p>Create saved searches that focus on specific recurring issues like:</p>
<ul>
<li><p>Build failures based on a specific tool or version.</p>
</li>
<li><p>Test failures within a particular module or set of tests.</p>
</li>
</ul>
<p>Example search for “flaky tests”:</p>
<pre><code class="lang-javascript">textCopyEdittest_status: <span class="hljs-string">"failed"</span> AND error_message: <span class="hljs-string">"*timeout*"</span>
</code></pre>
<p><strong>3. Saving Multiple Variations:</strong></p>
<p>You can save multiple variations of queries based on different error types or CI/CD tools:</p>
<ul>
<li><p><strong>Failed Jobs:</strong> <code>status: "failure"</code></p>
</li>
<li><p><strong>Test Failures in Build:</strong> <code>log_type: "test" AND status: "failure"</code></p>
</li>
<li><p><strong>Resource Constraints:</strong> <code>error_message: "*memory*"</code></p>
</li>
</ul>
<p>These saved searches will allow you to quickly troubleshoot specific issues that occur frequently.</p>
<h4 id="heading-building-visualizations-to-spot-patterns-over-time">Building Visualizations to Spot Patterns Over Time</h4>
<p>Once you have saved searches, Kibana allows you to create visualizations from your data, making it easier to spot trends, anomalies, or patterns over time.</p>
<p><strong>1. Create a Visualization:</strong></p>
<p>Go to the <strong>Visualize</strong> tab in Kibana. Select the appropriate visualization type. Common visualizations for debugging CI/CD pipelines include:</p>
<ul>
<li><p><strong>Line Chart:</strong> Track build failure rates over time.</p>
</li>
<li><p><strong>Bar Chart:</strong> Show the number of failures per CI tool or service.</p>
</li>
<li><p><strong>Pie Chart:</strong> Breakdown of failure reasons (for example, compilation errors, test failures, resource constraints).</p>
</li>
</ul>
<p><strong>2. Track Failure Trends Over Time:</strong></p>
<p>Create a line chart to track build failures over a given period:</p>
<ul>
<li><p><strong>X-Axis:</strong> Time (for example, daily or weekly).</p>
</li>
<li><p><strong>Y-Axis:</strong> Count of build failures.</p>
</li>
<li><p><strong>Aggregation:</strong> Date histogram with <code>@timestamp</code> field.</p>
</li>
</ul>
<p>This will help you visualize how build failures are trending, making it easier to identify recurring issues or spikes in failures.</p>
<p><strong>3. Monitor Failure Types by CI Tool:</strong></p>
<p>Create a bar chart that shows the number of failures broken down by CI tool:</p>
<ul>
<li><p><strong>X-Axis:</strong> CI tool (Jenkins, GitHub Actions, GitLab, and so on).</p>
</li>
<li><p><strong>Y-Axis:</strong> Count of failures.</p>
</li>
<li><p><strong>Aggregation:</strong> Terms aggregation on the <code>ci_tool</code> field.</p>
</li>
</ul>
<p>This visualization helps identify which CI tool is experiencing the most failures and focus troubleshooting efforts there.</p>
<p><strong>4. Visualize Error Messages by Frequency:</strong></p>
<p>You can visualize which error messages appear most frequently, helping you understand what might be causing recurring issues:</p>
<ul>
<li><p><strong>X-Axis:</strong> Error message type.</p>
</li>
<li><p><strong>Y-Axis:</strong> Count of occurrences.</p>
</li>
<li><p><strong>Aggregation:</strong> Terms aggregation on the <code>error_message</code> field.</p>
</li>
</ul>
<p><strong>5. Dashboard for Holistic Monitoring:</strong></p>
<p>Create a dashboard that brings together multiple visualizations. You can have one graph for failure trends, another for failure types (bar chart), and a pie chart showing the percentage of failures caused by different issues. This dashboard gives you a holistic view of your pipeline's health.</p>
<h4 id="heading-advanced-visualization-techniques">Advanced Visualization Techniques:</h4>
<p>There are various advanced techniques you can use to dig further into your data.</p>
<ul>
<li><p><strong>Heatmaps</strong>: Use heatmaps to spot time-based anomalies in build durations or test failures.</p>
</li>
<li><p><strong>Anomaly Detection</strong>: Kibana has built-in anomaly detection that can be applied to log data to automatically detect patterns that deviate from the norm. This is especially useful for catching rare or unexpected errors in your CI/CD pipeline.</p>
<p>  Example for anomaly detection:</p>
<pre><code class="lang-javascript">  textCopyEditfield: duration
  <span class="hljs-attr">aggregation</span>: average
  anomaly detection model: <span class="hljs-string">"baseline"</span>
</code></pre>
</li>
</ul>
<h2 id="heading-how-to-set-up-prometheus-metrics-alongside-your-logs">How to Set Up Prometheus Metrics Alongside Your Logs</h2>
<p>To fully understand your CI/CD pipeline's health and performance, combining metrics and logs is essential. Prometheus is an excellent tool for capturing time-series metrics, and it works seamlessly with Grafana and Loki (or any log aggregation system).</p>
<h3 id="heading-how-to-set-up-prometheus-for-cicd-metrics-collection"><strong>How to Set Up Prometheus for CI/CD Metrics Collection:</strong></h3>
<h4 id="heading-1-install-prometheus">1. Install Prometheus:</h4>
<p>You can install Prometheus using Docker or Kubernetes for easy deployment.</p>
<p>For Docker-based installation:</p>
<pre><code class="lang-bash">docker run -d -p 9090:9090 --name prometheus prom/prometheus
</code></pre>
<h4 id="heading-2-configure-prometheus-to-scrape-metrics"><strong>2. Configure Prometheus to Scrape Metrics:</strong></h4>
<p>Prometheus needs to be configured to scrape metrics from your CI/CD services.</p>
<p>Edit the <code>prometheus.yml</code> file:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">scrape_configs:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">job_name:</span> <span class="hljs-string">'ci_cd_metrics'</span>
    <span class="hljs-attr">static_configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">targets:</span> [<span class="hljs-string">'localhost:8080'</span>, <span class="hljs-string">'localhost:9091'</span>]
</code></pre>
<h4 id="heading-3-instrument-your-cicd-services">3. Instrument Your CI/CD Services:</h4>
<p>To expose metrics, you need to integrate Prometheus client libraries into your CI/CD services.</p>
<p>For example, to expose build metrics from a Jenkins job, use the <a target="_blank" href="https://plugins.jenkins.io/prometheus/">Prometheus plugin for Jenkins</a>. In GitHub Actions, you can use <a target="_blank" href="https://github.com/prometheus/prometheus">Prometheus</a> to expose job metrics.</p>
<h4 id="heading-4-expose-metrics-endpoint"><strong>4. Expose Metrics Endpoint:</strong></h4>
<p>You’ll want to make sure your services expose a <code>/metrics</code> endpoint that Prometheus can scrape. For example, use Prometheus client libraries in your application to expose this endpoint.</p>
<h4 id="heading-troubleshooting-prometheus-setup-issues">Troubleshooting Prometheus Setup Issues</h4>
<p>If Prometheus fails to start or scrape metrics, here are some things that might be going wrong:</p>
<ol>
<li><p><strong>Container Crashes</strong>: Check logs with <code>docker logs prometheus</code>. Look for errors like “port already in use” (for example, 9090) or configuration parsing issues.</p>
<ul>
<li>Fix: Change the port in <code>docker run</code> (for example, <code>-p 9091:9090</code>) or correct the <code>prometheus.yml</code> file syntax.</li>
</ul>
</li>
<li><p><strong>Metrics Not Scraped</strong>: Verify targets are reachable using <code>docker logs prometheus</code> or test with curl <code>http://localhost:9090/targets</code>. Check <code>prometheus.yml</code> for correct endpoints.</p>
<ul>
<li>Fix: Update <code>targets</code> in <code>scrape_configs</code> (for example, <code>localhost:8080</code>) to match your CI/CD service’s metrics endpoint.</li>
</ul>
</li>
<li><p><strong>Resource Constraints</strong>: Monitor usage with docker stats or top on the host.</p>
<ul>
<li>Fix: Ensure at least 4GB RAM and 10GB disk space. Increase storage retention or reduce scrape frequency in <code>prometheus.yml</code> if needed.</li>
</ul>
</li>
</ol>
<h2 id="heading-how-to-create-grafana-dashboards-that-combine-metrics-and-logs">How to Create Grafana Dashboards That Combine Metrics and Logs</h2>
<p>Once Prometheus is collecting metrics, the next step is to visualize and correlate them in Grafana.</p>
<h3 id="heading-how-to-integrate-prometheus-with-grafana"><strong>How to Integrate Prometheus with Grafana:</strong></h3>
<p>First, you’ll need to install Grafana. You can use Docker or Kubernetes for quick deployment:</p>
<pre><code class="lang-bash">docker run -d -p 3000:3000 --name grafana grafana/grafana
</code></pre>
<p>Next, configure Grafana to use Prometheus as a data source. To do this, log in to Grafana (<code>localhost:3000</code> by default). Go to <code>Configuration</code> &gt; <code>Data Sources</code> &gt; <code>Add Data Source</code> &gt; Choose <code>Prometheus</code>. Enter your Prometheus server URL (for example, <code>http://localhost:9090</code>) and click <code>Save &amp; Test</code>.</p>
<p>Now it’s time to build a unified dashboard. To do this, create a new dashboard in Grafana that combines both logs (Loki) and metrics (Prometheus).</p>
<p>Add a panel with Prometheus data queries to visualize pipeline metrics like build success rate, deployment duration, and failure count. Use the <code>Graph</code> visualization type for time-series data and <code>Stat</code> for quick summary metrics.</p>
<p>Finally, in the same Grafana dashboard, add panels for logs (from Loki or any other logging system). Use the <code>Logs</code> panel to visualize log data and link them with the relevant Prometheus metrics by using time-based correlations.</p>
<p><strong>Example</strong>: If a spike in CPU usage is detected (Prometheus metric), the logs panel could show related logs, like errors or failed build jobs.</p>
<h2 id="heading-how-to-use-exemplars-to-jump-from-metrics-to-relevant-logs">How to Use Exemplars to Jump from Metrics to Relevant Logs</h2>
<p>Exemplars are an advanced feature in Prometheus that allow you to connect metric data with logs and traces. Grafana supports this feature, and it can be incredibly helpful when investigating issues.</p>
<h3 id="heading-how-to-set-up-exemplars-in-prometheus">How to Set Up Exemplars in Prometheus:</h3>
<p><strong>1. Enable Exemplars in Your Application:</strong></p>
<p>Exemplars are essentially traces embedded into your metrics. To use them, you’ll need to make sure your application is instrumented to send exemplar data alongside your metrics.</p>
<p>Many libraries support adding exemplars to Prometheus metrics, such as <code>prom-client</code> (Node.js) and <code>prometheus-net</code> (C#).</p>
<p>Here’s an example in Node.js:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Demonstrates adding an exemplar to a Prometheus metric for linking to logs or traces.</span>
<span class="hljs-keyword">const</span> promClient = <span class="hljs-built_in">require</span>(<span class="hljs-string">'prom-client'</span>);

<span class="hljs-comment">// Creates a counter metric to track failed CI/CD builds.</span>
<span class="hljs-keyword">const</span> counter = <span class="hljs-keyword">new</span> promClient.Counter({
  <span class="hljs-attr">name</span>: <span class="hljs-string">'ci_cd_failed_builds_total'</span>,  <span class="hljs-comment">// Metric name for failed builds.</span>
  <span class="hljs-attr">help</span>: <span class="hljs-string">'Total number of failed builds'</span>,  <span class="hljs-comment">// Description of the metric.</span>
});

<span class="hljs-comment">// Increments the counter with an exemplar for tracing.</span>
counter.inc({ <span class="hljs-attr">exemplar</span>: <span class="hljs-string">'build_failed'</span> });
</code></pre>
<p><strong>2. Enable Exemplars in Prometheus Config:</strong></p>
<p>Make sure your Prometheus server is configured to store and expose exemplars. Exemplars are typically included with histogram or summary metrics, so make sure you’ve configured them correctly.</p>
<p><strong>3. Visualizing Exemplars in Grafana:</strong></p>
<p>In Grafana, when you query Prometheus for metrics with exemplars, Grafana will show the linked logs or traces when you hover over a metric.</p>
<p>Use the <code>Exemplar</code> option in Grafana panels to quickly access logs from specific metrics.</p>
<p>For example, if you have a <code>build_failure_total</code> metric and you detect a failure in your pipeline, you can click on the failure metric in Grafana and instantly view the relevant logs for that specific failure using the exemplars.</p>
<h2 id="heading-how-to-diagnose-and-fix-common-cicd-problems">How to Diagnose and Fix Common CI/CD Problems</h2>
<p>CI/CD pipelines often encounter issues like build failures, dependency problems, and flaky tests that can disrupt development workflows. This section provides practical strategies to diagnose and resolve these common problems using log analysis and systematic debugging techniques, helping you restore pipeline stability quickly.</p>
<h3 id="heading-strategy-1-systematically-debug-build-failures"><strong>Strategy 1: Systematically Debug Build Failures</strong></h3>
<p>Build failures are a frequent CI/CD challenge, often stemming from errors in code, tests, or configurations. Systematically debugging these issues involves analyzing logs to pinpoint root causes, using the following approaches.</p>
<h4 id="heading-identifying-patterns-in-compiler-and-test-output">Identifying Patterns in Compiler and Test Output</h4>
<p>When debugging build failures, you need to first examine the logs from the compiler and test outputs. Let’s go over some key strategies.</p>
<h4 id="heading-1-check-for-specific-error-messages">1. Check for Specific Error Messages:</h4>
<p>There are a few common types of error messages you might get. They are:</p>
<ul>
<li><p><strong>Syntax errors</strong>: Look for lines indicating that there's a mismatch in syntax, such as missing semicolons, undeclared variables, or incorrect function calls.</p>
</li>
<li><p><strong>Linker errors</strong>: These often occur when the required libraries or dependencies are not found. You'll typically see errors like <code>undefined reference</code> or <code>symbol not found</code>.</p>
</li>
<li><p><strong>Build tool errors</strong>: If you are using build systems like Maven, Gradle, or MSBuild, their logs will give specific error codes or missing configurations.</p>
</li>
</ul>
<h4 id="heading-2-look-for-common-error-patterns">2. Look for Common Error Patterns:</h4>
<p>Often, failed builds repeat the same error or pattern across multiple runs. Check logs for recurring terms or errors that point to specific modules or functions. And remember that grouping similar issues can help you identify the root cause faster.</p>
<h4 id="heading-3-use-regular-expressions-for-log-filtering">3. Use Regular Expressions for Log Filtering:</h4>
<p>You can use regular expressions to search for keywords in the logs that match common failure patterns (for example, "error", "failed", "exception", "out of memory"). This will help you filter out unrelated messages and focus on the failures.</p>
<p><strong>As an example:</strong></p>
<ul>
<li><p>If the build fails with an "Out of Memory" error, search for any memory allocation issues or settings that can be increased.</p>
</li>
<li><p>If test failures are related to specific modules, inspect those modules for recent changes or dependency issues.</p>
</li>
</ul>
<h3 id="heading-strategy-2-troubleshooting-dependency-issues-with-log-analysis">Strategy 2: Troubleshooting Dependency Issues with Log Analysis</h3>
<p>Dependency issues are common in build failures, especially in complex CI/CD pipelines with multiple modules or services. To resolve these issues, consider the following:</p>
<p><strong>1. Check for Missing or Outdated Dependencies</strong>:</p>
<p>Start by reviewing the build tool’s output to check for messages related to missing dependencies (for example, <code>dependency not found</code>, <code>version conflict</code>).</p>
<p>Many build tools (like Maven, npm, or .NET) will include specific error messages when a dependency is missing or incompatible.</p>
<p><strong>2. Inspect Dependency Resolution Logs</strong>:</p>
<p>Some build tools provide detailed logs showing how dependencies were resolved (for example, the version of a library that was used). These logs can show you if there’s a version mismatch.</p>
<p>Make sure that your <code>package.json</code> (for JavaScript projects), <code>pom.xml</code> (for Java), or <code>csproj</code> (for C#) files are correctly defined with compatible versions.</p>
<p><strong>3. Verify Network Connectivity</strong>:</p>
<p>CI/CD tools sometimes fail to fetch dependencies due to network issues (for example, proxy settings, repository access). Look for any errors indicating that a repository couldn’t be reached.</p>
<p><strong>4. Log Example:</strong></p>
<p>If a Java project fails with <code>Could not find artifact</code>, it's likely a dependency missing or inaccessible. Check the repository URL or if the artifact exists in your Maven repo.</p>
<p><strong>5. Resolve Version Conflicts</strong>:</p>
<p>Version conflicts occur when different dependencies require incompatible versions of the same library. This is especially true in Java (with Maven/Gradle) and .NET projects. Consider using tools to resolve version conflicts automatically or define compatible versions manually.</p>
<h3 id="heading-fixing-flaky-tests-based-on-historical-log-data">Fixing Flaky Tests Based on Historical Log Data</h3>
<p><strong>Note:</strong> Issues like container crashes, logs not ingested, or resource constraints here may resemble those in other sections. These are common across CI/CD services and processes, but each section offers unique context to avoid redundancy.</p>
<p>Flaky tests – that is, those that pass sometimes and fail at other times – are common in CI/CD pipelines, and they can be frustrating. Let’s discuss some strategies for how you can tackle them:</p>
<p><strong>1. Analyze Test Logs Over Time</strong>:</p>
<p>Review historical logs to identify patterns in when the test fails. Look for timing issues, resource limits, or external dependencies that could affect test reliability.</p>
<p>For example, if a test intermittently fails after a certain amount of time or only during specific pipeline stages, it could indicate resource exhaustion or race conditions.</p>
<p><strong>2. Check Test Dependencies</strong>:</p>
<p>Often, flaky tests are dependent on external services or resources (for example, databases, APIs, file systems). Check if these services are consistently available and properly mocked during test execution.</p>
<p>Logs that mention failed connections to external services or unstable environments can give you insights into potential issues with dependencies.</p>
<p><strong>3. Run Tests with Increased Logging</strong>:</p>
<p>Increase the verbosity of test logs to capture more information about the failures. This can help you detect why tests fail in certain conditions.</p>
<p>For example, adding debug logs inside tests can provide more context on the state of the application when the failure occurs.</p>
<p><strong>4. Time of Day Issues</strong>:</p>
<p>Some flaky tests may fail during peak usage times, especially if they rely on shared resources. Look for patterns that correlate with resource contention (for example, database locks, API rate limits).</p>
<p>Logs showing high CPU or memory usage can indicate that resource constraints are affecting the stability of your tests.</p>
<p><strong>5. Implement Retry Logic for Flaky Tests</strong>:</p>
<p>To mitigate the effects of flaky tests, implement automatic retries for tests that fail intermittently. This can help reduce the noise in your CI/CD pipeline while you investigate the root causes.</p>
<p>For example, if a database connection test fails intermittently, you may want to inspect database logs for signs of timeouts or connection pool exhaustion.</p>
<h3 id="heading-how-to-resolve-deployment-pipeline-failures">How to Resolve Deployment Pipeline Failures</h3>
<p>Deployment pipeline failures can stem from several sources, and diagnosing them requires a systematic approach using logs and available observability tools. Below, we will outline the common patterns in logs that indicate resource constraints, permission/authentication issues, and configuration drift between environments.</p>
<p><strong>Log Patterns That Indicate Resource Constraints</strong></p>
<p>Resource constraints are a common cause of pipeline failures. These can include CPU limits, memory usage, or disk space running out. Here's how to recognize these patterns:</p>
<h4 id="heading-key-indicators-in-logs">Key Indicators in Logs:</h4>
<ul>
<li><strong>Memory Issues</strong>: Look for messages like <em>"out of memory"</em>, <em>"memory limit exceeded"</em>, or <em>"OOM killed"</em> in your logs. Here’s an example in Kubernetes logs:</li>
</ul>
<pre><code class="lang-javascript">pod has been OOMKilled
</code></pre>
<ul>
<li><strong>CPU Limits</strong>: Watch for logs showing that a process exceeded CPU limits or was throttled. Here’s an example:</li>
</ul>
<pre><code class="lang-javascript">process <span class="hljs-string">'foo'</span> hit CPU limit, throttling at <span class="hljs-number">100</span>%
</code></pre>
<ul>
<li><strong>Disk Space</strong>: Logs may show file write errors or messages about a disk being full. Here’s an example:</li>
</ul>
<pre><code class="lang-javascript">Unable to write to file, disk space is full.
</code></pre>
<p>You can resolve the memory issues by increasing the allocated memory for your containers, VM, or cloud instances.</p>
<p>You can resolve the CPU issues by adjusting CPU limits or scaling your infrastructure to add more resources.</p>
<p>And finally, you can resolve disk space issues by cleaning up unused files or increasing disk capacity on the server/container.</p>
<p><strong>Identify Permission and Authentication Issues</strong></p>
<p>Permission and authentication issues often result in pipeline failures due to a lack of access to necessary resources or services. These issues might occur when you’re trying to access databases, deploy to cloud services, or authenticate third-party APIs.</p>
<p>There are some key indicators in the logs that you can look out for:</p>
<h4 id="heading-1-authentication-failures">1. Authentication Failures:</h4>
<p>Look for messages related to failed logins, incorrect credentials, or invalid tokens.</p>
<p>Here’s an example:</p>
<pre><code class="lang-javascript">Authentication failed <span class="hljs-keyword">for</span> user <span class="hljs-string">'admin'</span>
</code></pre>
<pre><code class="lang-javascript">Invalid API token provided.
</code></pre>
<h4 id="heading-2-permission-denied">2. Permission Denied:</h4>
<p>Logs may indicate that the CI/CD pipeline lacks the permissions to perform a certain action.</p>
<p>Here’s an example:</p>
<pre><code class="lang-javascript">Access denied <span class="hljs-keyword">for</span> /path/to/deployment/target
</code></pre>
<pre><code class="lang-javascript">Unauthorized request to cloud service.
</code></pre>
<p><strong>How to resolve these errors</strong>:</p>
<ul>
<li><p><strong>Credentials</strong>: Ensure the credentials (API keys, access tokens, SSH keys) used in the pipeline are up-to-date and correctly configured.</p>
</li>
<li><p><strong>Permissions</strong>: Review and update the role-based access control (RBAC) settings for the service account running the pipeline to ensure it has the necessary permissions.</p>
</li>
<li><p><strong>Secrets Management</strong>: Use tools like Vault, AWS Secrets Manager, or Azure Key Vault to securely manage secrets and credentials.</p>
</li>
</ul>
<p><strong>Troubleshooting Configuration Drift Between Environments</strong></p>
<p>Configuration drift occurs when different environments (like development, staging, production) are not synchronized. This can lead to inconsistent behavior during deployments, and often results in failures in one environment but not in others.</p>
<p>Look out for these key indicators in the logs:</p>
<h4 id="heading-1-mismatch-in-environment-variables">1. Mismatch in Environment Variables:</h4>
<p>If you’re using environment variables, check for discrepancies across different stages. For example:</p>
<pre><code class="lang-javascript">Environment variable DATABASE_URL not found <span class="hljs-keyword">in</span> production
</code></pre>
<h4 id="heading-2-dependency-versions">2. Dependency Versions:</h4>
<p>Mismatched versions of dependencies between environments can cause unexpected issues.</p>
<p>Here’s an example:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">Error</span>: Dependency <span class="hljs-string">'libxyz'</span> version mismatch between environments
</code></pre>
<h4 id="heading-3-service-configuration">3. Service Configuration:</h4>
<p>Look for configuration-related errors that might not be present in a development environment but occur in production.</p>
<p>Here’s an example:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">Error</span>: Invalid config <span class="hljs-keyword">in</span> <span class="hljs-string">'production-config.yaml'</span>
</code></pre>
<p><strong>How to resolve these errors</strong>:</p>
<ul>
<li><p><strong>Use Infrastructure as Code (IaC)</strong>: Tools like Terraform, Ansible, or CloudFormation can help ensure that environments are provisioned consistently.</p>
</li>
<li><p><strong>Automated Configuration Management</strong>: Use CI/CD pipeline steps to automate environment setup to avoid manual changes that can cause drift.</p>
</li>
<li><p><strong>Environment Consistency Checks</strong>: Implement checks to compare configurations and dependencies across environments before deployment.</p>
<ul>
<li>Example: You can add a pre-deployment stage to run a script that compares environment variables, configurations, and dependency versions between staging and production.</li>
</ul>
</li>
<li><p><strong>Configuration Management Tools</strong>: Use configuration management tools like Chef, Puppet, or SaltStack to maintain consistent configurations across environments.</p>
</li>
</ul>
<h3 id="heading-how-to-debug-container-based-deployment-issues">How to Debug Container-Based Deployment Issues</h3>
<p>Debugging container-based deployment issues requires specialized tools and techniques to trace errors in containerized environments. Below are strategies to efficiently collect logs, diagnose failures, and use ephemeral containers for investigation.</p>
<h4 id="heading-collecting-and-analyzing-container-logs-effectively">Collecting and Analyzing Container Logs Effectively</h4>
<p>Container logs are essential for troubleshooting issues, and effective collection and analysis can significantly speed up the debugging process.</p>
<p>Here’s how you can collect container logs:</p>
<p><strong>1. Docker Logs:</strong></p>
<p>You can use Docker’s <code>logs</code> command to view logs of a specific container:</p>
<pre><code class="lang-bash">docker logs &lt;container_name_or_id&gt;
</code></pre>
<p>If your container uses a logging driver (like <code>json-file</code> or <code>fluentd</code>), ensure that logs are being written to an accessible location.</p>
<p><strong>2. Kubernetes Logs:</strong></p>
<p>For Kubernetes-managed containers, use <code>kubectl</code> to access pod logs:</p>
<pre><code class="lang-bash">kubectl logs &lt;pod_name&gt;
</code></pre>
<p>To view logs for all containers in a pod:</p>
<pre><code class="lang-bash">kubectl logs &lt;pod_name&gt; --all-containers=<span class="hljs-literal">true</span>
</code></pre>
<p><strong>3. Log Aggregation:</strong></p>
<p>You can integrate with centralized logging systems (like, <strong>Grafana Loki</strong>, <strong>Elastic Stack</strong>). You can also use Fluentd or Logstash as log shippers for forwarding logs from containers to a logging backend.</p>
<h4 id="heading-analyzing-logs">Analyzing Logs:</h4>
<p><strong>1. Filter and Search Logs:</strong></p>
<p>Use <code>grep</code> to filter logs for specific error messages or patterns:</p>
<pre><code class="lang-bash">docker logs &lt;container_name&gt; | grep <span class="hljs-string">"ERROR"</span>
</code></pre>
<p>In Kubernetes, you can combine <code>kubectl</code> with <code>grep</code> or other tools for advanced filtering.</p>
<p><strong>2. Log Contextualization:</strong></p>
<p>Include metadata in your logs (for example, container ID, environment, timestamps) for easier debugging. Ensure logs are structured in formats like JSON to allow for better querying and filtering.</p>
<h3 id="heading-how-to-diagnose-image-pull-and-networking-failures">How to Diagnose Image Pull and Networking Failures</h3>
<p>Container deployment failures often stem from issues related to image pulling or network connectivity. Here’s how to troubleshoot these problems:</p>
<h4 id="heading-image-pull-failures">Image Pull Failures:</h4>
<p>There are some common issues you might see, such as:</p>
<ul>
<li><p><strong>Authentication failures:</strong> If the container registry requires authentication, ensure your credentials (username/password or tokens) are correct.</p>
</li>
<li><p><strong>Network connectivity:</strong> Check if the container can access the registry endpoint. Often, firewalls or DNS issues block the image pull.</p>
</li>
<li><p><strong>Image not found:</strong> Verify the image name and tag are correct. Use <code>docker pull</code> to manually pull the image to see if the issue is specific to the deployment process.</p>
</li>
</ul>
<p>There are various ways to diagnose them:</p>
<p>For <strong>Docker</strong>, use:</p>
<pre><code class="lang-bash">docker pull &lt;image_name&gt;
</code></pre>
<p>This will output the specific error message if the image pull fails.</p>
<p>For <strong>Kubernetes</strong>, check the event logs for the pod:</p>
<pre><code class="lang-bash">kubectl describe pod &lt;pod_name&gt;
</code></pre>
<p>Look for the <code>Failed</code> status under "Events" for information about why the image pull failed (for example, wrong credentials or tag). If the issue is with the registry authentication, configure the Kubernetes <strong>imagePullSecrets</strong> or Docker's credentials to ensure the correct access.</p>
<h4 id="heading-networking-failures">Networking Failures:</h4>
<p>Some common issues you may encounter are:</p>
<ul>
<li><p><strong>DNS resolution problems:</strong> Containers may fail to resolve hostnames if DNS configurations are incorrect.</p>
</li>
<li><p><strong>Network policies and firewall rules:</strong> Network policies or firewalls may block necessary ports.</p>
</li>
<li><p><strong>Inter-container communication:</strong> If containers need to talk to each other, ensure they’re on the same network or subnet.</p>
</li>
</ul>
<p>Again, there are various ways to diagnose these issues:</p>
<p><strong>For Docker networking:</strong></p>
<p>You can do this to view all Docker networks:</p>
<pre><code class="lang-bash">docker network ls
</code></pre>
<p>You can also inspect the network of your container like this:</p>
<pre><code class="lang-bash">docker network inspect &lt;network_name&gt;
</code></pre>
<p>Check if the container is correctly attached to the network and if necessary ports are exposed.</p>
<p><strong>For Kubernetes Networking:</strong></p>
<p>You can use <code>kubectl</code> to check network policies:</p>
<pre><code class="lang-bash">kubectl get networkpolicies
</code></pre>
<p>You can also check the pod’s network settings like this:</p>
<pre><code class="lang-bash">kubectl describe pod &lt;pod_name&gt; | grep -i <span class="hljs-string">"Network"</span>
</code></pre>
<p><strong>Testing Connectivity Inside Containers:</strong></p>
<p>For Docker, exec into the container and test:</p>
<pre><code class="lang-bash">docker <span class="hljs-built_in">exec</span> -it &lt;container_id&gt; /bin/bash
ping &lt;hostname_or_ip&gt;
curl http://&lt;service_address&gt;:&lt;port&gt;
</code></pre>
<p>In Kubernetes, use <code>kubectl exec</code> to access the pod and test connectivity:</p>
<pre><code class="lang-bash">kubectl <span class="hljs-built_in">exec</span> -it &lt;pod_name&gt; -- /bin/bash
</code></pre>
<h3 id="heading-how-to-use-ephemeral-debug-containers-for-investigation">How to Use Ephemeral Debug Containers for Investigation</h3>
<p>Ephemeral debug containers are short-lived containers that help investigate issues in a running environment without altering the main application container.</p>
<h4 id="heading-what-are-ephemeral-debug-containers">What are Ephemeral Debug Containers?</h4>
<p>Ephemeral debug containers allow you to run diagnostic commands (like shell access, <code>ping</code>, or <code>curl</code>) in the same network environment as the failing application container, without modifying the application itself.</p>
<h4 id="heading-how-to-set-up-ephemeral-containers-in-docker">How to Set Up Ephemeral Containers in Docker:</h4>
<p><strong>1. Use the</strong> <code>docker run</code> Command:</p>
<p>You can create a new container for debugging by running a container with the same network settings as the failing container:</p>
<pre><code class="lang-bash">docker run -it --network container:&lt;container_name_or_id&gt; --entrypoint /bin/bash &lt;debug_image&gt;
</code></pre>
<p>This command runs an interactive shell inside the debug container using the same network as the target container.</p>
<h4 id="heading-ephemeral-containers-in-kubernetes">Ephemeral Containers in Kubernetes:</h4>
<p>Kubernetes allows you to inject an ephemeral debug container into a running pod. You can add a temporary debug container to your pod using the following command:</p>
<pre><code class="lang-bash">kubectl debug &lt;pod_name&gt; -it --image=&lt;debug_image&gt; --target=&lt;container_name&gt;
</code></pre>
<p>This command will run a new container in the same pod as the target container, allowing you to run diagnostic commands.</p>
<p>Example use cases are investigating file systems, running network diagnostics, checking configuration files, and so on.</p>
<p>These debug containers are meant to be temporary and can be discarded after the issue is resolved.</p>
<h2 id="heading-how-to-implement-advanced-debugging-techniques">How to Implement Advanced Debugging Techniques</h2>
<p>This section covers advanced methods to diagnose complex CI/CD pipeline issues that standard log analysis might miss. We’ll explore distributed tracing to track requests across multiple services and combine traces with logs and metrics for deeper insights.</p>
<p>These techniques are designed to work within budget constraints, ensuring effective debugging for your CI/CD workflows.</p>
<h3 id="heading-choosing-a-tracing-backend-for-cicd"><strong>Choosing a Tracing Backend for CI/CD</strong></h3>
<p>Distributed tracing enables you to monitor a request’s path through various services in your CI/CD pipeline, such as from a build step to a deployment, identifying delays or failures. Choosing a tracing backend involves selecting a tool to store and analyze these trace data. Below, we compare Jaeger, Tempo, and hosted solutions for distributed tracing.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td><strong>Tool</strong></td><td><strong>Resource Usage</strong></td><td><strong>Setup Complexity</strong></td><td><strong>Best For</strong></td><td><strong>CI/CD Fit</strong></td></tr>
</thead>
<tbody>
<tr>
<td><strong>Jaeger</strong></td><td>Low</td><td>Easy (Docker-based)</td><td>Small teams, local setups</td><td>Simple pipelines, quick trace views</td></tr>
<tr>
<td><strong>Tempo</strong></td><td>Low</td><td>Moderate (Grafana integration)</td><td>Grafana users, log/metric correlation</td><td>Complex pipelines, unified observability</td></tr>
<tr>
<td><strong>Hosted (e.g., Lightstep)</strong></td><td>Variable (cloud-based)</td><td>Easy (managed)</td><td>Teams with budget for cloud services</td><td>Scalable, production-grade tracing</td></tr>
</tbody>
</table>
</div><p>When to choose each one:</p>
<ul>
<li><p><strong>Jaeger</strong>: Ideal for quick, local tracing setups with minimal overhead.</p>
</li>
<li><p><strong>Tempo</strong>: Best for teams already using Grafana Loki/Prometheus for unified observability.</p>
</li>
<li><p><strong>Hosted Solutions</strong>: Suited for large-scale pipelines needing managed scalability.</p>
</li>
</ul>
<h3 id="heading-how-to-set-up-distributed-tracing-on-a-budget">How to Set Up Distributed Tracing on a Budget</h3>
<p>Distributed tracing is crucial for debugging and observing complex, multi-step operations across services. It allows you to follow requests as they propagate through different services and components of your pipeline. Implementing this on a budget can still provide valuable insights.</p>
<h4 id="heading-how-to-use-opentelemetry-with-free-backends">How to Use OpenTelemetry with Free Backends</h4>
<p><a target="_blank" href="https://www.freecodecamp.org/news/how-to-use-opentelementry-to-trace-node-js-applications/">OpenTelemetry</a> is an open-source framework that enables you to collect, process, and export telemetry data like traces and metrics. It supports multiple backends, and we’ll focus on using free, budget-friendly backends for trace storage and analysis.</p>
<p><strong>1. Install OpenTelemetry Collector:</strong></p>
<p>OpenTelemetry provides an agent (collector) that collects traces and metrics from your application and sends them to a backend.</p>
<p>To install the OpenTelemetry Collector, download the binary for your OS or use Docker to deploy it:</p>
<pre><code class="lang-bash">docker pull otel/opentelemetry-collector:latest
</code></pre>
<p>Then run the OpenTelemetry Collector in Docker with a configuration file:</p>
<pre><code class="lang-bash">docker run -d --name opentelemetry-collector -p 55680:55680 -p 14250:14250 otel/opentelemetry-collector
</code></pre>
<p><strong>2. Configure OpenTelemetry to Export to Free Backends:</strong></p>
<p>There are a few popular free backends you can use for distributed tracing, like Jaeger and Prometheus + Tempo. Let’s see how to use both here.</p>
<p>We’ll start with <strong>Jaeger</strong>, an open-source tracing backend. It’s highly scalable and works well with OpenTelemetry.</p>
<p>You can use the Docker version for easy deployment:</p>
<pre><code class="lang-bash">docker run -d --name jaeger -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 -p 5775:5775 -p 6831:6831/udp -p 6832:6832/udp -p 5778:5778 -p 16686:16686 -p 14250:14250 -p 14268:14268 -p 14250:14250 -p 9431:9431 jaegertracing/all-in-one:1.30
</code></pre>
<p>Alternatively, you can use hosted services like <strong>Lightstep</strong>, <strong>AWS X-Ray</strong>, or <strong>Honeycomb</strong> for cloud-native environments.</p>
<p>Now let’s see how to use <strong>Prometheus</strong> + <strong>Tempo</strong> for logs and metrics correlation.</p>
<p>Tempo is a distributed tracing backend built by Grafana that integrates well with other Grafana tools (Loki and Prometheus).</p>
<p>You can install Tempo using Docker:</p>
<pre><code class="lang-bash">docker run -d --name tempo -p 14268:14268 grafana/tempo:latest
</code></pre>
<p><strong>3. Instrument Your Code with OpenTelemetry SDK:</strong></p>
<p>For Python/Node.js/Java/Go applications, you can install the appropriate OpenTelemetry SDK and start tracing.</p>
<p>Here’s a Python example:</p>
<pre><code class="lang-bash">pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation
</code></pre>
<p>And a Node.js example:</p>
<pre><code class="lang-bash">npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/instrumentation
</code></pre>
<p>And one in Java:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">dependency</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">groupId</span>&gt;</span>io.opentelemetry<span class="hljs-tag">&lt;/<span class="hljs-name">groupId</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">artifactId</span>&gt;</span>opentelemetry-api<span class="hljs-tag">&lt;/<span class="hljs-name">artifactId</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">version</span>&gt;</span>1.0.0<span class="hljs-tag">&lt;/<span class="hljs-name">version</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">dependency</span>&gt;</span>
</code></pre>
<p>After installation, you can use the OpenTelemetry SDK to instrument the application and start collecting traces for HTTP requests, database queries, and other pipeline interactions.</p>
<p><strong>4. Send Data to the Collector:</strong></p>
<p>You can configure the SDK to send trace data to your OpenTelemetry Collector, which will then forward it to your backend (Jaeger, Tempo, and so on). Here’s an example for Python:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> opentelemetry <span class="hljs-keyword">import</span> trace
<span class="hljs-keyword">from</span> opentelemetry.exporter.otlp.proto.http.trace_exporter <span class="hljs-keyword">import</span> OTLPSpanExporter
<span class="hljs-keyword">from</span> opentelemetry.sdk.trace <span class="hljs-keyword">import</span> TracerProvider
<span class="hljs-keyword">from</span> opentelemetry.sdk.trace.export <span class="hljs-keyword">import</span> BatchExportSpanProcessor

trace.set_tracer_provider(TracerProvider())
exporter = OTLPSpanExporter(endpoint=<span class="hljs-string">"http://localhost:55680"</span>)
processor = BatchExportSpanProcessor(exporter)
trace.get_tracer_provider().add_span_processor(processor)
</code></pre>
<p>If traces aren’t appearing, several issues might be occurring:</p>
<ol>
<li><p><strong>Collector fails to start</strong>: Check logs with <code>docker logs otel-collector</code>. Look for errors like “port conflict” or “invalid config.”</p>
<ul>
<li>Fix: Change ports (for example, <code>55681:55680</code>) or verify the config file.</li>
</ul>
</li>
<li><p><strong>No traces in Jaeger</strong>: Ensure the collector is sending data to Jaeger (<code>http://localhost:14250</code>). Test with <code>curl http://localhost:55680</code>.</p>
<ul>
<li>Fix: Update the exporter endpoint in your SDK configuration.</li>
</ul>
</li>
<li><p><strong>Resource constraints</strong>: Monitor usage with <code>docker stats</code>.</p>
<ul>
<li>Fix: Allocate at least 2GB RAM and 10GB disk space for the collector and backend.</li>
</ul>
</li>
</ol>
<h4 id="heading-correlating-traces-with-logs-and-metrics">Correlating Traces with Logs and Metrics</h4>
<p>Combining traces with logs and metrics provides a holistic view of your pipeline’s operations, allowing you to pinpoint the root cause of issues more effectively.</p>
<p>OpenTelemetry and Grafana allow you to link traces, logs, and metrics into a unified view.</p>
<p>Let’s see how you can do this now.</p>
<p><strong>1. Link Logs and Traces Using Correlation IDs:</strong></p>
<p>When generating logs, include trace and span IDs in the log entries. This allows you to correlate logs with specific trace requests.</p>
<p>Here’s an example:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"timestamp"</span>: <span class="hljs-string">"2025-05-10T12:00:00Z"</span>,
  <span class="hljs-attr">"level"</span>: <span class="hljs-string">"error"</span>,
  <span class="hljs-attr">"message"</span>: <span class="hljs-string">"Build failure"</span>,
  <span class="hljs-attr">"trace_id"</span>: <span class="hljs-string">"1234567890abcdef"</span>,
  <span class="hljs-attr">"span_id"</span>: <span class="hljs-string">"0987654321abcdef"</span>
}
</code></pre>
<p><strong>2. Integrating Logs (Loki) with Traces (Jaeger/Tempo) in Grafana:</strong></p>
<p>Grafana can integrate traces from Jaeger or Tempo and correlate them with logs from Loki.</p>
<p>To do this:</p>
<ol>
<li><p><strong>Set up Loki and Tempo in Grafana.</strong></p>
</li>
<li><p>In Grafana’s Explore view, you can search logs and traces side-by-side.</p>
</li>
<li><p>Create dashboards that show metrics, logs, and traces for a complete view of a request flow.</p>
</li>
</ol>
<p><strong>3. Using Prometheus Metrics with Traces:</strong></p>
<p>Prometheus provides metrics that can be correlated with traces. For example, you can use <strong>exemplars</strong> in Prometheus to link specific metric data to trace data.</p>
<p><strong>Example:</strong> If you have a high error rate in your build step, you can correlate this with trace data to identify which requests failed.</p>
<h4 id="heading-creating-trace-visualizations-for-complex-pipeline-operations">Creating Trace Visualizations for Complex Pipeline Operations</h4>
<p>You can visualize traces with Jaeger or Tempo.</p>
<p><strong>To do this in Jaeger:</strong></p>
<p>Once your traces are in Jaeger, you can access the Jaeger UI (<a target="_blank" href="http://localhost:16686"><code>http://localhost:16686</code></a> by default) and use the search functionality to explore traces based on service name, trace ID, or specific operations.</p>
<p>Jaeger allows you to create custom dashboards to visualize the latency, throughput, and errors of requests across services.</p>
<p><strong>To do this in Tempo (Grafana Integration):</strong></p>
<p>Tempo integrates with Grafana, where you can create dashboards that visualize trace data from your pipeline.</p>
<p><strong>Create a Grafana dashboard:</strong></p>
<ol>
<li><p>Add Tempo as a data source in Grafana.</p>
</li>
<li><p>Use the "Trace" panel to query and visualize traces.</p>
</li>
<li><p>Combine trace visualizations with metrics (from Prometheus) and logs (from Loki) to get a unified view of your pipeline.</p>
</li>
</ol>
<p>A typical trace visualization dashboard could show the duration of each step in your pipeline (build, test, deploy) and highlight where delays or errors occur, such as slow database queries or flaky tests.</p>
<p><strong>Troubleshooting Tempo Setup Issues</strong></p>
<p>If Tempo fails to collect or display traces:</p>
<ol>
<li><p><strong>Container fails to start</strong>: Check logs with <code>docker logs tempo</code>. Look for errors like “port already in use” (for example, 14268) or “storage backend unavailable.”</p>
<ul>
<li>Fix: Change ports in the Docker command (for example, <code>-p 14269:14268</code>) or ensure the storage directory (for example, <code>/tmp/tempo</code>) exists and is writable.</li>
</ul>
</li>
<li><p><strong>No traces in Tempo</strong>: Verify the OpenTelemetry Collector is sending traces to Tempo’s endpoint (<code>http://localhost:14268</code>). Test connectivity with <code>curl http://localhost:14268</code>.</p>
<ul>
<li>Fix: Update the collector’s exporter configuration to point to the correct Tempo endpoint, and ensure no firewalls are blocking the connection.</li>
</ul>
</li>
<li><p><strong>Resource constraints</strong>: Monitor usage with <code>docker stats</code> or <code>top</code> on the host.</p>
<ul>
<li>Fix: Allocate at least 2GB RAM and 10GB disk space for Tempo, as tracing data can grow quickly with high-volume pipelines.</li>
</ul>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748226837500/c9865f8c-f737-49a5-a346-a56f4fac37fd.png" alt="Bar chart showing CI/CD pipeline trace latency for May 2025. Three pipeline stages are displayed: Build stage (blue bar) shows approximately 1,200ms latency, Test stage (yellow bar) shows approximately 800ms latency, and Deploy stage (red bar) shows approximately 1,500ms latency. The Deploy stage has the highest latency, followed by Build, then Test." class="image--center mx-auto" width="1468" height="866" loading="lazy"></p>
<p>This bar chart displays the average latency (in milliseconds) for key stages of a CI/CD pipeline in May 2025. The Build stage averages around 1,200 ms (blue), the Test stage around 800 ms (yellow), and the Deploy stage around 1,500 ms (pink), highlighting that deployment is the most time-intensive step.</p>
<h2 id="heading-how-to-build-comprehensive-debugging-dashboards">How to Build Comprehensive Debugging Dashboards</h2>
<p>This section explains how to create Grafana dashboards to troubleshoot CI/CD pipeline issues effectively. We’ll focus on setting up visualizations for key metrics, logs, and system resources to identify problems like build failures or resource bottlenecks, using budget-friendly tools to keep your observability stack lean and actionable.</p>
<h3 id="heading-designing-grafana-dashboards-specifically-for-troubleshooting">Designing Grafana Dashboards Specifically for Troubleshooting</h3>
<h4 id="heading-step-1-understand-the-key-metrics-and-logs-to-monitor">Step 1: Understand the Key Metrics and Logs to Monitor</h4>
<p>When designing a Grafana dashboard for debugging, you should focus on metrics and logs that help identify issues in the pipeline. These could include:</p>
<ul>
<li><p><strong>Build failures</strong>: Errors during build processes (compilation, test failures).</p>
</li>
<li><p><strong>Deployment failures</strong>: Issues in deployment, such as failed jobs, resource limitations, or misconfigurations.</p>
</li>
<li><p><strong>Container logs</strong>: Information about container status and logs (if using containers in your pipeline).</p>
</li>
<li><p><strong>System resource usage</strong>: CPU, memory, and disk usage that may lead to performance bottlenecks.</p>
</li>
<li><p><strong>CI/CD-specific metrics</strong>: Number of successful vs. failed pipeline runs, job duration, job queue times.</p>
</li>
</ul>
<h4 id="heading-step-2-set-up-data-sources">Step 2: Set Up Data Sources</h4>
<p>To start building the dashboard, you’ll need to set up your data sources in Grafana. First, connect your Prometheus instance for collecting metrics. To do this, go to <code>Configuration</code> &gt; <code>Data Sources</code> in Grafana. Then just add <code>Prometheus</code> as a data source and enter the URL (for example, <a target="_blank" href="http://localhost:9090"><code>http://localhost:9090</code></a>).</p>
<p>Next, you need to connect your Loki instance for logs. So go ahead and add <code>Loki</code> as a data source by specifying the URL (for example, <a target="_blank" href="http://localhost:3100"><code>http://localhost:3100</code></a>).</p>
<p>Note that if you're using other sources like InfluxDB or Elasticsearch, you’ll need to make sure that they’re properly connected as data sources.</p>
<h4 id="heading-step-3-create-panels-and-visualizations">Step 3: Create Panels and Visualizations</h4>
<p>Now that your data sources are connected, you can start building your dashboard with the following panels:</p>
<ul>
<li><p><strong>Build Status Panel:</strong></p>
<ul>
<li><p>Create a <strong>stat panel</strong> or <strong>gauge panel</strong> to show the success/failure ratio of pipeline runs.</p>
</li>
<li><p>Query Prometheus or Loki for data like build status (success or failure), number of errors, and job durations.</p>
</li>
</ul>
</li>
<li><p><strong>Error Breakdown Panel:</strong></p>
<ul>
<li><p>Use a <strong>pie chart</strong> to visualize the types of errors (for example, build, deployment, or system resource failures).</p>
</li>
<li><p>Query the logs in Loki to break down error types based on the CI tool (for example, Jenkins, GitHub Actions).</p>
</li>
</ul>
</li>
<li><p><strong>Resource Utilization Panel:</strong></p>
<ul>
<li>Use <strong>time series graphs</strong> to monitor CPU, memory, and disk usage over time, especially for resource-heavy builds or deployments.</li>
</ul>
</li>
<li><p><strong>Job Duration Panel:</strong></p>
<ul>
<li>Use <strong>bar charts</strong> or <strong>line graphs</strong> to track the average duration of jobs over time. Set thresholds for warning signs if a job takes longer than expected.</li>
</ul>
</li>
</ul>
<h4 id="heading-troubleshooting-grafana-dashboard-issues">Troubleshooting Grafana Dashboard Issues</h4>
<p>If Grafana dashboards fail to display data or show errors, you might be having one of these issues:</p>
<ol>
<li><p><strong>Missing data sources</strong>: If metrics, logs, or traces aren’t appearing, verify data source connections in Grafana (for example, Prometheus, Loki, Tempo). Check under Configuration &gt; Data Sources.</p>
<ul>
<li>Fix: Ensure the data source URLs are correct (for example, <code>http://localhost:9090</code> for Prometheus) and test the connection. Re-add the data source if needed.</li>
</ul>
</li>
<li><p><strong>Incorrect Trace IDs</strong>: If trace visualizations (for example, Tempo panels) show no data, confirm that trace IDs in logs match those in Tempo. Use a query like <code>{job="ci_cd"} | json | trace_id="1234567890abcdef"</code> in Loki to cross-check.</p>
<ul>
<li>Fix: Ensure your application logs include trace and span IDs, and verify the OpenTelemetry SDK is correctly instrumented to send traces to Tempo.</li>
</ul>
</li>
<li><p><strong>Resource Constraints</strong>: Monitor Grafana’s resource usage with <code>docker stats</code> if running in a container, or <code>top</code> on the host.</p>
<ul>
<li>Fix: Allocate at least 4GB RAM and 10GB disk space for Grafana, especially when rendering complex dashboards with multiple data sources.</li>
</ul>
</li>
</ol>
<h3 id="heading-how-to-set-up-drill-down-paths-from-high-level-to-detailed-views">How to Set Up Drill-Down Paths from High-Level to Detailed Views</h3>
<h4 id="heading-step-1-create-high-level-overview-panel">Step 1: Create High-Level Overview Panel</h4>
<p>At the top of the dashboard, include a high-level overview panel that summarizes the overall status of the pipeline. This could be:</p>
<ul>
<li><p><strong>Success/Failure Count</strong>: A simple stat panel showing the count of successful vs. failed runs.</p>
</li>
<li><p><strong>Pipeline Health Status</strong>: Display an overall health check of your pipeline using color-coded indicators (green for healthy, red for issues).</p>
</li>
</ul>
<h4 id="heading-step-2-set-up-drill-down-links">Step 2: Set Up Drill-Down Links</h4>
<p>To allow users to drill down from high-level information to detailed views:</p>
<p><strong>1. Link to detailed build information</strong>:</p>
<p>You can create a time series graph that shows build job durations. Add a link to a detailed log view when clicking on a failed job.</p>
<p>For example, when clicking a failed build, you can link to a detailed panel or a separate dashboard that shows the logs and error messages related to that specific run.</p>
<p><strong>2. Link to Logs in Loki</strong>:</p>
<p>You can use <strong>Loki's LogQL</strong> queries to set up a drill-down path. When users click on an error type or a specific job name, it should automatically filter logs for that job or error type.</p>
<p>You can set up drill-down interactions using Dashboard Links in Grafana. In the panel settings, under <code>Links</code>, specify the link to another dashboard that shows detailed logs filtered by the job name or failure type.</p>
<h4 id="heading-step-3-implement-time-range-filters">Step 3: Implement Time Range Filters</h4>
<p>To enhance drill-down functionality, you can add a <strong>time range filter</strong> to allow users to adjust the time window for both logs and metrics. This enables them to zoom in on a specific time frame where failures occurred.</p>
<h3 id="heading-how-to-create-shared-dashboards-for-team-troubleshooting">How to Create Shared Dashboards for Team Troubleshooting</h3>
<h4 id="heading-step-1-share-your-dashboard">Step 1: Share Your Dashboard</h4>
<p>Once your dashboard is designed, you can share it with your team for collaborative troubleshooting:</p>
<p>First, you’ll want to make sure that the correct permissions are set up for your team. You can define specific roles in Grafana with access to the dashboard. Go to <code>Dashboard Settings</code> &gt; <code>Permissions</code>, and grant view or edit access to users or teams.</p>
<p>Next, you can directly share a link to the dashboard with your team members. Use the <code>Share</code> option in the top-right corner of the dashboard, which provides a direct URL and also options to embed the dashboard into other tools (for example, Slack, email).</p>
<p>You can also use <strong>template variables</strong> to allow users to filter and adjust the dashboard for different pipeline runs or environments. For example, add a variable for <code>build_id</code>, <code>job_name</code>, or <code>branch_name</code> that allows users to select specific builds or branches for more granular troubleshooting.</p>
<h4 id="heading-step-2-set-up-alerting">Step 2: Set Up Alerting</h4>
<p>To ensure your team is notified of any pipeline failures, you can set up <strong>alerting rules</strong>. There are a few important ones you’ll want to set up.</p>
<p>First, create alerts for critical issues, like when a pipeline fails or exceeds expected resource usage. This could be for things like build time exceeding a threshold or failure of a deployment stage.</p>
<p>Grafana can send alerts via various channels such as Slack, email, or webhook.</p>
<p>You can also integrate your dashboards with tools like Slack or Teams for real-time notifications and collaboration. Set up automated messages for your team when the dashboard indicates an issue.</p>
<h3 id="heading-how-to-create-automated-diagnostic-tools"><strong>How to Create Automated Diagnostic Tools</strong></h3>
<h4 id="heading-building-scripts-that-collect-relevant-logs-during-failures">Building Scripts that Collect Relevant Logs During Failures</h4>
<p>To automate log collection during failures, you need scripts that can capture logs from different CI/CD stages and services as soon as a failure is detected. Here are the steps you can follow to do this:</p>
<p><strong>1. Write Failure Detection Script:</strong></p>
<p>You can leverage the exit status codes of your CI/CD tools to detect failures. For example, in GitLab CI/CD or GitHub Actions, you can check if the last command failed by inspecting <code>$?</code> in Unix-based systems.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Example for GitLab CI/CD</span>
<span class="hljs-keyword">if</span> [ $? -ne 0 ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Failure detected, collecting logs..."</span>
    <span class="hljs-comment"># Custom log collection script call</span>
    ./collect_logs.sh
<span class="hljs-keyword">fi</span>
</code></pre>
<p><strong>2. Log Collection Script (collect_</strong><a target="_blank" href="http://logs.sh"><strong>logs.sh</strong></a><strong>):</strong></p>
<p>The script should collect relevant logs, system metrics, and trace information. For instance:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>
LOG_DIR=<span class="hljs-string">"/path/to/logs"</span>
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=<span class="hljs-string">"<span class="hljs-variable">${LOG_DIR}</span>/backup/<span class="hljs-variable">${TIMESTAMP}</span>"</span>
mkdir -p <span class="hljs-variable">$BACKUP_DIR</span>

<span class="hljs-comment"># Collect logs from CI/CD agents, containers, or system logs</span>
cp /var/<span class="hljs-built_in">log</span>/ci_cd/*.<span class="hljs-built_in">log</span> <span class="hljs-variable">$BACKUP_DIR</span>/
cp /path/to/docker_logs/*.<span class="hljs-built_in">log</span> <span class="hljs-variable">$BACKUP_DIR</span>/
<span class="hljs-comment"># Collect metrics or traces from monitoring systems if needed</span>
</code></pre>
<p><strong>3. Use CI/CD Artifacts:</strong></p>
<p>For platforms like GitLab, GitHub Actions, or Jenkins, you can upload logs as artifacts for further investigation. Configure these platforms to save logs in case of a failure.</p>
<p>Here’s an example for GitHub Actions:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">steps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">Tests</span>
    <span class="hljs-attr">run:</span> <span class="hljs-string">|
      npm run test
</span>  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Upload</span> <span class="hljs-string">logs</span> <span class="hljs-string">if</span> <span class="hljs-string">test</span> <span class="hljs-string">fails</span>
    <span class="hljs-attr">if:</span> <span class="hljs-string">failure()</span>
    <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/upload-artifact@v2</span>
    <span class="hljs-attr">with:</span>
      <span class="hljs-attr">name:</span> <span class="hljs-string">test-logs</span>
      <span class="hljs-attr">path:</span> <span class="hljs-string">/path/to/test/logs</span>
</code></pre>
<p><strong>4. Centralized Logging:</strong></p>
<p>Instead of manually collecting logs, you can centralize log storage using logging systems like Grafana Loki, ELK stack, or even cloud-based solutions. This will ensure that logs are accessible even if they are overwritten or lost on individual systems.</p>
<h3 id="heading-how-to-implement-automatic-analysis-of-common-error-patterns">How to Implement Automatic Analysis of Common Error Patterns</h3>
<p>Once logs are collected, you can automate the analysis process by defining common error patterns and automatically searching for them in your logs.</p>
<h4 id="heading-step-1-define-error-patterns">Step 1: Define Error Patterns:</h4>
<p>Establish error signatures or patterns that are common in your CI/CD process, such as failed builds due to missing dependencies, permission issues, or network timeouts.</p>
<p>You can use regex or regular expressions to capture these patterns. Here’s an example – define a regex for failed test patterns:</p>
<pre><code class="lang-bash">TEST_FAILURE_REGEX=<span class="hljs-string">".*FAILURE.*"</span>
</code></pre>
<h4 id="heading-step-2-create-log-analysis-script">Step 2: Create Log Analysis Script:</h4>
<p>Next, you can write a script that scans logs for these common patterns. The script could then categorize or flag errors.</p>
<p>Here’s an example using <code>grep</code> to detect failure patterns:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>
LOG_DIR=<span class="hljs-string">"/path/to/logs"</span>
ERROR_LOG=<span class="hljs-string">"<span class="hljs-variable">${LOG_DIR}</span>/error_patterns.log"</span>
touch <span class="hljs-variable">$ERROR_LOG</span>

<span class="hljs-comment"># Define error patterns to search for</span>
ERROR_PATTERNS=(<span class="hljs-string">"FAILURE"</span> <span class="hljs-string">"ERROR"</span> <span class="hljs-string">"TIMEOUT"</span>)

<span class="hljs-keyword">for</span> PATTERN <span class="hljs-keyword">in</span> <span class="hljs-string">"<span class="hljs-variable">${ERROR_PATTERNS[@]}</span>"</span>; <span class="hljs-keyword">do</span>
    grep -i <span class="hljs-variable">$PATTERN</span> <span class="hljs-variable">$LOG_DIR</span>/*.<span class="hljs-built_in">log</span> &gt;&gt; <span class="hljs-variable">$ERROR_LOG</span>
<span class="hljs-keyword">done</span>

<span class="hljs-keyword">if</span> [ -s <span class="hljs-variable">$ERROR_LOG</span> ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Error patterns found, review the log file."</span>
<span class="hljs-keyword">fi</span>
</code></pre>
<h4 id="heading-step-3-automate-alerting">Step 3: Automate Alerting:</h4>
<p>Once an error pattern is detected, you can integrate the log analysis script with your alerting system (for example, sending an email or Slack notification).</p>
<p>Here’s an example of sending a Slack notification:</p>
<pre><code class="lang-bash"><span class="hljs-keyword">if</span> [ -s <span class="hljs-variable">$ERROR_LOG</span> ]; <span class="hljs-keyword">then</span>
    curl -X POST -H <span class="hljs-string">'Content-type: application/json'</span> \
         --data <span class="hljs-string">'{"text":"Error detected in CI pipeline. Check error log."}'</span> \
         https://hooks.slack.com/services/YOUR_SLACK_WEBHOOK_URL
<span class="hljs-keyword">fi</span>
</code></pre>
<h4 id="heading-step-4-use-observability-tools-for-pattern-recognition">Step 4: Use Observability Tools for Pattern Recognition:</h4>
<p>Leverage observability tools (Grafana Loki, Prometheus) that support log querying and visualization. You can create dashboards that automatically detect anomalies like high failure rates or recurring errors.</p>
<p>Example: Set up a Grafana dashboard with alert rules based on log frequency.</p>
<h3 id="heading-how-to-create-self-healing-pipelines-based-on-known-issues">How to Create Self-Healing Pipelines Based on Known Issues</h3>
<p>Self-healing pipelines can automatically address issues when they are detected by executing pre-defined corrective actions. Let’s walk through how you can set one up.</p>
<h4 id="heading-step-1-define-common-failures-and-solutions">Step 1: Define Common Failures and Solutions:</h4>
<p>Identify recurring issues (for example, dependency issues, build timeouts, flaky tests) that occur in your pipeline. Then, define self-healing actions to mitigate these issues.</p>
<p>Here’s an example of automatically retrying a failed step if it is a known flaky test:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">build:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">ubuntu-latest</span>
    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Run</span> <span class="hljs-string">Tests</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          npm run test
</span>      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Retry</span> <span class="hljs-string">Tests</span> <span class="hljs-string">if</span> <span class="hljs-string">Failed</span>
        <span class="hljs-attr">if:</span> <span class="hljs-string">failure()</span> <span class="hljs-string">&amp;&amp;</span> <span class="hljs-string">(steps.tests.outcome</span> <span class="hljs-string">==</span> <span class="hljs-string">'failure'</span><span class="hljs-string">)</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          echo "Retrying tests..."
          npm run test</span>
</code></pre>
<h4 id="heading-step-2-automatic-rollbacks">Step 2: Automatic Rollbacks:</h4>
<p>Set up a rollback process for failed deployments. For instance, if a deployment to production fails, the pipeline can automatically revert to the last successful build.</p>
<p>Example in GitLab CI/CD:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">deploy_production:</span>
  <span class="hljs-attr">script:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">./deploy.sh</span>
  <span class="hljs-attr">when:</span> <span class="hljs-string">on_failure</span>
  <span class="hljs-attr">retry:</span> <span class="hljs-number">3</span>
</code></pre>
<h4 id="heading-step-3-build-self-healing-logic-using-retry-mechanisms">Step 3: Build Self-Healing Logic Using Retry Mechanisms:</h4>
<p>Implement retry logic for transient issues (like network glitches) that often cause failures.</p>
<p>Example of retrying a step in GitHub Actions:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">steps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Retry</span> <span class="hljs-string">Deployment</span>
    <span class="hljs-attr">run:</span> <span class="hljs-string">|
      attempts=0
      max_attempts=3
      until [ $attempts -ge $max_attempts ]
      do
        deploy_script &amp;&amp; break
        attempts=$((attempts+1))
        echo "Attempt $attempts failed. Retrying..."
        sleep 5
      done</span>
</code></pre>
<h4 id="heading-step-4-automate-corrective-actions-for-dependency-issues">Step 4: Automate Corrective Actions for Dependency Issues:</h4>
<p>Set up automatic fixes for dependency-related failures, like clearing caches or re-installing dependencies:</p>
<pre><code class="lang-bash"><span class="hljs-keyword">if</span> [[ $(cat error.log) =~ <span class="hljs-string">"dependency not found"</span> ]]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Dependency issue detected, reinstalling dependencies..."</span>
    npm install
<span class="hljs-keyword">fi</span>
</code></pre>
<h4 id="heading-step-5-integrate-with-self-healing-services">Step 5: Integrate with Self-Healing Services:</h4>
<p>For more complex self-healing, you can integrate tools like Ansible, Puppet, or even create custom scripts that auto-patch common configuration issues.</p>
<h2 id="heading-how-to-conduct-effective-postmortems-using-logs">How to Conduct Effective Postmortems Using Logs</h2>
<p>Logs are often the single most valuable resource when reconstructing what went wrong in a CI/CD pipeline. Conducting effective postmortems with log data allows teams to extract clear timelines, pinpoint root causes, and define steps to prevent recurrence – all based on concrete evidence.</p>
<h3 id="heading-extract-timeline-and-key-events-from-the-logs">Extract Timeline and Key Events from the Logs</h3>
<p>To accurately understand what happened and when from the info contained in your logs, there’s a straightforward process you can follow.</p>
<h4 id="heading-step-1-centralize-and-structure-logs">Step 1: Centralize and Structure Logs:</h4>
<p>First, make sure that the logs from all pipeline stages (build, test, deploy) are aggregated in a central place like Grafana Loki, ELK, or OpenSearch.</p>
<p>And you’ll want to use a consistent log format (like structured JSON) that includes timestamps, log levels, pipeline stage identifiers, and correlation/request IDs.</p>
<h4 id="heading-step-2-build-a-chronological-view">Step 2: Build a Chronological View:</h4>
<p>You can use timestamp filters in your log UI (for example, Kibana, Grafana Explore) to isolate logs from the incident timeframe.</p>
<p>Look for key lifecycle events, like:</p>
<ul>
<li><p>Start and completion of pipeline steps</p>
</li>
<li><p>Status changes (for example, "test failed", "deployment started", "build queued")</p>
</li>
<li><p>Error messages and warnings</p>
</li>
<li><p>Retry events or unexpected restarts</p>
</li>
</ul>
<h4 id="heading-step-3-extract-logs-programmatically-optional">Step 3: Extract Logs Programmatically (optional):</h4>
<p>Use queries (LogQL, Elasticsearch DSL) to export relevant logs for analysis or inclusion in a post-mortem document.</p>
<h3 id="heading-how-to-identify-root-causes-through-log-analysis">How to Identify Root Causes Through Log Analysis</h3>
<p>To go beyond symptoms and find the real issue, there are various steps you can take.</p>
<p>Start by <strong>looking for the first failure</strong>. You can filter logs by <code>level=error</code> or use log pattern matching to identify the <em>earliest</em> sign of failure. Then trace backward from the failure using correlation IDs or pipeline step identifiers.</p>
<p>Second, make sure you <strong>correlate logs across systems.</strong> Match logs across CI/CD tools (like GitHub Actions → Docker logs → Kubernetes logs). You can use shared correlation IDs or job IDs to group logs from related events.</p>
<p>Next, <strong>pay attention to intermittent signals.</strong> Warnings, retries, or degraded performance preceding the failure may reveal environmental or configuration-related issues.</p>
<p>And finally, <strong>check for external dependencies.</strong> Look for timeout or connection errors involving third-party services, cloud APIs, or internal infrastructure components.</p>
<h3 id="heading-how-to-create-actionable-follow-ups-to-prevent-recurrence"><strong>How to Create Actionable Follow-Ups to Prevent Recurrence</strong></h3>
<p>There are various things you can do to turn your findings into meaningful process improvements.</p>
<p><strong>1. Document the Findings Clearly:</strong></p>
<p>Create a structured post-mortem doc that includes:</p>
<ul>
<li><p>Timeline of events with log excerpts</p>
</li>
<li><p>Immediate trigger and root cause (based on logs)</p>
</li>
<li><p>Impact summary and affected components</p>
</li>
<li><p>Screenshots or saved log queries for reference</p>
</li>
</ul>
<p><strong>2. Define Preventive Actions:</strong></p>
<p>Examples include:</p>
<ul>
<li><p>Adding missing alerts or log-based monitors</p>
</li>
<li><p>Improving log verbosity or adding missing metadata</p>
</li>
<li><p>Fixing brittle test cases or deployment scripts</p>
</li>
<li><p>Updating infrastructure limits or retry strategies</p>
</li>
</ul>
<p><strong>3. Assign Ownership and Deadlines:</strong></p>
<p>Each action item should have a responsible owner and a due date. If applicable, create automated tests or guardrails to catch similar issues in the future.</p>
<p><strong>4. Update Runbooks and Incident Playbooks:</strong></p>
<p>Add log patterns, example queries, and resolutions to shared documentation. This ensures the next person facing a similar issue can act faster.</p>
<p><strong>Pro Tip:</strong> Automate part of your post-mortem process by tagging logs from failed CI runs, exporting them to a shared location, and pre-generating dashboards or incident reports. This reduces manual effort and increases consistency.</p>
<h2 id="heading-how-to-optimize-log-storage-and-management"><strong>How to Optimize Log Storage and Management</strong></h2>
<p>As your CI/CD system grows, logs can become massive, consuming storage and impacting performance. Optimizing log storage helps you make sure that you're retaining what's valuable while staying efficient.</p>
<h3 id="heading-how-to-implement-log-rotation-and-retention-policies">How to Implement Log Rotation and Retention Policies</h3>
<p>Without rotation and retention, logs will pile up endlessly, leading to disk space exhaustion and poor performance. You can help prevent this with <strong>log rotation</strong>.</p>
<p>Log rotation involves creating new log files after a size or time threshold and archiving or deleting old ones.</p>
<p><strong>Linux logrotate tool</strong> – Configure <code>/etc/logrotate.d/&lt;your-app&gt;</code>:</p>
<pre><code class="lang-javascript">/<span class="hljs-keyword">var</span>/log/ci_cd<span class="hljs-comment">/*.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
    create 0640 root adm
}</span>
</code></pre>
<p>This example:</p>
<ul>
<li><p>Rotates daily</p>
</li>
<li><p>Keeps 7 days of logs</p>
</li>
<li><p>Compresses old logs to save space</p>
</li>
</ul>
<p><strong>Docker logs rotation</strong> – in <code>daemon.json</code>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"log-driver"</span>: <span class="hljs-string">"json-file"</span>,
  <span class="hljs-attr">"log-opts"</span>: {
    <span class="hljs-attr">"max-size"</span>: <span class="hljs-string">"50m"</span>,
    <span class="hljs-attr">"max-file"</span>: <span class="hljs-string">"5"</span>
  }
}
</code></pre>
<p>Retention policies ensure that old logs are automatically deleted based on age or storage usage.</p>
<p>You can set one up in Loki like this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">table_manager:</span>
  <span class="hljs-attr">retention_deletes_enabled:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">retention_period:</span> <span class="hljs-string">168h</span>  <span class="hljs-comment"># 7 days</span>
</code></pre>
<p>Or in Elasticsearch, use Index Lifecycle Management (ILM):</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"policy"</span>: {
    <span class="hljs-attr">"phases"</span>: {
      <span class="hljs-attr">"hot"</span>: {
        <span class="hljs-attr">"actions"</span>: {
          <span class="hljs-attr">"rollover"</span>: { <span class="hljs-attr">"max_age"</span>: <span class="hljs-string">"3d"</span>, <span class="hljs-attr">"max_size"</span>: <span class="hljs-string">"1gb"</span> }
        }
      },
      <span class="hljs-attr">"delete"</span>: {
        <span class="hljs-attr">"min_age"</span>: <span class="hljs-string">"7d"</span>,
        <span class="hljs-attr">"actions"</span>: { <span class="hljs-attr">"delete"</span>: {} }
      }
    }
  }
}
</code></pre>
<h3 id="heading-how-to-set-up-log-compaction-for-long-term-storage">How to Set Up Log Compaction for Long-Term Storage</h3>
<p>Compaction reduces redundancy and keeps only critical log info, which is ideal for long-term audits or analytics.</p>
<h4 id="heading-compaction-techniques">Compaction Techniques:</h4>
<p>There are various different compaction techniques you can try. Here are a couple:</p>
<p><strong>1. Loki (boltdb-shipper mode)</strong>:</p>
<ul>
<li><p>Uses compaction to merge log chunks and reduce storage.</p>
</li>
<li><p>Configure in <code>loki-config.yaml</code>:</p>
<pre><code class="lang-yaml">  <span class="hljs-attr">schema_config:</span>
    <span class="hljs-attr">configs:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">from:</span> <span class="hljs-number">2023-01-01</span>
        <span class="hljs-attr">store:</span> <span class="hljs-string">boltdb-shipper</span>
        <span class="hljs-attr">object_store:</span> <span class="hljs-string">filesystem</span>
        <span class="hljs-attr">schema:</span> <span class="hljs-string">v11</span>
</code></pre>
</li>
<li><p>Use a low-retention, high-compaction strategy for archived logs.</p>
</li>
</ul>
<p><strong>2. Elasticsearch</strong>:</p>
<ul>
<li><p>Use <strong>rollup jobs</strong> to reduce resolution of old data.</p>
</li>
<li><p>Stores summarized logs, for example, hourly counts of similar events.</p>
</li>
</ul>
<p><strong>3. Archive to cheaper storage</strong>:</p>
<ul>
<li>Move infrequent-access logs to S3 or Azure Blob Storage using lifecycle rules.</li>
</ul>
<h3 id="heading-how-to-balance-observability-with-resource-constraints">How to Balance Observability with Resource Constraints</h3>
<p>More logs = more observability, but also more cost and overhead. This means that you need a balance. There are various strategies that can help you achieve this balance:</p>
<ol>
<li><p><strong>Log at appropriate levels</strong>:</p>
<ul>
<li><p>Avoid excessive <code>debug</code> or <code>trace</code> logs in production.</p>
</li>
<li><p>Use <code>info</code> and <code>warn</code> levels judiciously.</p>
</li>
<li><p>Only use <code>error</code> or <code>critical</code> for actionable failures.</p>
</li>
</ul>
</li>
<li><p><strong>Sample logs</strong>:</p>
<ul>
<li><p>If high-volume pipelines generate repetitive logs, enable log sampling to reduce duplicates.</p>
</li>
<li><p>Tools like Vector or Fluent Bit support sampling.</p>
</li>
</ul>
</li>
<li><p><strong>Filter out noise</strong>:</p>
<ul>
<li>Use log filters to exclude non-critical logs before they reach the central system.</li>
</ul>
</li>
<li><p><strong>Separate hot vs. cold logs</strong>:</p>
<ul>
<li><p><strong>Hot logs</strong>: recent, real-time data for active debugging.</p>
</li>
<li><p><strong>Cold logs</strong>: archived for compliance, stored with lower performance/storage priority.</p>
</li>
</ul>
</li>
<li><p><strong>Compress everything</strong>:</p>
<ul>
<li><p>Use gzip/zstd compression for both stored and transmitted logs.</p>
</li>
<li><p>Loki, Elasticsearch, and Vector support compression out of the box.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>In this handbook, you have built a full-stack observability layer specifically optimized for CI/CD pipelines without breaking your infrastructure budget. You now have the tools and know-how to:</p>
<ul>
<li><p>Deploy Grafana Loki or a lightweight ELK alternative to capture structured logs from all parts of your pipeline.</p>
</li>
<li><p>Unify and enrich logs across CI/CD tools (for example, GitHub Actions, Jenkins, GitLab) using consistent formats and correlation IDs.</p>
</li>
<li><p>Use powerful log queries (LogQL, Kibana Query Language) to diagnose build failures, flaky tests, and deployment issues with precision.</p>
</li>
<li><p>Correlate logs with metrics and traces to gain deep, contextual visibility into pipeline behavior.</p>
</li>
<li><p>Design reusable debugging dashboards and automation that turn raw logs into insights and action.</p>
</li>
<li><p>Build a culture of shared troubleshooting knowledge through post-mortems, runbooks, and log-driven retrospectives.</p>
</li>
</ul>
<p>To see the full-stack observability layer in action, check out the complete code and configurations in my GitHub repository: <a target="_blank" href="https://github.com/Emidowojo/CICDObservability.git">github.com/Emidowojo/CICDObservability</a>. This repo includes all the setups for Grafana Loki, OpenTelemetry, Prometheus, and more, so you can deploy and explore the entire pipeline observability stack.</p>
<h3 id="heading-next-steps-for-advanced-observability-implementation">Next Steps for Advanced Observability Implementation</h3>
<p>Here’s how you can take your setup even further:</p>
<ol>
<li><p><strong>Fully integrate distributed tracing</strong>: Deploy OpenTelemetry agents across your build and deployment stages. This will help you visualize how code, builds, and deployments flow across systems in real-time.</p>
</li>
<li><p><strong>Automate diagnostic scripts and alerts</strong>: Build scripts to auto-collect logs and metrics on failure, and trigger alerts when known patterns reoccur. This enables faster detection and even self-healing pipelines.</p>
</li>
<li><p><strong>Scale and harden your log infrastructure</strong>: As usage grows, implement log retention, compaction, and storage policies. Explore scalable backends like ClickHouse or object storage (e.g., S3) for long-term archiving.</p>
</li>
<li><p><strong>Train your team on observability best practices</strong>: Share dashboards, create onboarding docs, and schedule log-analysis sessions to build team familiarity with your tools and practices.</p>
</li>
</ol>
<h3 id="heading-resources-for-continued-learning">📚 Resources for Continued Learning</h3>
<p><strong>Official Docs and Tools:</strong></p>
<ul>
<li><p><a target="_blank" href="https://grafana.com/docs/loki/">Grafana Loki Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://grafana.com/docs/loki/latest/clients/promtail/">Promtail Configuration Guide</a></p>
</li>
<li><p><a target="_blank" href="https://opentelemetry.io/docs/">OpenTelemetry</a></p>
</li>
<li><p><a target="_blank" href="https://grafana.com/docs/loki/latest/logql/">LogQL Syntax</a></p>
</li>
<li><p><a target="_blank" href="https://www.elastic.co/guide/en/kibana/current/kuery-query.html">Kibana Query Language</a></p>
</li>
<li><p><a target="_blank" href="https://vector.dev/docs/">Vector (log forwarding)</a></p>
</li>
</ul>
<p><strong>Communities:</strong></p>
<ul>
<li><p><a target="_blank" href="https://www.reddit.com/r/devops/">r/devops on Reddit</a></p>
</li>
<li><p><a target="_blank" href="https://slack.cncf.io/">CNCF Slack – #observability channel</a></p>
</li>
<li><p><a target="_blank" href="https://stackoverflow.com/questions/tagged/logging">Log Management Best Practices on Stack Overflow</a></p>
</li>
</ul>
<p>By investing in observability early and thoughtfully, you not only reduce the time to detect and resolve issues, you also build a more resilient, predictable, and transparent delivery process for your entire engineering team.</p>
<p>I hope this comes in handy for you someday. If you made it to the end of this handbook, thanks for reading! You can connect with me on <a target="_blank" href="https://www.linkedin.com/in/emidowojo/">LinkedIn</a> or on X <a target="_blank" href="https://x.com/Emidowojo">@Emidowojo</a> if you’d like to stay in touch.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ A Beginner's Guide to Observability in Cloud Native Applications ]]>
                </title>
                <description>
                    <![CDATA[ If you're new to cloud native technologies, you may have heard the term 'observability' before. But what exactly does it mean? Is it simply the ability to observe? And if so, what are we observing and why? I had the same questions when I started lear... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/observability-in-cloud-native-applications/</link>
                <guid isPermaLink="false">67e2d66c64d44185d5a6d406</guid>
                
                    <category>
                        <![CDATA[ otlp resource attributes ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloud native applications ]]>
                    </category>
                
                    <category>
                        <![CDATA[ observability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #prometheus ]]>
                    </category>
                
                    <category>
                        <![CDATA[ OpenTelemetry ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Otel ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Victoria Nduka ]]>
                </dc:creator>
                <pubDate>Tue, 25 Mar 2025 16:14:36 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742917070693/fa372981-fb20-4230-bd9f-43b7255b8ced.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you're new to cloud native technologies, you may have heard the term 'observability' before. But what exactly does it mean? Is it simply the ability to observe? And if so, what are we observing and why?</p>
<p>I had the same questions when I started learning about cloud-native technologies. In this article, I'll share my understanding of core observability concepts, introduce essential observability tools, and share insights from a related project I’m working on.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-my-introduction-to-cloud-native-technologies">My Introduction to Cloud Native Technologies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-observability">What is Observability?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-types-of-observability-data">Types of Observability Data</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-metrics">1. Metrics</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-logs">2. Logs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-traces">3. Traces</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-observability-tools">Observability Tools</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-prometheus">Prometheus</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-opentelemetry">OpenTelemetry</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-otlp-resource-attributes">What are OTLP Resource Attributes?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-importance-of-otlp-resource-attributes">Importance of OTLP Resource Attributes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-my-project-work-fits-into-all-this">How My Project Work Fits into All This</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-additional-resources">Additional Resources</a></p>
</li>
</ul>
<h2 id="heading-my-introduction-to-cloud-native-technologies">My Introduction to Cloud Native Technologies</h2>
<p>I recently got selected as a mentee for the Linux Foundation Mentorship to work on the <a target="_blank" href="https://mentorship.lfx.linuxfoundation.org/project/36e3f336-ce78-4074-b833-012015eb59be">CNCF - Prometheus project</a>. The project is UX-focused, and for the next few months, I'll be working with my mentors to understand how users expect to use OpenTelemetry Line Protocol (OTLP) Resource Attributes in Prometheus.</p>
<p>That's quite a mouthful, I know. I was overwhelmed at first, and honestly, I’m still figuring it out. This is my third week, and although I still have a lot to learn—given that I had no knowledge of cloud native technologies when I applied for this internship—I've already learned quite a bit.</p>
<p>As I often do, I intend to document what I learn through articles to help reinforce concepts in my memory and serve as a resource for other newcomers who may find themselves grappling with these technical terms in the future. You know what they say: you can't say you've understood something until you're able to explain it to someone else who's also new to the topic.</p>
<h2 id="heading-what-is-observability">What is Observability?</h2>
<p>First, I had to learn what the unfamiliar terms meant—and there were a lot of them flying around. OpenTelemetry. Prometheus. Resource attributes. I’ve come to understand that these terms fall under one umbrella: Observability. Let's start there.</p>
<p>Let’s use a food delivery app to illustrate. When someone orders food, a lot happens behind the scenes:</p>
<ul>
<li><p>The app connects to different services (restaurants, payments, delivery)</p>
</li>
<li><p>Data flows between different systems to process the order, assign a driver, and track delivery</p>
</li>
</ul>
<p>Engineers need to monitor all the processes to ensure everything works smoothly. Are orders taking too long to process? Is the payment system failing? Does the app suddenly crash under load? Which part of the system is causing delays?</p>
<p>To answer these questions, engineers <strong>instrument</strong> their code. This means that they configure it to send back real-time data about the state, performance, and behavior of the application. This practice of understanding what's happening inside a complex system based on the data it generates is known as <strong>Observability</strong>.</p>
<p>You can see the process illustrated in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742335445056/5fe7bb0b-bdf9-4f52-a2c1-7f2977411c6c.png" alt="A flowchart diagram titled &quot;Visual flow of observability data&quot; showing how data moves through a food delivery application system. The flow starts with a User who orders food from a Food App. The Food App connects to three services (Restaurant, Payment, and Delivery). All these components send data to OpenTelemetry, which collects three types of data: Metrics, Logs, and Traces. OpenTelemetry then forwards only the Metrics data to Prometheus, which stores metrics." class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>In the above flowchart diagram, you can see how data might move through a food delivery application system. The flow starts with a User who orders food from a Food App. The Food App connects to three services (Restaurant, Payment, and Delivery). All these components send data to OpenTelemetry, which collects three types of data: Metrics, Logs, and Traces. OpenTelemetry then forwards only the Metrics data to Prometheus, which stores metrics.</p>
<h2 id="heading-types-of-observability-data">Types of Observability Data</h2>
<p>There are three key types of data that systems generate for observability:</p>
<h3 id="heading-1-metrics"><strong>1. Metrics</strong></h3>
<p>Metrics are numerical measurements collected over time that represent the state or performance of your system. Examples in a food delivery app would be the number of orders processed per minute, average order processing time in milliseconds, number of active users or delivery drivers, and so on.</p>
<h3 id="heading-2-logs"><strong>2. Logs</strong></h3>
<p>Logs are text-based records of discrete events that occur within your application. Logs for our food delivery app would look something like this:</p>
<pre><code class="lang-http"><span class="hljs-attribute">ERROR</span>: Payment failed for order #12345 - Credit card declined
<span class="hljs-attribute">INFO</span>: Driver #789 assigned to order #12345
</code></pre>
<h3 id="heading-3-traces"><strong>3. Traces</strong></h3>
<p>Traces track the entire lifecycle of a request as it moves through different services in a system. They help engineers see how different components interact and identify bottlenecks in complex, distributed systems.</p>
<p>For example, in our food delivery app, a single order request might go through the following steps:<br><code>User places an order</code> → <code>Request sent to restaurant system</code> → <code>Payment processor verifies payment</code> → <code>Delivery system assigns a driver</code> → <code>User receives confirmation</code>.</p>
<p>Each step in this journey is recorded as part of a trace. This helps engineers pinpoint where delays occur and optimize the system for better performance.</p>
<p>Observability relies on metrics, logs, and traces working together to provide full system visibility. Metrics tell you something is wrong (“Error rate increased by 5%”). Logs tell you why it happened (“Payment failed due to invalid card details”). Traces show exactly where it happened (“Delay in restaurant service response”).</p>
<h2 id="heading-observability-tools"><strong>Observability Tools</strong></h2>
<p>Observability tools give you visibility into what’s going on within your application. There are a lot of them, but for the purpose of this article, we’ll talk about two: Prometheus and OpenTelemetry. </p>
<h3 id="heading-prometheus"><strong>Prometheus</strong></h3>
<p><a target="_blank" href="https://prometheus.io/">Prometheus</a> is an open-source monitoring and alerting toolkit. It does two things:</p>
<ul>
<li><p>Collects data from applications, specifically metrics (remember the data types we talked about earlier)</p>
</li>
<li><p>and stores them in a time-series database.</p>
</li>
</ul>
<p>A time-series database is a database specifically designed to handle measurements or events that occur over time.</p>
<p>Prometheus uses what's called a <strong>pull-based model</strong> to collect metrics from applications. Pull-based means Prometheus actively requests (pulls) data from services at regular intervals. Think of it like refreshing a webpage to get the latest content.</p>
<h3 id="heading-opentelemetry"><strong>OpenTelemetry</strong></h3>
<p><a target="_blank" href="https://opentelemetry.io/">OpenTelemetry (OTel)</a> collects, processes, and exports observability data. Unlike Prometheus, which mainly focuses on metrics, OpenTelemetry provides a standardized way to instrument applications for all three types of observability data: logs, metrics, and traces.</p>
<p>OpenTelemetry is designed to be vendor-agnostic. This means you can instrument your applications once with OpenTelemetry and then send that telemetry data to any supported observability backend, which could be an open-source solution like Jaeger or Prometheus, or commercial platforms like Datadog, New Relic, Dynatrace, or Honeycomb.</p>
<p>So, for example, you can use OpenTelemetry to instrument your application – and then Prometheus can pull metrics from OpenTelemetry while other tools handle logs and traces.</p>
<h2 id="heading-what-are-otlp-resource-attributes"><strong>What are OTLP Resource Attributes?</strong></h2>
<p>When OpenTelemetry collects data from applications, it does more than just gather raw telemetry data. It also provides context about that data. This context comes in the form of <strong>resource attributes</strong>, which describe where the data came from and what it relates to.</p>
<p>The 'resource' is the component (or entity) producing the data, while the 'attributes' are specific details about that resource.</p>
<p>Resource attributes are structured as pairs of information:</p>
<ul>
<li><p>The "key" is the name or identifier of the attribute (like <code>service.name</code> or <code>host.id</code>)</p>
</li>
<li><p>The "value" is the specific information for that attribute (like <code>payment-service</code> or <code>server-123</code>)</p>
</li>
</ul>
<p>Together, these key-value pairs identify and describe the specific component that's generating the observability data.</p>
<p>For example, if a payment processing service is sending metrics about transaction times, the resource attributes might include:</p>
<ul>
<li><p><code>service.name: "payment-service"</code></p>
</li>
<li><p><code>service.version: "1.2.3"</code></p>
</li>
<li><p><code>deployment.environment: "production"</code></p>
</li>
</ul>
<p>These attributes tell you exactly which service, which version, and in which environment the data is coming from, providing context for interpreting the metrics, logs, or traces.</p>
<p>Resource attributes are not arbitrary. OpenTelemetry provides a standardized set of attribute names and formats that everyone should follow, similar to having an agreed-upon language for describing services and their properties.</p>
<p>For example, OpenTelemetry specifies that you should use <code>service.name</code> (not <code>app_name</code> or <code>service_id</code>) to identify your service. They've created these standardized naming conventions (called <a target="_blank" href="https://opentelemetry.io/docs/concepts/semantic-conventions/">semantic conventions</a>) so that:</p>
<ol>
<li><p>All tools in the ecosystem can understand the same attributes</p>
</li>
<li><p>Engineers across different companies use consistent terminology</p>
</li>
<li><p>Observability data can be easily shared between different systems</p>
</li>
</ol>
<p>You can still create your own custom attributes when you need something specific (like <code>payment.provider</code> for a payment service), but using the standard attributes whenever possible means your telemetry data will work better with existing tools and be more easily understood by other engineers.</p>
<h2 id="heading-importance-of-otlp-resource-attributes">Importance of OTLP Resource Attributes</h2>
<p>Let’s say engineers want to monitor how long food deliveries take and whether there are delays in specific locations. Without resource attributes, OpenTelemetry might simply collect and report this metric like this:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">delivery_time_seconds:</span> <span class="hljs-number">1800</span>
</code></pre>
<p>This tells us that a delivery took 1,800 seconds, or 30 minutes, but nothing else. That’s useful, but it lacks context. Where did this happen? Which service handled it? If there was a delay in delivery and engineers wanted to investigate the cause, this alone would not help.</p>
<p>With OpenTelemetry’s resource attributes, the metric becomes more meaningful:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">delivery_time_seconds:</span> <span class="hljs-number">1800</span>
<span class="hljs-attr">resource:</span>
  <span class="hljs-attr">service.name:</span> <span class="hljs-string">"delivery-service"</span>
  <span class="hljs-attr">service.instance.id:</span> <span class="hljs-string">"instance-456"</span>
  <span class="hljs-attr">cloud.region:</span> <span class="hljs-string">"ng-west-2"</span>
  <span class="hljs-attr">deployment.environment:</span> <span class="hljs-string">"production"</span>
  <span class="hljs-attr">customer.city:</span> <span class="hljs-string">"Lagos"</span>
  <span class="hljs-attr">restaurant.id:</span> <span class="hljs-string">"rest-789"</span>
</code></pre>
<p>This tells us:</p>
<ul>
<li><p>The data came from the delivery service.</p>
</li>
<li><p>The instance handling the request is "instance-456".</p>
</li>
<li><p>It’s running in the ng-west-2 cloud region.</p>
</li>
<li><p>The environment is Production (not testing or staging), and so on.</p>
</li>
</ul>
<p>Now, engineers can answer more specific questions:</p>
<ul>
<li><p>Are deliveries slower in certain cities? (Filter by <code>customer.city</code>)</p>
</li>
<li><p>Are certain restaurants taking longer to prepare food? (Filter by <code>restaurant.id</code>)</p>
</li>
<li><p>Are delays only happening in a specific cloud region? (Filter by <code>cloud.region</code>)</p>
</li>
<li><p>Are issues only happening in production or also in staging? (Filter by <code>deployment.environment</code>)</p>
</li>
</ul>
<p>When issues arise, resource attributes allow engineers to quickly narrow down the source of problems. Rather than investigating every service, they can filter by specific attributes to focus their efforts.</p>
<h2 id="heading-how-my-project-work-fits-into-all-this"><strong>How My Project Work Fits into All This</strong></h2>
<p>Many engineers use OpenTelemetry for data collection and then send metrics to Prometheus for storage, querying, and analysis.</p>
<p>But Prometheus does not natively support resource attributes in the same way as OpenTelemetry. Instead, it relies on labels to organize metrics. Since Prometheus traditionally has its own labeling system for metrics, integrating OpenTelemetry's resource attributes creates interesting UX challenges.</p>
<p>One key challenge is the <strong>cardinality</strong> explosion. Cardinality refers to the number of unique combinations of label values (or dimensions) that a metric can have. A "cardinality explosion" occurs when you add labels with many possible values. OpenTelemetry often includes many detailed attributes that, if directly converted to Prometheus labels, would create an overwhelming number of time series. This can slow down Prometheus dramatically or even cause it to crash.</p>
<p>The existing solution involves stuffing all resource attributes into a single JSON-encoded Prometheus label. While this prevents the cardinality explosion, it makes querying extremely cumbersome. Users have to use complex join operations and specialized query syntax to filter or aggregate based on these attributes.</p>
<p>This approach is technically functional but creates a poor user experience. My research aims to understand how users mentally model the transition from OpenTelemetry's rich attribute system to Prometheus's more constrained label system.</p>
<p>The research goals are to:</p>
<ol>
<li><p>Understand how engineers currently use OpenTelemetry resource attributes with Prometheus</p>
</li>
<li><p>Identify pain points in the current integration between these systems</p>
</li>
<li><p>Discover user expectations for how resource attributes should be represented in Prometheus</p>
</li>
</ol>
<p>This work is particularly important as more organizations adopt OpenTelemetry as their instrumentation standard while continuing to use Prometheus for metrics monitoring. Creating a seamless experience between these two popular open-source projects will help improve the overall observability ecosystem.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Observability in cloud native applications is clearly an interesting subject and important for building reliable, performant systems. The tools and concepts we've explored – metrics, logs, traces, Prometheus, and OpenTelemetry – form the foundation of modern observability practices.</p>
<p>As I continue my mentorship program, I'll share more insights about how these technologies work together and try to break them down from the perspective of a first-time learner.</p>
<h2 id="heading-additional-resources">Additional Resources</h2>
<p>Learn more about:</p>
<ol>
<li><p><a target="_blank" href="https://opentelemetry.io/docs/">OpenTelemetry</a></p>
</li>
<li><p><a target="_blank" href="https://prometheus.io/docs/introduction/overview/">Prometheus</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/prometheus/prometheus/issues/15909">My UX research project</a></p>
</li>
</ol>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ An Intro to Metrics Driven Development: What Are Metrics and Why Should You Use Them? ]]>
                </title>
                <description>
                    <![CDATA[ By dor sever One of the coolest things I have learned in the last year is how to constantly deliver value into production without causing too much chaos. In this post, I’ll explain the metrics-driven development approach and how it helped me to achie... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/metrics-driven-development/</link>
                <guid isPermaLink="false">66d45e43182810487e0ce151</guid>
                
                    <category>
                        <![CDATA[ Metrics driven development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ agile development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Grafana ]]>
                    </category>
                
                    <category>
                        <![CDATA[ MDD ]]>
                    </category>
                
                    <category>
                        <![CDATA[ metrics ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #prometheus ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Thu, 12 Mar 2020 16:47:37 +0000</pubDate>
                <media:content url="https://cdn-media-2.freecodecamp.org/w1280/5f9c9c2b740569d1a4ca3062.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By dor sever</p>
<p>One of the coolest things I have learned in the last year is how to constantly deliver value into production without causing <strong>too</strong> much chaos.</p>
<p>In this post, I’ll explain the metrics-driven development approach and how it helped me to achieve that. By the end of the post, you’ll be able to answer the following questions:</p>
<ul>
<li>What are metrics and why should I use them</li>
<li>What are the different types of metrics</li>
<li>What tools could I use to store and display metrics</li>
<li>What is a real-world example of metrics-driven development</li>
</ul>
<h2 id="heading-what-are-metrics-and-why-should-i-use-them">What are metrics and why should I use them?</h2>
<p>Metrics give you the ability to collect information on an actively running system without changing its code.</p>
<p>It allows you to gain valuable data on the behavior of your application while it runs so you can make <strong><a target="_blank" href="https://www.techopedia.com/definition/32877/data-driven-decision-making-dddm">data-driven decisions</a></strong> based on real customer feedback and usage in production.</p>
<h2 id="heading-what-are-the-types-of-metrics-available-to-me">What are the types of metrics available to me?</h2>
<p>These are the most common metrics used today:</p>
<ul>
<li>Counter — Represents a monotonically increasing value.</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-5780-06-10-at-12.37.42-PM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Counters are really useful for measuring rates!</em></p>
<p>In this example, a counter metric is used to calculate the rate of events over time, by counting events per second</p>
<ul>
<li>Gauge — Represents a single value that can go up or down.</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-5780-06-10-at-12.42.06-PM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Gauges are really useful for measuring CPU usage!</em></p>
<p>In this example, a gauge metric is used to monitor the <a target="_blank" href="https://blog.appsignal.com/2018/03/06/understanding-cpu-statistics.html">user CPU</a> in percentages</p>
<ul>
<li>Histogram — A counting of observations (like request durations or sizes) in configurable buckets.</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-5780-06-10-at-12.44.12-PM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Histograms are really useful for measuring request duration!</em></p>
<p>In this example, a histogram metric is used to calculate the 75th and 90th percentiles of an HTTP request duration.</p>
<p>The bits and bytes of the types: counter, histogram, and gauge can be quite confusing. Try reading about it further <a target="_blank" href="https://prometheus.io/docs/concepts/metric_types/">here</a>.</p>
<h2 id="heading-what-tools-can-i-use-to-store-and-display-metrics">What tools can I use to store and display metrics?</h2>
<p>Most monitoring systems consist of a few parts:</p>
<ol>
<li>Time-series database — A database software that optimizes storing and serving <a target="_blank" href="https://en.wikipedia.org/wiki/Time_series">time-series</a> data. Two examples of this kind of database are <a target="_blank" href="https://graphite.readthedocs.io/en/latest/whisper.html">Whisper</a> and <a target="_blank" href="https://prometheus.io/">Prometheus</a>.</li>
<li>Querying engine (with a querying language) — Two examples of common query engines are: <a target="_blank" href="https://graphiteapp.org/">Graphite</a> and <a target="_blank" href="https://prometheus.io/docs/prometheus/latest/querying/basics/">PromQL</a></li>
<li>Alerting system — The mechanism that allows you to configure alerts based on graphs created by the querying language. The system can send these alerts to Mail, Slack, PagerDuty. Two examples of common alerting systems are: <a target="_blank" href="https://grafana.com/">Grafana</a> and <a target="_blank" href="https://prometheus.io/">Prometheus</a>.</li>
<li>UI — Allows you to view the graphs generated by the incoming data and configure queries and alerts. Two examples of common UI systems are: <a target="_blank" href="https://graphiteapp.org/">Graphite</a> and <a target="_blank" href="https://grafana.com/">Grafana</a></li>
</ol>
<p>The setup we are using today in <a target="_blank" href="https://medium.com/@bigpanda_engineering">BigPanda Engineering</a> is</p>
<ul>
<li><a target="_blank" href="https://www.influxdata.com/time-series-platform/telegraf/">Telegraf</a> — used as a StatsD server.</li>
<li><a target="_blank" href="https://prometheus.io/">Prometheus</a> — used as our scrapping engine, Time-series database and querying engine.</li>
<li><a target="_blank" href="https://grafana.com/">Grafana</a> — used for Alerting, and UI</li>
</ul>
<p>And the constraints we had in mind while choosing this stack were:</p>
<ul>
<li>We want scalable and elastic metrics scraping</li>
<li>We want a performant query engine</li>
<li>We want the ability to query our metrics using custom tags(such as service names, hosts, etc.)</li>
</ul>
<h2 id="heading-a-real-world-example-of-metrics-driven-development-of-a-sentiment-analysis-service">A real-world example of Metrics-driven development of a Sentiment Analysis service</h2>
<p>Let’s develop a new pipeline service that calculates sentiments based on textual inputs and does it in a Metrics Driven Development way!</p>
<p>Let’s say I need to develop this pipeline service:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/1_bj6DWm4987CuedEclpyvVw.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sentiment analysis pipeline architecture</em></p>
<p>And this is my usual development process:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/Screen-Shot-5780-06-16-at-7.31.52-AM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Usual development process - Test, code and deploy. Oh my!</em></p>
<p>So I write the following implementation:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">let</span> senService: SentimentAnalysisService = <span class="hljs-keyword">new</span> SentimentAnalysisService();
<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
    <span class="hljs-keyword">let</span> tweetInformation = kafkaConsumer.consume()
    <span class="hljs-keyword">let</span> deserializedTweet: { msg: <span class="hljs-built_in">string</span> } = deSerialize(tweetInformation)
    <span class="hljs-keyword">let</span> sentimentResult = senService.calculateSentiment(deserializedTweet.msg)
    <span class="hljs-keyword">let</span> serializedSentimentResult = serialize(sentimentResult)
    sentimentStore.store(sentimentResult);
    kafkaProducer.produce(serializedSentimentResult, <span class="hljs-string">'sentiment_topic'</span>, <span class="hljs-number">0</span>);
}
</code></pre>
<p>The full gist can be found <a target="_blank" href="https://gist.github.com/dorsev/387800acee8d1b8e6af29c86101fedb8">here</a>.</p>
<p><strong>And t</strong>his method works<strong> perfectly </strong>fine<em>**</em>. </p>
<p><strong>But what happens when it doesn’t</strong>?</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/but-.gif" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The reality is that while working (in an agile development process) we make mistakes. That’s a fact of life. </p>
<p>I believe that the real challenge with making mistakes is not to avoid them, but rather to optimize how fast we detect and repair them. So, we need to gain the ability to <strong>quickly</strong> discover our mistakes.  </p>
<p>It's time for the MDD-way.</p>
<h2 id="heading-the-metrics-driven-development-mdd-way">The Metrics Driven Development (MDD) way</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/commandments.gif" alt="Image" width="600" height="400" loading="lazy">
<em>Behold! <strong>The Three Commandments of Production!</strong></em></p>
<p>The MDD approach is heavily inspired by the <strong>Three Commandments of Production</strong> (which I had learned about the hard way).</p>
<p><strong>The</strong> Three <strong>Commandments of Production are:</strong></p>
<ol>
<li>There are mistakes and bugs in the code you write and deploy.</li>
<li>The data flowing in production is unpredictable and <strong>unique!</strong></li>
<li>Perfect your code from <strong>real customer feedback and usage in production</strong>.</li>
</ol>
<p>And since we now know the <strong>Commandments</strong>, it's time to go over the 4 step plan of the Metrics-Driven development process.</p>
<h2 id="heading-the-4-step-plan-for-a-successful-mdd">The 4-step plan for a successful MDD</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/MDD---oh-wow.png" alt="Image" width="600" height="400" loading="lazy">
<em>Metrics-driven development ?Oh wow!</em></p>
<h3 id="heading-develop-code">Develop code </h3>
<p>I write the code, and whenever possible, wrap it with a feature flag that  allows me to gradually open it for users.</p>
<h3 id="heading-metrics">Metrics</h3>
<p>This consists of two parts:</p>
<p><strong>Add metrics on relevant parts</strong></p>
<p>In this part, I ask myself what are the success or failure metrics I can define to make sure my feature works? In this case, does my new pipeline application perform its logic correctly?</p>
<p><strong>Add alerts on top of them so that I’ll be alerted when a bug occurs</strong></p>
<p>In this part, I ask myself What metric could alert me if I forgot something or did not implement it correctly?</p>
<h3 id="heading-deployment">Deployment</h3>
<p>I deploy the code and immediately monitor it to verify that it’s behaving as I have anticipated.</p>
<h3 id="heading-iterate-this-process-to-perfection">Iterate this process to perfection</h3>
<p>And that's it! Now that we have learned the process, let's tackle an important task inside it.</p>
<h2 id="heading-metrics-to-report-what-should-we-monitor">Metrics to Report — what should we monitor?</h2>
<p>One of the toughest questions for me, when I’m doing MDD, is: “what should I monitor”?</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/MALLTHINGZ.jpeg" alt="Image" width="600" height="400" loading="lazy">
<em>That’s a lovely gif. but un-realistic in most cases.</em></p>
<p>In order to answer the question, lets try to zoom out and look at the big picture.<br>All the possible information available to monitor can be divided into two parts:</p>
<ol>
<li><strong>Applicative information</strong> — Information that has an applicative context and meaning. An example of this will be — “How many tweets did we classify as positive in the last hour”?</li>
<li><strong>Operational information</strong> — Information that is related to the infrastructure that surrounds our application — Cloud data, CPU and disk utilization, network usage, etc.</li>
</ol>
<p>Now, since we cannot monitor everything, we need to choose what applicative and operational information we want to monitor.</p>
<ul>
<li>The operational part really depends on your ops stack and has built-in solutions for (almost) all your monitoring needs.</li>
<li>The applicative part is more unique to your needs, and I'll try to explain how I think about it later in this post.</li>
</ul>
<p>After we do that, we can ask ourselves the question: what alerts do we want to set up on top of the metrics we just defined?</p>
<p>The diagram (of information, metrics, alerts) can be drawn like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/world-of.png" alt="Image" width="600" height="400" loading="lazy">
<em>The world of information, metrics, and alerts.</em></p>
<h3 id="heading-applicative-metrics">Applicative metrics</h3>
<p>I usually add applicative metrics out of two needs:</p>
<h4 id="heading-to-answer-questions">To answer questions</h4>
<p>A question is something like, “When my service misbehaves, what information would be helpful to know about?”</p>
<p>Some answers to that question can be — latencies of all IO calls, processing rate, throughput, etc…</p>
<p>Most of these questions will be helpful while you are searching for the answer. But once you found it, chances are you will not look at it again (since you already know the answer).</p>
<p>These questions are usually driven by RND and are (usually) used to gather information internally.</p>
<h4 id="heading-to-add-alerts">To add Alerts</h4>
<p>This may sound backward, but I usually add applicative metrics in order to define alerts on top of them. Meaning, we define the list of alerts and then deduce from them what are the applicative metrics to report.</p>
<p>These alerts are derived from the SLA of the product and are usually treated with mission-critical importance.</p>
<h2 id="heading-common-types-of-alerts">Common types of alerts</h2>
<p>Alerts can be broken down into three parts:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/alert-types.png" alt="Image" width="600" height="400" loading="lazy">
<em>Alerts types to Metrics list</em></p>
<h3 id="heading-sla-alerts">SLA Alerts</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/sla-breach.jpeg" alt="Image" width="600" height="400" loading="lazy">
<em>SLA alerts in reality</em></p>
<p><a target="_blank" href="https://en.wikipedia.org/wiki/Service-level_agreement">SLA</a> alerts surround the places in our system where an SLA is specified to meet explicit customer or internal requirements (i.e availability, throughput, latency, etc.). SLA breaches involve paging RND and waking people up, so try to keep the alerts in this list to a minimum.</p>
<p>Also, we can define <strong>Degradation</strong> Alerts in addition to SLA Alerts.<br>Degradation alerts are defined with lower thresholds then SLA alerts, and are therefore useful in reducing the amount of SLA breaches — by giving you a proper heads-up before they happen.</p>
<p>An example of an SLA alert would be, “All sentiment requests must finish in under 500ms.”</p>
<p>An example of a Degradation Alert will be: “All sentiment requests must finish in under 400ms”.</p>
<p>These are the alerts I defined:</p>
<ol>
<li>Latency — I expect the 90th percentile of a single request duration not to exceed 300ms.</li>
<li>Success/Failure ratio of requests — I expect the number of failures per second, success per second, to remain under 0.01.</li>
<li>Throughput — I expect that the number of operations per second (ops) that the application handles will be &gt; 200</li>
<li>Data Size — I expect the amount of data that we store in a single day should not exceed 2GB.</li>
</ol>
<blockquote>
<p><em>200 ops <em> 60 bytes(Size of Sentiment Result)</em> 86400 sec in a day = 1GB &lt; 2GB</em></p>
</blockquote>
<h3 id="heading-baseline-breaching-alerts">Baseline Breaching Alerts</h3>
<p>These alerts usually involve measuring and defining a baseline and making sure it doesn’t (dramatically) change over time with alerts.</p>
<p>For example, the 99th processing latency for an event must stay relatively the same across time unless we have made dramatic changes to the logic.</p>
<p>These are the alerts I defined:</p>
<ol>
<li>Amount of Positive or Neutral or Negative Sentiment tweets — If for whatever reason, the sum of Positive tweets has increased or decreased dramatically, I might have a bug somewhere in my application.</li>
<li>All latency \ Success ratio of requests \ Throughput \ Data size must not increase\decrease dramatically over time.</li>
</ol>
<h3 id="heading-runtime-properties-alerts">Runtime Properties Alerts</h3>
<p>I’ve given a talk about <a target="_blank" href="https://www.youtube.com/watch?v=Xtuv_aduYjM">Property-Based Tests</a> and their insane strength. As it turns out, collecting metrics allows us to run property-based tests on our system <strong>in production</strong>!</p>
<p>Some properties of our system:</p>
<ol>
<li>Since we consume messages from a Kafka topic, the handled offset must monotonically increase over time.</li>
<li>1 ≥ sentiment score ≥ 0</li>
<li>A tweet should classify as either Negative \ Positive \ Neutral.</li>
<li>A tweet classification must be unique.</li>
</ol>
<p>These alerts helped me validate that:</p>
<ol>
<li>We are reading with the same group-id. Changing consumer group ids by mistake in deployment is a common mistake when using Kafka. It causes a lot of mayhem in production.</li>
<li>The sentiment score is consistently between 0 and 1.</li>
<li>Tweet category length should always be 1.</li>
</ol>
<p>In order to define these alerts, you need to submit metrics from your application. Go <a target="_blank" href="https://gist.github.com/dorsev/181e84e091ae545cb7825b782faf9d20">here</a> for the complete metrics list.</p>
<p>Using these metrics, I can create <strong>alerts</strong> that will “page” me whenever one of these properties do not hold anymore in production.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/processing-latency-alert.png" alt="Image" width="600" height="400" loading="lazy">
<em>Processing latency breached configured SLA! Oh my! ?</em></p>
<p>Let’s take a look at a possible implementation of all these metrics</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> SDC = <span class="hljs-built_in">require</span>(<span class="hljs-string">"statsd-client"</span>);
<span class="hljs-keyword">let</span> sdc = <span class="hljs-keyword">new</span> SDC({ host: <span class="hljs-string">'localhost'</span> });
<span class="hljs-keyword">let</span> senService: SentimentAnalysisService; <span class="hljs-comment">//...</span>
<span class="hljs-keyword">while</span> (<span class="hljs-literal">true</span>) {
    <span class="hljs-keyword">let</span> tweetInformation = kafkaConsumer.consume()
    sdc.increment(<span class="hljs-string">'incoming_requests_count'</span>)
    <span class="hljs-keyword">let</span> deserializedTweet: { msg: <span class="hljs-built_in">string</span> } = deSerialize(tweetInformation)
    sdc.histogram(<span class="hljs-string">'request_size_chars'</span>, deserializedTweet.msg.length);
    <span class="hljs-keyword">let</span> sentimentResult = senService.calculateSentiment(deserializedTweet.msg)
    <span class="hljs-keyword">if</span> (sentimentResult !== <span class="hljs-literal">undefined</span>) {
        <span class="hljs-keyword">let</span> serializedSentimentResult = serialize(sentimentResult)
        sdc.histogram(<span class="hljs-string">'outgoing_event_size_chars'</span>, serializedSentimentResult.length);
        sentimentStore.store(sentimentResult)
        kafkaProducer.produce(serializedSentimentResult, <span class="hljs-string">'sentiment_topic'</span>, <span class="hljs-number">0</span>);
    }

}
</code></pre>
<p>The full code can be found <a target="_blank" href="https://gist.github.com/dorsev/d7737ed6a866cf98b026d47f4f7faae8">here</a></p>
<p><strong>A few thoughts on the code example above:</strong></p>
<ol>
<li>There has been a staggering amount of metrics added to this codebase.</li>
<li>Metrics add complexity to the codebase, so, like all good things, add them responsibly and in moderation.</li>
<li>Choosing correct metric names is hard. Take your time selecting proper names. <a target="_blank" href="https://prometheus.io/docs/practices/naming/">Here’s</a> an excellent post about this.</li>
<li>You still need to collect these metrics and display them in a monitoring system (like Grafana), plus add alerts on top of them, but that’s a topic for a different post.</li>
</ol>
<h2 id="heading-did-we-reach-the-initial-goal-of-identifying-issues-and-resolving-them-faster">Did we reach the initial goal of identifying issues and resolving them faster?</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2020/03/yes-it-was-.gif" alt="Image" width="600" height="400" loading="lazy">
<em>YESSSS, it was!</em></p>
<p>We can now make sure the application latency and throughput do not degrade over time. Also, adding alerts on these metrics allows for a much faster issue discovery and resolution.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Metrics-driven development goes hand in hand with CI\CD, DevOps, and agile development process. If you are using any of the above keywords, then you are in the right place.</p>
<p>When done right, metrics make you feel more confident in your deployment in the same way that seeing passing unit-tests in your build makes you feel confident in the code you write.</p>
<p>Adding metrics allows you to deploy code and feel confident that your production environment is stable and that your application is behaving as expected over time. So I encourage you to try it out!</p>
<h4 id="heading-some-references">Some references</h4>
<ol>
<li>Here is a <a target="_blank" href="https://github.com/dorsev/MetricsSentimentAnalysis">link</a> to the code shown in this post, and <a target="_blank" href="https://gist.github.com/dorsev/181e84e091ae545cb7825b782faf9d20">here</a> is the full metrics list described.</li>
<li>If you are eager to try writing some metrics and to connect them to a monitoring system, check out <a target="_blank" href="https://prometheus.io/docs/introduction/first_steps/">Prometheus</a>, <a target="_blank" href="https://grafana.com/docs/grafana/latest/guides/getting_started/">Grafana</a> and possibly this <a target="_blank" href="https://dev.to/kirklewis/metrics-with-prometheus-statsd-exporter-and-grafana-5145">post</a></li>
<li>This guy wrote a delightful <a target="_blank" href="https://sookocheff.com/post/mdd/mdd/">post</a> about metrics-driven development. GO read it.</li>
</ol>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
