<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kasper Borg Nissen</title>
    <description>The latest articles on DEV Community by Kasper Borg Nissen (@kaspernissen).</description>
    <link>https://dev.to/kaspernissen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2062736%2Fcac81826-890e-4879-8981-3a008b7a1889.jpg</url>
      <title>DEV Community: Kasper Borg Nissen</title>
      <link>https://dev.to/kaspernissen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kaspernissen"/>
    <language>en</language>
    <item>
      <title>Observing Goose on Ollama with OpenTelemetry</title>
      <dc:creator>Kasper Borg Nissen</dc:creator>
      <pubDate>Thu, 27 Aug 2026 06:22:00 +0000</pubDate>
      <link>https://dev.to/kaspernissen/observing-goose-on-ollama-with-opentelemetry-4nf2</link>
      <guid>https://dev.to/kaspernissen/observing-goose-on-ollama-with-opentelemetry-4nf2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I work at &lt;a href="https://www.dash0.com/" rel="noopener noreferrer"&gt;Dash0&lt;/a&gt;, which is why the screenshots are from there. Nothing in this post depends on it. goose speaks OTLP, so point the exporter at whatever you prefer, whether that is Jaeger, a local Collector writing to a file, or another vendor. Every number below came out of a plain OpenTelemetry Collector before it reached any UI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Coding agents are easy to run and hard to evaluate. A session either moved your work forward or burned tokens going in circles, and once you are past a handful of runs, those two look identical. You have a bill, a pile of sessions, and no way to sort one from the other.&lt;/p&gt;

&lt;p&gt;Answering that starts with a more basic question: what does a coding agent actually report about itself? So I put one under a microscope. &lt;a href="https://goose-docs.ai/" rel="noopener noreferrer"&gt;goose&lt;/a&gt; driven by open-weight models on &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt; is a good subject, because the entire loop runs on your own machine. Nothing is proxied, summarized, or hidden behind a vendor dashboard, so every span, metric, and log record is there to read.&lt;/p&gt;

&lt;p&gt;Getting at them is genuinely easy. &lt;code&gt;goose&lt;/code&gt; speaks OpenTelemetry natively, so it takes three environment variables and no code changes. What the telemetry contains is more interesting, let's dive into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why goose and Ollama
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;goose&lt;/code&gt; is an open source coding agent now hosted under the &lt;a href="https://aaif.io/" rel="noopener noreferrer"&gt;Agentic AI Foundation&lt;/a&gt;. It runs in your terminal, calls tools, edits files, and speaks &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;. Two things make it a good subject: it is provider agnostic, so pointing it at a local model is a config change rather than a fork, and it already links the OpenTelemetry SDK, so no plugin is needed to get telemetry out.&lt;/p&gt;

&lt;p&gt;Ollama runs open-weight models on your own hardware behind an OpenAI-compatible API on &lt;code&gt;localhost:11434&lt;/code&gt;. goose treats it like any other provider.&lt;/p&gt;

&lt;p&gt;Running the pair locally means no API keys, no per-token bill, and nothing leaving your machine, which matters more than usual once you see what the agent puts in its telemetry. The cost is quality. A 30B model on a laptop is not a frontier model, and the gap shows up exactly where it hurts agents most, in reliable tool calling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up goose with Ollama
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install goose
&lt;/h3&gt;

&lt;p&gt;On macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;block-goose-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the formula name. The &lt;code&gt;goose&lt;/code&gt; formula in homebrew-core is an unrelated Go database migration tool, and the two conflict because both install a &lt;code&gt;goose&lt;/code&gt; binary.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;goose --version&lt;/code&gt; should report &lt;code&gt;1.46.0&lt;/code&gt;, which is what everything below is measured against.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Install Ollama and pull a model
&lt;/h3&gt;

&lt;p&gt;Install Ollama from &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;ollama.com&lt;/a&gt;, then pull a model. The hard requirement is native tool calling. A model that cannot emit tool calls will flail no matter how good its prose is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull qwen3.6:35b-a3b-q4_K_M
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two that work well:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3.6:35b-a3b-q4_K_M&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;23 GB&lt;/td&gt;
&lt;td&gt;256K&lt;/td&gt;
&lt;td&gt;Mixture of experts, roughly 3B active parameters, so it is quick on Apple Silicon despite its size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;devstral-small-2:24b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;15 GB&lt;/td&gt;
&lt;td&gt;384K&lt;/td&gt;
&lt;td&gt;Built for tool-driven codebase work, lighter on memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;ollama list&lt;/code&gt; confirms what you have locally.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;**Keep Ollama on the host.&lt;/em&gt;* On a Mac, Docker Desktop cannot pass the GPU through to a container, so a containerized Ollama falls back to CPU inference and gets several times slower. Every number in this post comes from Ollama running natively.*&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Connect goose to Ollama
&lt;/h3&gt;

&lt;p&gt;Three environment variables, no config file editing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GOOSE_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ollama
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GOOSE_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;qwen3.6:35b-a3b-q4_K_M
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OLLAMA_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;localhost:11434
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you would rather persist it, &lt;code&gt;goose configure&lt;/code&gt; writes the same settings to &lt;code&gt;~/.config/goose/config.yaml&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Check that it works
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;goose run &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"Reply with exactly the word: PONG. Nothing else."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    __( O)&amp;gt;  ● new session · ollama qwen3.6:35b-a3b-q4_K_M
   ____)    20260810_1 · /tmp/demo
     L L     goose is ready
PONG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header line confirms the provider and model. Get the model name wrong and goose says so plainly, listing what is actually available, which is a nicer failure than most.&lt;/p&gt;

&lt;p&gt;Now give it real work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;goose run &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="s2"&gt;"Create a Node.js Express app in app.js exposing GET /rolldice that returns a random integer from 1 to 6 as plain text. Also create a package.json with express as a dependency. Do not run npm install."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; ────────────────────────────────────────
  ▸ write
    path ~/dash0/demos/dash0-examples/goose/app.js
    content: const express = require("express");
const app = express();
const PORT = proce...

    Created ../goose/package.json (12 lines)
    Created ../goose/app.js (12 lines)
Done. Here's what I created:

### `package.json`
- Defines the `rolldice` app with a `"start"` script (`node app.js`)
- Lists `express: ^4.21.0` as a dependency (no `npm install` run)

### `app.js`
- Creates an Express server on port 3000 (or `$PORT`)
- Exposes **GET /rolldice** which returns a random integer from **1 to 6** as plain text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a local coding agent doing local work, with no account anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother observing it?
&lt;/h2&gt;

&lt;p&gt;Watching one session in a terminal, you barely need telemetry. You can see the agent working and read the files it produced.&lt;/p&gt;

&lt;p&gt;That stops being true almost immediately. Agents run headless, in CI, on other people's machines, and across a team. Coding agents also move on three fronts at once: the agent itself, the harness and configuration around it, and the underlying model. Change any one and both your spend and your output shift. Without a common set of signals, you cannot tell which change did what.&lt;/p&gt;

&lt;p&gt;Instrumenting the agent gives you the raw material for those comparisons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Where does the time go?&lt;/strong&gt; In the run below, 15.5 of 28.5 seconds sat inside two inference calls. For a local agent, "why is this slow" is nearly always the model, and now you can prove it.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What does a task cost?&lt;/strong&gt; 8,694 tokens for two files. That is the number you compare models on.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which tools does it reach for, and how often do they fail?&lt;/strong&gt; Rarely the ones you assume.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do two models compare on the same job?&lt;/strong&gt; This turns "are open models good enough yet" from a debate into a measurement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one thing this list does not give you is whether the work was any good. Hold that thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenTelemetry, and how to turn it on
&lt;/h2&gt;

&lt;p&gt;The case for OpenTelemetry here is that goose already speaks it. It links the OpenTelemetry Rust SDK and reads the standard &lt;code&gt;OTEL_*&lt;/code&gt; variables at startup, initializing traces, metrics, and logs if it finds an endpoint. No plugin, no wrapper, no code changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:4318
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http/protobuf
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;goose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole integration.&lt;/p&gt;

&lt;p&gt;To see what comes out, run a collector with a &lt;code&gt;debug&lt;/code&gt; exporter. This config also writes each signal to disk, which is how every table in this post was produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318

exporters:
  debug:
    verbosity: detailed
  file/traces:
    path: /out/traces.jsonl
  file/metrics:
    path: /out/metrics.jsonl
  file/logs:
    path: /out/logs.jsonl

service:
  pipelines:
    traces:  { receivers: [otlp], exporters: [debug, file/traces] }
    metrics: { receivers: [otlp], exporters: [debug, file/metrics] }
    logs:    { receivers: [otlp], exporters: [debug, file/logs] }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;out/traces.jsonl out/metrics.jsonl out/logs.jsonl

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; goose-collector &lt;span class="nt"&gt;-p&lt;/span&gt; 4318:4318 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/collector.yaml:/etc/otelcol-contrib/config.yaml"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/out:/out"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  otel/opentelemetry-collector-contrib:0.158.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run goose again and spans start arriving.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NB: goose has no gRPC exporter.&lt;/strong&gt; It is compiled without the &lt;code&gt;grpc-tonic&lt;/code&gt; feature, so the endpoint has to be OTLP over HTTP on &lt;strong&gt;4318&lt;/strong&gt;, not 4317. Setting &lt;code&gt;OTEL_EXPORTER_OTLP_PROTOCOL=grpc&lt;/code&gt; produces no spans, no metrics, no logs, and exit status 0, with nothing on the terminal to say so. Leaving the protocol unset is fine, since the spec default is already &lt;code&gt;http/protobuf&lt;/code&gt;.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;GOOSE_TELEMETRY_ENABLED&lt;/code&gt; is not the setting you want.&lt;/strong&gt; That flag controls goose's own product analytics, which by its own description collects error types and never collects your conversations, code, or tool arguments. It has nothing to do with OTLP export.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What goose 1.46.0 emits
&lt;/h2&gt;

&lt;p&gt;Here is the rolldice run, captured. 28.5 seconds, 2 tool calls, one whole trace.&lt;/p&gt;

&lt;p&gt;The resource attributes are clean and conventional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service.name            goose
service.namespace       goose
service.version         1.46.0
host.name               Mac.localdomain
user.name               kaspernissen
telemetry.sdk.language  rust
telemetry.sdk.name      opentelemetry
telemetry.sdk.version   0.32.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Traces
&lt;/h3&gt;

&lt;p&gt;Six spans in a single trace, properly nested, each carrying a conventional operation name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reply                                 invoke_agent      63.5 ms
  └── reply_stream                    invoke_agent   28,273.5 ms
        ├── stream_response_from_provider   chat      14,272.4 ms
        ├── dispatch_tool_call        execute_tool         1.2 ms
        ├── dispatch_tool_call        execute_tool         0.1 ms
        └── stream_response_from_provider   chat       1,195.8 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr14gdrajs8h4xe5dh255.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr14gdrajs8h4xe5dh255.png" alt="A single goose trace on 1.46.0 with the Gen AI panel open" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;One trace, properly nested. Because the attributes are conventional, the backend renders a dedicated Gen AI view without anyone writing a query.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;All three conventional operation names are there: &lt;code&gt;invoke_agent&lt;/code&gt; on the agent spans, &lt;code&gt;execute_tool&lt;/code&gt; on the tool calls, and &lt;code&gt;chat&lt;/code&gt; on the two inference spans. Those inference spans also carry the model and the provider as first-class attributes, rather than buried in a log line.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fknrjd2ay5jbcbwyfqea3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fknrjd2ay5jbcbwyfqea3.png" alt="Span attributes showing gen_ai.operation.name chat, provider ollama, and the per-call token split" width="800" height="1638"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The inference call: &lt;code&gt;stream_response_from_provider&lt;/code&gt;, tagged &lt;code&gt;gen_ai.operation.name = chat&lt;/code&gt;, carrying the provider, the model, and the real per-call token split.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That matters more than tidiness. goose can run different models for different roles through &lt;code&gt;GOOSE_SUBAGENT_MODEL&lt;/code&gt; and &lt;code&gt;GOOSE_PLANNER_MODEL&lt;/code&gt;, and attributing cost or latency to the model that incurred it needs the model on the span.&lt;/p&gt;

&lt;p&gt;Tokens arrive per call,  and the input grows as the conversation accumulates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stream_response_from_provider   7,944 in / 448 out
stream_response_from_provider   8,470 in / 224 out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tool calls are described through &lt;code&gt;gen_ai.tool.name&lt;/code&gt;, &lt;code&gt;gen_ai.tool.call.id&lt;/code&gt;, and the session is grouped by &lt;code&gt;gen_ai.conversation.id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One trap for queries.&lt;/strong&gt; Both &lt;code&gt;reply&lt;/code&gt; and &lt;code&gt;reply_stream&lt;/code&gt; are tagged &lt;code&gt;invoke_agent&lt;/code&gt;, and both carry the &lt;em&gt;same&lt;/em&gt; aggregated token counts, 16,414 in and 672 out. Read the &lt;code&gt;stream_response_from_provider&lt;/code&gt; spans instead, the ones tagged &lt;code&gt;chat&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metrics
&lt;/h3&gt;

&lt;p&gt;The metrics did not move. Seven instruments, every one a monotonic cumulative sum with no unit set, all still custom-named:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;goose.session_tokens         8694   message="Session tokens"
goose.session_duration_ms   28474   message="Session duration"
goose.tool_calls                2   tool_name=write
goose.tool_completions          2   tool_name=write, result=success
goose.session_starts            1   interactive=False
goose.session_completions       1   exit_type=normal, duration_ms=28474, total_tokens=8694, message_count=6
goose.cli_commands              1   command=run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkci5vk0nnqxl5etk9kal.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkci5vk0nnqxl5etk9kal.png" alt="All seven goose metrics, every one a SUM, with an empty Unit column" width="800" height="237"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every instrument a &lt;code&gt;SUM&lt;/code&gt;, and an entirely empty Unit column.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;None of the GenAI metric instruments exist. The conventions specify &lt;code&gt;gen_ai.client.token.usage&lt;/code&gt; and &lt;code&gt;gen_ai.client.operation.duration&lt;/code&gt; as &lt;strong&gt;histograms&lt;/strong&gt;; goose has sums, so distributions cannot be recovered. There is no p95 inference latency at any price.&lt;/p&gt;

&lt;p&gt;Three things break OpenTelemetry's own rules, independent of the GenAI conventions.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The unit is in the name.&lt;/strong&gt; &lt;code&gt;goose.session_duration_ms&lt;/code&gt; should be a duration with a unit of seconds, as the &lt;a href="https://opentelemetry.io/docs/specs/semconv/general/metrics/" rel="noopener noreferrer"&gt;metric naming guidance&lt;/a&gt; spells out.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;message&lt;/code&gt; is a description, not a dimension.&lt;/strong&gt; Every metric carries one, holding text like &lt;code&gt;"Session tokens"&lt;/code&gt;. That belongs in the instrument description.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;goose.session_completions&lt;/code&gt; puts measurements in attributes.&lt;/strong&gt; &lt;code&gt;duration_ms&lt;/code&gt;, &lt;code&gt;total_tokens&lt;/code&gt;, and &lt;code&gt;message_count&lt;/code&gt; are values, not dimensions. As attributes they have unbounded cardinality, so every session creates its own time series.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is a fourth thing, and it will cost you money if you trust it. &lt;strong&gt;&lt;code&gt;goose.session_tokens&lt;/code&gt; is not the session total.&lt;/strong&gt; It reported 8,694 for this run. The two inference calls actually consumed 7,944 + 448 + 8,470 + 224 = 17,086 tokens. The metric matches the &lt;em&gt;last&lt;/em&gt; call exactly, because each call resends the growing conversation, so what it really measures is final context size. Useful for context-budget work, wrong for spend.&lt;/p&gt;
&lt;h3&gt;
  
  
  Logs, emitted twice
&lt;/h3&gt;

&lt;p&gt;Ten records, all &lt;code&gt;INFO&lt;/code&gt;, with bodies like &lt;code&gt;Tool call started&lt;/code&gt; and &lt;code&gt;Session completed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Look at their attributes and you find keys named &lt;code&gt;monotonic_counter.goose.session_tokens&lt;/code&gt; and &lt;code&gt;monotonic_counter.goose.tool_calls&lt;/code&gt;. goose produces its metrics through the Rust &lt;code&gt;tracing&lt;/code&gt; crate's metrics-via-logs convention, and the logs pipeline exports those same events &lt;em&gt;again&lt;/em&gt; as log records. Every counter increment is billed twice, once as a data point and once as a log record, for no added information.&lt;/p&gt;

&lt;p&gt;None of the ten records carry a trace ID, so there is still no log-to-trace correlation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where it still diverges
&lt;/h2&gt;

&lt;p&gt;The attribute story is nearly complete. Four things are not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Span names are internal.&lt;/strong&gt; The convention asks for &lt;code&gt;{operation} {target}&lt;/code&gt;, so &lt;code&gt;chat qwen3.6:35b-a3b-q4_K_M&lt;/code&gt; and &lt;code&gt;execute_tool write&lt;/code&gt;. goose keeps &lt;code&gt;stream_response_from_provider&lt;/code&gt; and &lt;code&gt;dispatch_tool_call&lt;/code&gt; and layers the attributes on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Span kind is &lt;code&gt;Internal&lt;/code&gt; everywhere&lt;/strong&gt;, including the &lt;code&gt;stream_response_from_provider&lt;/code&gt; spans that wrap an HTTP call to Ollama. Those are &lt;code&gt;Client&lt;/code&gt; spans on their own terms, before any GenAI convention applies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gen_ai.agent.name&lt;/code&gt; is absent&lt;/strong&gt;, and agent-aware tooling keys off it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The metrics are untouched&lt;/strong&gt;, as above.&lt;/p&gt;

&lt;p&gt;If you are still on 1.45.0, none of this applies, because none of it is there. The GenAI work merged in &lt;a href="https://github.com/aaif-goose/goose/pull/10700" rel="noopener noreferrer"&gt;#10700&lt;/a&gt; and &lt;a href="https://github.com/aaif-goose/goose/pull/10816" rel="noopener noreferrer"&gt;#10816&lt;/a&gt; and shipped in 1.46.0 on 2026-08-12. On 1.45.0 the string &lt;code&gt;gen_ai&lt;/code&gt; does not appear in the binary at all, the trace arrives as two unrelated fragments, and the model name is recorded once, inside a log body. Upgrade rather than work around it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Message content is opt-in, and the gate leaks
&lt;/h2&gt;

&lt;p&gt;1.46.0 puts message content behind an opt-in, using the variable the conventions themselves suggest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It defaults to off, and there is a test in the codebase named &lt;code&gt;content_capture_requires_explicit_opt_in&lt;/code&gt;. One detail worth knowing: it accepts only the literal string &lt;code&gt;true&lt;/code&gt;, compared case-insensitively. &lt;code&gt;1&lt;/code&gt; silently does nothing, which will catch anyone who reaches for &lt;code&gt;=1&lt;/code&gt; out of habit.&lt;/p&gt;

&lt;p&gt;Turn it on and the conversation arrives properly structured, prompt and response side by side:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fon4prjmjhx6fr2bhnif3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fon4prjmjhx6fr2bhnif3.png" alt="Gen AI Messages panel showing the user prompt and assistant reply" width="800" height="500"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;With capture enabled, &lt;code&gt;gen_ai.input.messages&lt;/code&gt; and &lt;code&gt;gen_ai.output.messages&lt;/code&gt; populate a readable conversation view.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the right shape. Content sits behind a flag, under the conventional attribute names, which is what redaction features and shared collector configs know to look for.&lt;/p&gt;

&lt;p&gt;Now run the same task twice, once with the gate at its default and once enabled, and compare what is actually on the wire:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attribute&lt;/th&gt;
&lt;th&gt;Capture off&lt;/th&gt;
&lt;th&gt;Capture on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gen_ai.input.messages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gen_ai.output.messages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gen_ai.tool.call.arguments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gen_ai.tool.call.result&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;output&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;absent&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user_message&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;present&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trace_input&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;present&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;input&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;present&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The conventional attributes obey the flag exactly. Three legacy attributes ignore it.&lt;/p&gt;

&lt;p&gt;With capture off, this still leaves the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[span=reply]              user_message: Create a Node.js Express app in app.js exposing GET /rolldice ...
[span=reply]              trace_input:  Create a Node.js Express app in app.js exposing GET /rolldice ...
[span=dispatch_tool_call] input:        {"tool":"write","arguments":{"path":".../app.js",
                                         "content":"const express = require(\"express\");\n..."}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full prompt, twice, and the complete contents of every file the agent writes.&lt;/p&gt;

&lt;p&gt;This shipped. goose 1.46.0 advertises opt-in message capture, and with capture at its default the prompt and the file contents go out anyway. It looks like the legacy attributes were kept for backward compatibility without the gate being extended to cover them, which is easy to miss in review. I have reported it upstream.&lt;/p&gt;

&lt;p&gt;Until it is fixed, strip them at the collector. Three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;processors:
  transform:
    trace_statements:
      - delete_key(span.attributes, "trace_input")
      - delete_key(span.attributes, "user_message")
      - delete_key(span.attributes, "input")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sending it somewhere permanent
&lt;/h2&gt;

&lt;p&gt;A file of JSONL is fine for investigation, not for keeping. The same variables that pointed at a local collector point at a hosted endpoint. Using Dash0, because that is what I had running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://ingress.eu-west-1.aws.dash0.com"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_PROTOCOL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http/protobuf"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_EXPORTER_OTLP_HEADERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Authorization=Bearer%20&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DASH0_AUTH_TOKEN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,Dash0-Dataset=goose"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"goose"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;%20&lt;/code&gt; is the percent-encoded space in &lt;code&gt;Bearer &amp;lt;token&amp;gt;&lt;/code&gt;. goose decodes it correctly and appends &lt;code&gt;/v1/traces&lt;/code&gt;, &lt;code&gt;/v1/metrics&lt;/code&gt;, and &lt;code&gt;/v1/logs&lt;/code&gt; itself, so leave the path off. Note this is the HTTPS ingress on 443, not the gRPC endpoint.&lt;/p&gt;

&lt;p&gt;One trick worth knowing for comparing runs. &lt;code&gt;gen_ai.request.model&lt;/code&gt; tells you which model served a call, but it lives on spans, not on metrics, so comparing sessions by model still means tagging them yourself. goose honors &lt;code&gt;OTEL_RESOURCE_ATTRIBUTES&lt;/code&gt; on all three signals, so you can tag runs yourself and compare them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;OTEL_RESOURCE_ATTRIBUTES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"demo.model=qwen3.6-35b-a3b,demo.run=1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every comparison in this post was produced that way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdmt5iof822zrnlar4hl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzdmt5iof822zrnlar4hl.png" alt="goose.session_tokens broken down by demo_run" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;&lt;code&gt;goose.session_tokens&lt;/code&gt; split by &lt;code&gt;demo_run&lt;/code&gt;. Three runs, each separable: the same task with content capture off and on, and the small model that failed.&lt;/em&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  The run that failed and said it succeeded
&lt;/h2&gt;

&lt;p&gt;Back to where I started.&lt;/p&gt;

&lt;p&gt;I ran the identical task on &lt;code&gt;llama3.2:3b&lt;/code&gt;, a much smaller model. It asked for two files and produced one, missing its &lt;code&gt;app.listen()&lt;/code&gt; call, so the server never starts. The second write degenerated into malformed JSON and unicode garbage, and &lt;code&gt;package.json&lt;/code&gt; was never created. Half the task silently dropped.&lt;/p&gt;

&lt;p&gt;Here is what goose reported:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;goose.tool_calls{tool_name="write"}                         1
goose.tool_completions{tool_name="write", result="success"} 1
goose.session_completions{exit_type="normal"}               1
process exit code                                           0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All five spans carry status &lt;code&gt;UNSET&lt;/code&gt;. Not one error, anywhere. And note the trap: the tool call that did happen genuinely succeeded. The file got written. It was just wrong, and the half that failed never registered as an attempt at all.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;result=success&lt;/code&gt; means the tool returned without raising, not that it did anything useful. &lt;code&gt;exit_type=normal&lt;/code&gt; means the session ended without crashing, not that the task was accomplished. I had assumed tool failure rate would be a decent proxy for whether a model is competent at agentic work. It is not.&lt;/p&gt;

&lt;p&gt;You could object that I was sitting right there and could see one broken file. True, and irrelevant the moment nobody is watching: headless runs, CI, a team reading dashboards instead of terminals. There, these are the signals you have.&lt;/p&gt;

&lt;p&gt;The only hints were indirect: 5,064 tokens against 8,694 for the same task, 4 messages against 6, 16.0 seconds against 28.5. The model gave up early. That is an anomaly signal, not a correctness signal, and it means nothing without a known-good baseline.&lt;/p&gt;

&lt;p&gt;Which points at where this has to end up. Tool-level signals tell you the agent ran. Whether it delivered is a question about outcomes: did the branch merge, did the tests pass, did the change survive review. Those live outside the agent, and joining them to the session is the only way to separate a run that moved work forward from one that burned the same tokens for nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;goose answers real questions for the price of three environment variables, and 1.46.0 brings the GenAI conventions far enough that a backend can read its traces without help. Two things it does not touch are the metrics, which stay custom-named and unit-less, and log-to-trace correlation, which still does not exist.&lt;/p&gt;

&lt;p&gt;None of those is the one that matters most, which is telling a successful run from a confidently wrong one. That is not goose's problem to solve, and it is the one worth designing around, because an agent that fails loudly is easy and an agent that fails quietly is expensive.&lt;/p&gt;

&lt;p&gt;So instrument the outcome, not just the agent. Start with a task you can verify independently, then check whether the telemetry agrees with reality.&lt;/p&gt;

&lt;p&gt;Useful next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://goose-docs.ai/" rel="noopener noreferrer"&gt;goose documentation&lt;/a&gt; and &lt;a href="https://github.com/aaif-goose/goose" rel="noopener noreferrer"&gt;repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/open-telemetry/semantic-conventions-genai" rel="noopener noreferrer"&gt;GenAI semantic conventions&lt;/a&gt;, now in their own repository
&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://opentelemetry.io/docs/collector/" rel="noopener noreferrer"&gt;OpenTelemetry Collector&lt;/a&gt; docs, particularly the &lt;code&gt;debug&lt;/code&gt; and &lt;code&gt;file&lt;/code&gt; exporters
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dash0.com/blog/observing-vllm-with-opentelemetry-and-dash0" rel="noopener noreferrer"&gt;Observing vLLM with OpenTelemetry&lt;/a&gt;, which covers the inference layer underneath an agent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the same lesson this experiment kept teaching at small scale. Tokens are easy to count. Whether they delivered anything is the part worth measuring.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>codingagent</category>
      <category>observability</category>
      <category>opentelemetry</category>
    </item>
    <item>
      <title>In Search of Observability's Rails Moment</title>
      <dc:creator>Kasper Borg Nissen</dc:creator>
      <pubDate>Fri, 14 Aug 2026 10:01:24 +0000</pubDate>
      <link>https://dev.to/kaspernissen/in-search-of-observabilitys-rails-moment-1h2</link>
      <guid>https://dev.to/kaspernissen/in-search-of-observabilitys-rails-moment-1h2</guid>
      <description>&lt;p&gt;Most organizations already have observability. Agents are deployed, dashboards exist, alerts fire, and there is a budget line that grows every year. What they don't have is an observability &lt;em&gt;product&lt;/em&gt;: something with defined users, opinionated defaults, documentation, and a lifecycle.&lt;/p&gt;

&lt;p&gt;That sounds like semantics. It isn't. Capability describes what exists somewhere in your organization. Product describes how it reaches the people who need it, and what happens when it doesn't fit their case. Observability nearly always exists as a capability. It rarely exists as a product, and that gap explains why so many teams collect more telemetry every year and understand their systems no better.&lt;/p&gt;

&lt;p&gt;In 2019, Bryan Liles stood on a KubeCon keynote stage and asked the community to go and find Kubernetes' Rails moment. We did, more or less. It was called platform engineering. Observability is still looking for its own, and I have spent the past year arguing on stage about where to look.&lt;/p&gt;

&lt;p&gt;Somewhere in there the argument stopped being mine. The industry gave it a name, ran a survey, and turned it into a position platform teams now claim to hold. The name is &lt;em&gt;shift down&lt;/em&gt;. Nearly half say it describes their strategy. Very few have built it. That gap between the strategy and the build is what the rest of this post is actually about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decade of shifting left
&lt;/h2&gt;

&lt;p&gt;For about ten years, the answer to any capability gap was to shift it left. Shift testing left. Shift security left. Shift observability left. Catch problems earlier, closer to the person who wrote the code.&lt;/p&gt;

&lt;p&gt;The cost landed on developers.&lt;/p&gt;

&lt;p&gt;I watched this from inside a bank, where I was a platform engineer while we moved onto Kubernetes. We did what everyone did. We handed developers Dockerfiles, manifests, ingress rules and a wiki page, and called it empowerment. What we had actually done was take a set of genuinely hard infrastructure problems and distribute them across every team in the company, so each one could solve them slightly differently and slightly wrong.&lt;/p&gt;

&lt;p&gt;Daniel Bryant has a way of visualizing what that did, and I have been reusing his framing in talks ever since. Plot developer cognitive load against time and you don't get a straight line. Monoliths in the early 2000s kept the boundaries clear: developers wrote code, operations ran infrastructure. Service-oriented architectures pushed the line up, because now the network was the developer's problem too. Then Heroku arrived and the curve dropped hard. &lt;code&gt;git push heroku master&lt;/code&gt;, and your application was running on somebody else's problem.&lt;/p&gt;

&lt;p&gt;Then containers, microservices and Kubernetes showed up, and the curve went almost vertical.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6p1sik7kwoap8hnwb6ur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6p1sik7kwoap8hnwb6ur.png" alt="Developer Cognitive load over time, roughly 2000 to today. Credits to Daniel Bryant." width="800" height="416"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Developer Cognitive load over time, roughly 2000 to today. Credits to Daniel Bryant, whose &lt;a href="https://www.youtube.com/watch?v=btUYeOa7JPI" rel="noopener noreferrer"&gt;talk&lt;/a&gt; the visualization is based on.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Kubernetes earned that complexity, and this isn't a complaint about it. Bryan Liles made the point better than I can in his 2019 KubeCon keynote, &lt;a href="https://youtu.be/ZqQTEdHVaCw" rel="noopener noreferrer"&gt;In Search of the Kubernetes "Rails" Moment&lt;/a&gt;. Kubernetes runs on bare metal, in VMs, in other people's clouds, on a Raspberry Pi, across multiple containers and networking and storage runtimes. "&lt;em&gt;Kubernetes complexity is necessary complexity,&lt;/em&gt;" he said, and people who call it too hard are usually only thinking about their own problem.&lt;/p&gt;

&lt;p&gt;His argument was that Rails didn't win by shipping more primitives than anyone else. It won on convention over configuration: good defaults, easy to override, and an opinionated path from empty directory to running application. The line I keep coming back to is his description of the job itself. You are, he said, the principal architect of defaults.&lt;/p&gt;

&lt;p&gt;He also said something that has stuck with me ever since: Kubernetes is a vehicle taking us to our destination, not the destination itself. We keep treating it as a product when it isn't one. It's a fancy car.&lt;/p&gt;

&lt;p&gt;So the problem was never too much power. Ship primitives without defaults and you haven't removed the assembly work, you've moved it onto the user. That is the gap platform engineering stepped into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform engineering was the correction
&lt;/h2&gt;

&lt;p&gt;Platform teams didn't remove Kubernetes. The raw power stayed exactly where it was. A layer went in between, and that layer absorbed the complexity instead of distributing it. Developers stopped writing manifests and started consuming self-service APIs, templates, golden paths and guardrails. GitOps gave a lot of organizations something close to the Heroku experience again, on infrastructure they controlled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://martinfowler.com/articles/talk-about-platforms.html" rel="noopener noreferrer"&gt;Evan Bottcher's definition&lt;/a&gt; is the one I keep coming back to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;A digital platform is a foundation of self-service APIs, tools, services, knowledge and support arranged as a compelling internal product.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Compelling. Not adequate, not mandated. A developer who could bypass the platform chooses not to, because what they get is better than what they would build themselves. &lt;a href="https://teamtopologies.com/key-concepts" rel="noopener noreferrer"&gt;Team Topologies&lt;/a&gt; makes the same point from the org side: a platform team exists to provide a compelling internal product that accelerates stream-aligned teams.&lt;/p&gt;

&lt;p&gt;So platform teams learned to think in products. They started conducting user research with their own developers. Documentation. Versioned interfaces, feedback loops, adoption treated as a signal rather than a mandate. Backstage gave a lot of organizations a front door. Golden paths became standard vocabulary.&lt;/p&gt;

&lt;p&gt;That work is a decade deep for infrastructure and CI/CD. It has barely started for observability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability is repeating the mistake
&lt;/h2&gt;

&lt;p&gt;The pattern rhymes uncomfortably well with early Kubernetes. Powerful primitives, an enormous ecosystem of genuinely good tools, total flexibility, no default experience.&lt;/p&gt;

&lt;p&gt;The promise was faster root cause analysis, lower MTTR, real understanding of system behavior including the failure modes nobody predicted. Those outcomes are achievable. I have seen teams get there. But what most engineers actually experience is a different set of questions. Where do I start? Which of these tools is the right one? Why is this metric spiking, and what shipped this component?&lt;/p&gt;

&lt;p&gt;Then something breaks. One browser tab for metrics, another for logs, a third for traces. Maybe a fourth for cloud monitoring and a fifth for RUM if the problem might be in the browser. You copy an ID out of one system and paste it into another. You translate between three query languages and juggle between three mental models of what the data even is. You reconstruct the sequence of events by hand.&lt;/p&gt;

&lt;p&gt;That's human correlation. We made engineers the integration layer between observability tools, and it doesn't scale. It gets worse as the system grows and worse again at three in the morning.&lt;/p&gt;

&lt;p&gt;The same causes come up every time I ask platform teams why. Every signal type ships its own query language, so during an incident you debug syntax alongside the system. Instrumentation was historically vendor-specific, so lock-in starts at the deepest layer and migrating means touching every codebase. Metadata drifts, so one system calls it &lt;code&gt;service.name&lt;/code&gt;, another calls it &lt;code&gt;app&lt;/code&gt;, and the join quietly fails. And when all that gets expensive enough, teams skip the deeper instrumentation, add a few logs, and call it done.&lt;/p&gt;

&lt;p&gt;The easy conclusion is that the tools are bad. They're not. Most are excellent within their scope. We don't have a metrics problem, or a logging problem, or a tracing problem. We have systems problems, and each signal only ever shows one partial view.&lt;/p&gt;

&lt;p&gt;That is why I stopped saying three pillars. Three was never the right number, and profiling and real user monitoring are already here. The bigger issue is that the metaphor implies the signals stand apart, when all of the value is in how they connect. A metric with no trace to jump to is a shape on a screen. A trace with no logs underneath is a shape at higher resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The industry has a name for it now
&lt;/h2&gt;

&lt;p&gt;Weave Intelligence, the analyst firm behind PlatformCon, published a &lt;a href="https://weaveintelligence.io/research/observability-trends-in-platform-engineering" rel="noopener noreferrer"&gt;market guide on observability trends in platform engineering&lt;/a&gt; built on a survey of 105 platform engineers, SREs and operations leaders, fielded in May and June 2026. They state that no vendor commissioned, sponsored or influenced it, and that a sample this size is directional rather than statistically representative. Both caveats are worth carrying.&lt;/p&gt;

&lt;p&gt;Asked which statement best describes their observability philosophy for 2026, 47.5% chose shift down: observability as a transparent capability of the platform, with auto-injected OpenTelemetry agents, default dashboards and managed telemetry pipelines. 36.6% chose shift left, where developers instrument their own code. 12.9% are still traditional, with a central reliability team building all the dashboards.&lt;/p&gt;

&lt;p&gt;Their definition is the cleanest statement of the idea I have seen written down:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Observability is no longer only a capability that platform teams provide to developers. It has become a capability that platform teams embed into the infrastructure so that developers no longer have to proactively take care of it, but get it by design.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the correction platform engineering made for Kubernetes, applied one layer up. Absorb the complexity centrally. Deliver simplicity at the edges. Developers shouldn't assemble telemetry, they should inherit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Almost nobody has actually built it
&lt;/h2&gt;

&lt;p&gt;A name is not an achievement.&lt;/p&gt;

&lt;p&gt;In the same survey, 18% of teams have automated more than half their observability work, counting things like dashboard and alert setup. 39% say adding new metrics or traces still costs them more than 24 hours of feature-to-production time, and 11% say more than a week. 58% named the instrumentation skill gap between developers and SREs as their biggest cultural blocker. Asked whether their setup gives people relevant, actionable insight, 42.6% said yes. The other 57% split between too much noise and alerts that fire without pointing anywhere useful.&lt;/p&gt;

&lt;p&gt;Nearly half of platform teams describe shift down as their strategy. Under a fifth have automated the work that strategy implies. Most still have developers hand-instrumenting, losing a day or more per feature, and getting paged by something that doesn't say what broke.&lt;/p&gt;

&lt;p&gt;The report's authors put it politely: the vision is widely shared, the execution is not yet widespread. I'd put it less politely. A lot of us are describing a platform we haven't finished building, and that includes earlier versions of my own talks.&lt;/p&gt;

&lt;p&gt;None of this is new, either. The &lt;a href="https://www.linuxfoundation.org/hubfs/Research%20Reports/CNCF_Annual_Survey_Report_1.15.26.pdf" rel="noopener noreferrer"&gt;CNCF's 2025 annual survey&lt;/a&gt; found 47% of organizations naming "cultural changes with the development team" as their number one adoption challenge, ahead of lack of training at 36%. The substrate has been settled for years. The practices around it haven't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shifting down actually requires
&lt;/h2&gt;

&lt;p&gt;Closing that gap isn't a strategy problem, it's a build problem. The technical foundation is no longer speculative. We know how to get there. Most of it is mature, and the parts that aren't are visibly improving.&lt;/p&gt;

&lt;h3&gt;
  
  
  OpenTelemetry is the settled part
&lt;/h3&gt;

&lt;p&gt;OpenTelemetry reaches 49% production use with another 26% evaluating, according to the same CNCF survey. It &lt;a href="https://opentelemetry.io/blog/2026/otel-graduates/" rel="noopener noreferrer"&gt;graduated&lt;/a&gt; in May 2026. And in CNCF's &lt;a href="https://www.cncf.io/blog/2026/02/09/what-cncf-project-velocity-in-2025-reveals-about-cloud-natives-future/" rel="noopener noreferrer"&gt;2025 project velocity data&lt;/a&gt; it is the second-largest project in the ecosystem, logging 46,707 pull requests and issues across the year against Kubernetes' 40,559, though Kubernetes still leads comfortably on raw commits. This is mainstream infrastructure now.&lt;/p&gt;

&lt;p&gt;What matters for shifting down is the separation it creates. Instrumentation is decoupled from destination. You instrument once and export wherever you choose. The &lt;a href="https://opentelemetry.io/docs/collector/" rel="noopener noreferrer"&gt;Collector&lt;/a&gt; becomes the control point where enrichment, filtering, redaction, sampling and routing live, owned by the platform team instead of copy-pasted across a hundred repositories.&lt;/p&gt;

&lt;p&gt;That's what makes it plausibly the last observability agent you install. Changing backend becomes a routing decision instead of a migration that touches every service. You still have to move dashboards, alerts and historical data, so the switch isn't free. But the invasive part, ripping instrumentation out of application code, stops being part of the equation.&lt;/p&gt;

&lt;p&gt;One thing OpenTelemetry deliberately does not do is define the product experience. It standardizes how telemetry is produced and transported. It is not storage, not a query language, not a UI. Adopting the standard does not by itself give developers anything they'd call usable, which is exactly what happened with Kubernetes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semantic conventions are the shared vocabulary
&lt;/h3&gt;

&lt;p&gt;Engineers don't debug in signals. They debug in layers of context. Who owns this? What service is it? Which container, which runtime, which build? Which cluster and namespace? Which region?&lt;/p&gt;

&lt;p&gt;Correlation works when telemetry encodes those layers consistently and breaks when it doesn't. &lt;a href="https://opentelemetry.io/docs/specs/semconv/" rel="noopener noreferrer"&gt;Semantic conventions&lt;/a&gt; are what make it consistent: &lt;code&gt;service.name&lt;/code&gt;, &lt;code&gt;deployment.environment.name&lt;/code&gt;, &lt;code&gt;k8s.cluster.name&lt;/code&gt;, &lt;code&gt;cloud.region&lt;/code&gt;, &lt;code&gt;http.request.method&lt;/code&gt;, &lt;code&gt;http.response.status_code&lt;/code&gt;. Not magic, and not AI. Structured metadata that means the same thing everywhere.&lt;/p&gt;

&lt;p&gt;One gap is worth being honest about, because it's the layer people ask me about most and the one I can't point them to. The conventions cover infrastructure, compute, platform and architecture well. The organizational layer, who owns this service and who you call at 2am, still isn't covered by stable conventions. Work is underway. Until it lands, &lt;a href="https://github.com/open-telemetry/weaver" rel="noopener noreferrer"&gt;OpenTelemetry Weaver&lt;/a&gt; lets you define and enforce your own, which beats waiting for the spec.&lt;/p&gt;

&lt;h3&gt;
  
  
  Operators are the delivery mechanism
&lt;/h3&gt;

&lt;p&gt;Conventions on their own are documentation, and documentation that says "please remember to enable tracing" is not a product. Something has to make the default happen without anyone choosing it.&lt;/p&gt;

&lt;p&gt;In Kubernetes that something is an operator. The &lt;a href="https://opentelemetry.io/docs/platforms/kubernetes/operator/" rel="noopener noreferrer"&gt;OpenTelemetry Operator&lt;/a&gt; gives you an &lt;code&gt;Instrumentation&lt;/code&gt; custom resource where the platform team defines where telemetry goes and how it's configured. Annotate a pod and the operator &lt;a href="https://opentelemetry.io/docs/platforms/kubernetes/operator/automatic/" rel="noopener noreferrer"&gt;injects zero-code instrumentation&lt;/a&gt; at deploy time for .NET, Java, Node.js, Python or Go. Zero-code is the term the project now uses for &lt;a href="https://opentelemetry.io/docs/zero-code/" rel="noopener noreferrer"&gt;instrumenting without touching application source&lt;/a&gt;, and it is the whole ballgame for a platform team, because it is the only approach that scales to services the platform team has never seen.&lt;/p&gt;

&lt;p&gt;I demo this at conferences because the shape lands better than the description. A developer scaffolds a service from a template, writes the business logic, deploys. There's nothing observability-related in the code at all. The service appears in the distributed trace immediately, parented correctly to the calls around it, carrying cluster and namespace and deployment attributes, context propagating across the boundary. Nobody did anything to make that happen except deploy.&lt;/p&gt;

&lt;p&gt;Coverage isn't uniform and I won't pretend otherwise. Some language ecosystems are considerably better instrumented than others, and auto-instrumentation occasionally conflicts with application frameworks. Treat it as a strong default with known edges.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developers still own the part only they can write
&lt;/h3&gt;

&lt;p&gt;None of this says developers should stop caring about observability. It says the opposite, by clearing space for the part that needs them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/adrianavillela/" rel="noopener noreferrer"&gt;Adriana Villela&lt;/a&gt; gave a talk at Cloud Native Bergen last year called &lt;a href="https://youtu.be/bSa6bBLymFE" rel="noopener noreferrer"&gt;Observability Is a Team Sport&lt;/a&gt;, and the line I took away from it is one she credits to Josh Lee: don't make the developers do everything, but make everyone do observability. Her point is that an observability team operating in a silo defeats its own purpose, because observability touches every part of the development lifecycle.&lt;/p&gt;

&lt;p&gt;The way I think about it is a football team. Different positions, different jobs, and the team fails when everyone crowds the same part of the field.&lt;/p&gt;

&lt;p&gt;The platform can produce HTTP spans, database calls, queue operations, resource attributes and context propagation, because those are identical everywhere and should be solved once. The platform cannot produce the span that says a settlement completed, or the attribute explaining which fraud rule rejected a payout, or the SLO capturing what this team promises its callers. Write those on a developer's behalf and you get spans that are empty or spans that lie.&lt;/p&gt;

&lt;p&gt;The platform provides the scaffolding. The developer provides the meaning. When a developer stops asking how to set up tracing and starts asking what business context to capture, the product is working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more now that agents are involved
&lt;/h2&gt;

&lt;p&gt;I'd make this argument regardless of what's happening in AI. Reducing cognitive load and correlating signals were worth doing in 2019. But the timeline has compressed.&lt;/p&gt;

&lt;p&gt;Agents consume telemetry differently than we do. A human investigates serially, one hypothesis at a time. Agentic tooling investigates in parallel, ten or more hypotheses at once, each hitting observability endpoints independently. The Weave guide puts a single LLM query at two to five times the telemetry of a standard log event, with parallel investigation generating query loads an order of magnitude or two beyond what existing architectures were sized for.&lt;/p&gt;

&lt;p&gt;The volume isn't the interesting part. Agents reason over structure.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;service.name&lt;/code&gt; means the same thing everywhere, an agent can navigate. If &lt;code&gt;cloud.region&lt;/code&gt; is consistent, it can scope. If trace context propagates across boundaries, it can follow a request through a system it has never seen. Without conventions, correlation fails. Without correlation, an agent guesses, and a confident guess in fluent prose is worse than no answer.&lt;/p&gt;

&lt;p&gt;Garbage in, garbage out, same as it ever was. The survey found 20% of respondents still running proprietary instrumentation exclusively, which the analysts call AI readiness debt. That framing is right. Every investment in telemetry structure pays twice, once in human debugging and once in whatever you connect to it later.&lt;/p&gt;

&lt;p&gt;So AI doesn’t remove the need to think about observability at the platform level. It raises the cost of skipping it. &lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Observability isn't failing. It's at the point Kubernetes was at in 2019: powerful primitives, an enormous ecosystem, real innovation, no coherent default experience for the people expected to use it. Still in search of its Rails moment.&lt;/p&gt;

&lt;p&gt;We solved that for infrastructure by treating the platform as a product, absorbing complexity centrally and leaving documented escape hatches for the cases the defaults don't fit. The same move works here, and most platform teams already believe it. What's left is closing the distance between believing it and having shipped it.&lt;/p&gt;

&lt;p&gt;If you want somewhere to start this quarter: deploy the OpenTelemetry Collector as the single place telemetry policy lives, enforce semantic conventions there rather than asking every team to remember them, then put the OpenTelemetry Operator in front so deploying onto your platform is the only thing a developer does to get instrumented. That order is deliberate. Each step makes the next one cheaper.&lt;/p&gt;

&lt;p&gt;I work at Dash0, so weigh the next paragraph accordingly. We built OpenTelemetry-native rather than OpenTelemetry-compatible, which means the conventions your platform team enforces are the ones the product reasons about directly, with no proprietary translation layer in between quietly losing things. Agent0, our AI control plane for production, scans the environment continuously, answers questions across telemetry and code in plain language with citations you can check, correlates across signals, and generates dashboards, alerts, runbooks and pull requests that are validated against live telemetry before they ship. It works better when the telemetry underneath has structure, which is this whole post applied to our own software.&lt;/p&gt;

&lt;p&gt;If you're earlier in this, we wrote &lt;a href="https://www.dash0.com/opentelemetry-for-dummies" rel="noopener noreferrer"&gt;OpenTelemetry for Dummies&lt;/a&gt; as a short introduction, and there's more on &lt;a href="https://www.dash0.com/knowledge/otel-semantic-conventions-explainer" rel="noopener noreferrer"&gt;semantic conventions&lt;/a&gt; and on &lt;a href="https://www.dash0.com/knowledge/logs-metrics-and-traces-observability" rel="noopener noreferrer"&gt;why logs, metrics and traces work better together&lt;/a&gt; in our knowledge base.&lt;/p&gt;

&lt;p&gt;Build the foundation now. The agents are already showing up, and they'll only ever be as good as the structure you hand them.&lt;/p&gt;

</description>
      <category>opentelemetry</category>
      <category>observability</category>
      <category>platformengineering</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
