<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Opentelemetry on PHP Boy Scout</title><link>https://phpboyscout.uk/tags/opentelemetry/</link><description>Recent content in Opentelemetry on PHP Boy Scout</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>Matt Cockayne</copyright><lastBuildDate>Thu, 04 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://phpboyscout.uk/tags/opentelemetry/index.xml" rel="self" type="application/rss+xml"/><item><title>Telemetry that asks, and telemetry that doesn't</title><link>https://phpboyscout.uk/telemetry-that-asks-and-telemetry-that-doesnt/</link><pubDate>Thu, 04 Jun 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/telemetry-that-asks-and-telemetry-that-doesnt/</guid><description>&lt;img src="https://phpboyscout.uk/telemetry-that-asks-and-telemetry-that-doesnt/cover-telemetry-that-asks-and-telemetry-that-doesnt.png" alt="Featured image of post Telemetry that asks, and telemetry that doesn't" /&gt;&lt;p&gt;go-tool-base has had a thing called telemetry for a long while now. It&amp;rsquo;s the
opt-in kind: the &lt;a class="link" href="https://phpboyscout.uk/telemetry-that-asks-first/" &gt;product analytics&lt;/a&gt;
that asks a user&amp;rsquo;s permission before it phones a single byte home, sits there as
a no-op until they say yes, and can be wiped on request. The whole package is
built around consent.&lt;/p&gt;
&lt;p&gt;Then the &lt;a class="link" href="https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-6/" &gt;web-service series&lt;/a&gt;
went and needed telemetry too. Not that telemetry. The other one, the one the
rest of the industry means when it says the word: traces, metrics and logs of a
running service. And the awkward thing about those two is that they share a name,
they want to share a package, and they pull in exactly opposite directions on the
one question that matters most.&lt;/p&gt;
&lt;p&gt;This is the story of how 0.7.x grew a second telemetry without breaking the
first, and where the line between them ended up getting drawn.&lt;/p&gt;
&lt;h2 id="why-bother-putting-it-in-the-framework-at-all"&gt;Why bother putting it in the framework at all
&lt;/h2&gt;&lt;p&gt;The starting point is that I could have left observability out. A reader could
wire up OpenTelemetry in their own service and go about their day. But the six
parts of the web-service series spent a lot of effort making the transports
first-class: a gRPC server, an HTTP server, a gateway, TLS across all of them,
each one a &lt;code&gt;Register&lt;/code&gt; call against the controller. Turning a CLI into a real
long-running service and then shrugging &amp;ldquo;observability is your problem&amp;rdquo; would
have left a hole exactly where it hurts.&lt;/p&gt;
&lt;p&gt;Because a service you can&amp;rsquo;t see into is a liability the moment it leaves your
laptop. The series ended with a macguffin service that was typed, fast and served
over TLS, and was also a black box: when it got slow, you had nowhere to look.
Metrics and traces are how you get the lights on, and they deserved the same
first-class treatment as the things they observe.&lt;/p&gt;
&lt;p&gt;The other half of the reason is that the framework already had a foot in this
world. The analytics package&amp;rsquo;s preferred backend speaks OTLP, the OpenTelemetry
wire protocol. So OpenTelemetry was already in the building. Doing observability
any other way would have meant two standards where one would do.&lt;/p&gt;
&lt;h2 id="the-catch-two-telemetries-opposite-instincts"&gt;The catch: two telemetries, opposite instincts
&lt;/h2&gt;&lt;p&gt;Here&amp;rsquo;s where it gets interesting, and it&amp;rsquo;s the part worth slowing down on.&lt;/p&gt;
&lt;p&gt;The analytics telemetry is about a user. It collects usage data, hashed machine
id, which command ran, exit code, and the entire design assumes you have to ask
first. It is off by default. The collector you get when it&amp;rsquo;s disabled is a no-op,
so nothing is recorded until the user opts in, and there&amp;rsquo;s a deletion path for
when they change their mind. That&amp;rsquo;s not an add-on, that&amp;rsquo;s by design.&lt;/p&gt;
&lt;p&gt;The observability telemetry is about a service. It emits operational data, how
long a request took, which span was slow, how many errored, to a collector the
operator runs. And there is no user in the loop to ask. The operator deploys the
service, points it at their collector, and that act is itself the consent. Asking
would be nonsensical: whose permission, for data about their own service, on
their own infrastructure?&lt;/p&gt;
&lt;p&gt;So you have two things called telemetry, wanting to live in one package, with the
opposite default on consent. One is off until someone says yes; the other is on
the moment it&amp;rsquo;s configured. Get that wiring wrong and you fail in one of two ugly
ways. Gate the operational telemetry behind the user&amp;rsquo;s analytics opt-in, and a
service&amp;rsquo;s tracing silently does nothing because nobody ticked a box meant for
something else. Or loosen the analytics gate to make observability flow, and you
start leaking usage data the user never agreed to share. Neither is acceptable,
and &amp;ldquo;just use two packages&amp;rdquo; throws away everything the two genuinely have in
common.&lt;/p&gt;
&lt;h2 id="what-they-actually-share"&gt;What they actually share
&lt;/h2&gt;&lt;p&gt;Quite a lot, as it turns out, and all of it below the consent line.&lt;/p&gt;
&lt;p&gt;Both ship their data over OTLP to a collector. Both need to describe who is
emitting, the service name and version, the resource in OpenTelemetry&amp;rsquo;s terms.
Both parse an endpoint, attach headers, decide whether the connection is
plaintext. None of that has the faintest thing to do with consent. It&amp;rsquo;s just the
plumbing of getting bytes to a collector, and the analytics backend already had
all of it, written inline.&lt;/p&gt;
&lt;p&gt;So the shape of the solution fell out of the problem. Lift the shared plumbing
into one place, let both telemetries stand on it, and keep the consent decision
firmly out of that shared layer. The structure under &lt;code&gt;pkg/telemetry&lt;/code&gt; ended up
like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pkg/telemetry/
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; telemetry.go the analytics Collector (consent-gated)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; backend_otel.go its OTLP backend
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; posthog/ datadog/ vendor analytics backends
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; otelcore/ shared: OTLP endpoint, resource, telemetry.* config
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; tracing/ observability signal
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; metrics/ observability signal
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; logs/ observability signal
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; observability.go Setup: builds the enabled signals (implied consent)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The new &lt;code&gt;otelcore&lt;/code&gt; is the keystone. It holds the three things both sides need and
nothing they don&amp;rsquo;t:
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/otelcore/endpoint.go#L22" target="_blank" rel="noopener"
 &gt;&lt;code&gt;ParseEndpoint&lt;/code&gt;&lt;/a&gt;
for the OTLP URL,
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/otelcore/resource.go#L11" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Resource&lt;/code&gt;&lt;/a&gt;
for the service identity, and
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/otelcore/config.go#L33" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Resolve&lt;/code&gt;&lt;/a&gt;
for reading the shared &lt;code&gt;telemetry.*&lt;/code&gt; config (a base endpoint, plus per-signal
overrides, in the same cascade as the TLS config). It imports no signal exporter
and knows nothing about traces, metrics, logs or analytics. It is deliberately
dumb plumbing.&lt;/p&gt;
&lt;h2 id="the-refactor-making-the-old-telemetry-stand-on-the-new-core"&gt;The refactor: making the old telemetry stand on the new core
&lt;/h2&gt;&lt;p&gt;This next part is where the old telemetry and the new one become a single thing.
The analytics OTLP backend was the first user of OTLP in the framework, and it had
grown its own copy of all that
plumbing: a function that parsed the endpoint URL, split out the host and path,
worked out the insecure flag, and built the resource from a service name. Exactly
the code the three new signals were about to need.&lt;/p&gt;
&lt;p&gt;So rather than write it a second time and let the two drift, the analytics
backend was refactored onto &lt;code&gt;otelcore&lt;/code&gt;. Its exporter builder,
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/backend_otel.go#L134" target="_blank" rel="noopener"
 &gt;&lt;code&gt;buildOTelExporterOpts&lt;/code&gt;&lt;/a&gt;,
now calls &lt;code&gt;otelcore.ParseEndpoint&lt;/code&gt;, the same function &lt;code&gt;tracing&lt;/code&gt;, &lt;code&gt;metrics&lt;/code&gt; and
&lt;code&gt;logs&lt;/code&gt; call, and the resource comes from &lt;code&gt;otelcore.Resource&lt;/code&gt;, the same one they
use. One implementation of &amp;ldquo;talk OTLP to a collector&amp;rdquo;, four callers: the
analytics backend and the three observability signals. Change how the framework
forms an OTLP endpoint, and every signal moves together.&lt;/p&gt;
&lt;p&gt;The reassuring part was that the analytics tests didn&amp;rsquo;t budge. The refactor moved
code without changing behaviour, and the consent machinery, the opt-in, the
no-op-when-disabled, the deletion path, never came near &lt;code&gt;otelcore&lt;/code&gt;. Which is
exactly the point.&lt;/p&gt;
&lt;h2 id="where-the-line-is"&gt;Where the line is
&lt;/h2&gt;&lt;p&gt;Because the shared core is the easy half. The half that earns its keep is the bit
that isn&amp;rsquo;t shared, and it&amp;rsquo;s a single, deliberate line.&lt;/p&gt;
&lt;p&gt;The analytics collector keeps its gate. The constructor,
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/telemetry.go#L84" target="_blank" rel="noopener"
 &gt;&lt;code&gt;NewCollector&lt;/code&gt;&lt;/a&gt;,
still returns a no-op the moment telemetry is disabled, so a user who hasn&amp;rsquo;t opted
in gets a collector that silently discards everything. Informed consent, untouched.&lt;/p&gt;
&lt;p&gt;Observability gets a different door entirely.
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/observability.go#L47" target="_blank" rel="noopener"
 &gt;&lt;code&gt;Setup&lt;/code&gt;&lt;/a&gt;
builds whichever signals the operator has switched on, and it is gated only by
&lt;code&gt;telemetry.tracing.enabled&lt;/code&gt; and its siblings, which the operator sets. It never
consults the analytics opt-in. Turning on tracing doesn&amp;rsquo;t turn on analytics;
disabling analytics doesn&amp;rsquo;t silence tracing. The two enable flags live under the
same &lt;code&gt;telemetry.*&lt;/code&gt; config root, sit next to each other, and never read each
other.&lt;/p&gt;
&lt;p&gt;So that&amp;rsquo;s the whole architecture in a sentence: one package, one OTLP export core,
two consent models that share everything except the answer to &amp;ldquo;do we need to
ask&amp;rdquo;. The principle underneath, the one that decided every one of these calls, is
that the &lt;em&gt;kind of data&lt;/em&gt; sets the consent model. Usage data about a person needs
informed consent. Operational data about a service runs on implied consent. The
CLI and the web service are just where each kind tends to live.&lt;/p&gt;
&lt;h2 id="where-this-leaves-the-framework"&gt;Where this leaves the framework
&lt;/h2&gt;&lt;p&gt;0.7.x came out the other side with both telemetries: the one that asks first,
exactly as it was, and a new one that doesn&amp;rsquo;t, because it has nobody to ask. They
share an export core, a config root and a name, and they part company on the only
thing they were ever going to disagree about.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been careful here to describe how the two consent models are kept apart, not
to argue why they have to be. That argument, that &amp;ldquo;the kind of data decides
the consent model&amp;rdquo; is a line worth holding rather than a convenient bit of
engineering, is a piece of its own, and it&amp;rsquo;s the one I&amp;rsquo;m writing next.&lt;/p&gt;</description></item><item><title>Building a web service with go-tool-base, part 6: seeing what your service is doing</title><link>https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-6/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-6/</guid><description>&lt;img src="https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-6/cover-building-a-web-service-with-go-tool-base-part-6.png" alt="Featured image of post Building a web service with go-tool-base, part 6: seeing what your service is doing" /&gt;&lt;p&gt;On paper the macguffin service is finished. &lt;a class="link" href="https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-5/" &gt;Part
5&lt;/a&gt;
left it typed, fast, documented and served over TLS. So you deploy it, traffic
starts flowing, and a week later someone wanders over to say &amp;ldquo;it&amp;rsquo;s slow&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Slow how? Slow where? Which endpoint, the gateway or the gRPC behind it, the
store or the network in between? You open the logs and there&amp;rsquo;s a wall of &amp;ldquo;request
completed&amp;rdquo; lines, no way to tell which ones belong to the same request, and not a
word about the half-second one of them spent somewhere inside. The service is a
black box that happens to be on fire.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the gap this part closes. A long-running service needs to be observable:
you need to see what it&amp;rsquo;s doing, how often, how fast, and where the time actually
goes. And we get there without bolting on a separate stack, because go-tool-base
speaks OpenTelemetry, and three signals fall out of the same wiring.&lt;/p&gt;
&lt;p&gt;This part is written against &lt;strong&gt;go-tool-base v0.7.1&lt;/strong&gt;, the release that brings the
observability components in.&lt;/p&gt;
&lt;h2 id="the-three-things-you-actually-want-to-see"&gt;The three things you actually want to see
&lt;/h2&gt;&lt;p&gt;&amp;ldquo;Observability&amp;rdquo; is a grand word for three fairly down-to-earth questions. Logs
answer &lt;em&gt;what happened&lt;/em&gt; (you&amp;rsquo;ve had those since part 1). Metrics answer &lt;em&gt;how often,
and how fast&lt;/em&gt;: request rates, error counts, the spread of latencies. And traces
answer &lt;em&gt;where did the time go&lt;/em&gt;, by following a single request as it crosses from
one piece of your system to the next.&lt;/p&gt;
&lt;p&gt;For a service shaped like ours, that last one is the prize. A REST call hits the
gateway, turns into a gRPC call, runs through the store, and comes back. A trace
stitches that whole journey into one timeline, so &amp;ldquo;it&amp;rsquo;s slow&amp;rdquo; stops being a shrug
and becomes &amp;ldquo;it&amp;rsquo;s slow in CreateMacguffin, in the store, for 400ms&amp;rdquo;. You can&amp;rsquo;t
get there from a pile of disconnected log lines.&lt;/p&gt;
&lt;p&gt;OpenTelemetry (OTel, if you&amp;rsquo;ve seen it bandied about) is the vendor-neutral
standard for all three. You instrument once and ship the data to whatever backend
you fancy (Jaeger, Grafana, Honeycomb, a plain collector) over one wire protocol,
OTLP. go-tool-base does the instrumenting; you point it at a collector and pick
your poison.&lt;/p&gt;
&lt;h2 id="turn-it-on"&gt;Turn it on
&lt;/h2&gt;&lt;p&gt;One call wires the lot. In the &lt;code&gt;serve&lt;/code&gt; command from &lt;a class="link" href="https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-4/" &gt;part
4&lt;/a&gt;,
right after the controller exists:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gitlab.com/phpboyscout/go-tool-base/pkg/telemetry&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// Build whichever signals are enabled, install them, flush them on shutdown.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/telemetry/observability.go#L47" target="_blank" rel="noopener"
 &gt;&lt;code&gt;telemetry.Setup&lt;/code&gt;&lt;/a&gt;
reads your config, builds whichever of the three signals you&amp;rsquo;ve switched on, and
installs them as the process-wide OTel providers. It also hangs their shutdown on
the controller, so when a SIGTERM lands the buffered spans and metrics flush
before the process exits. That&amp;rsquo;s the same graceful-shutdown promise from part 1,
now extended to your telemetry: a clean stop drops nothing, not even the trace
that was mid-flight.&lt;/p&gt;
&lt;p&gt;On its own that line shows you nothing. A signal you haven&amp;rsquo;t enabled is skipped,
so an unconfigured service pays exactly nothing. You turn each on in config, which
we&amp;rsquo;ll get to. First, the instrumentation.&lt;/p&gt;
&lt;h2 id="wire-it-onto-the-transports"&gt;Wire it onto the transports
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Setup&lt;/code&gt; builds the pipeline; two short additions feed it. The gRPC server takes a
stats handler:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;grpcSrv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbgrpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;grpc&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbgrpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OTelStatsHandler&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;and the HTTP server takes a middleware. This is also where we finally meet
go-tool-base&amp;rsquo;s middleware chain, which we&amp;rsquo;ve managed without until now. A chain is
just an ordered stack of wrappers around your handler. We put the OTel one first
so it opens the span, and the request logger second, so it runs inside that span
and can see it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// pkg/cmd/serve/main.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewChain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OTelMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoggingMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;http&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;gtbhttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s the whole code change. Both helpers,
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/http/otel.go#L20" target="_blank" rel="noopener"
 &gt;&lt;code&gt;OTelMiddleware&lt;/code&gt;&lt;/a&gt;
and
&lt;a class="link" href="https://gitlab.com/phpboyscout/go-tool-base/-/blob/f627270/pkg/grpc/otel.go#L19" target="_blank" rel="noopener"
 &gt;&lt;code&gt;OTelStatsHandler&lt;/code&gt;&lt;/a&gt;,
lean on the standard OTel instrumentation libraries underneath (&lt;code&gt;otelhttp&lt;/code&gt; and
&lt;code&gt;otelgrpc&lt;/code&gt;), so the spans and metrics they emit follow the conventions every
backend already knows how to read. Every request now opens a span and records its
own duration, on both transports, and you wrote none of it per handler.&lt;/p&gt;
&lt;h2 id="go-deeper-than-the-plumbing"&gt;Go deeper than the plumbing
&lt;/h2&gt;&lt;p&gt;The transport spans show the request crossing HTTP and gRPC, but they stop at the
edge of your handler. To see where the time goes inside it, you open a span
yourself, and that needs no go-tool-base API at all, just OpenTelemetry. Here&amp;rsquo;s
the store doing it, opening a span around the work and tagging it with what it&amp;rsquo;s
handling:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// internal/macguffin/store.go&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;go.opentelemetry.io/otel&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;go.opentelemetry.io/otel/attribute&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;otel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffinsvc/store&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Macguffin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Store.Create&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;End&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin.name&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;		&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin.quantity&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="c1"&gt;// ... store the macguffin, generate its id ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;macguffin.id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;	&lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The span starts as a child of whatever is already in the context, which is the
gRPC server span the handler is running under, so it slots into the trace on its
own. The one thing to get right is the &lt;code&gt;context&lt;/code&gt;: give the store method a
&lt;code&gt;ctx context.Context&lt;/code&gt; first parameter and thread it down from the handler, and the
span finds its parent. Tag it with the things you&amp;rsquo;d want when something&amp;rsquo;s wrong,
the macguffin&amp;rsquo;s name and its id, and they show up on the span in the UI, right
next to the timing.&lt;/p&gt;
&lt;h2 id="point-it-at-a-collector"&gt;Point it at a collector
&lt;/h2&gt;&lt;p&gt;The wiring&amp;rsquo;s on; now tell it where to send. The config lives under one root:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# config.yaml&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;http://localhost:4318&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;insecure: true # plaintext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;a local collector only&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sampling&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1.0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# every trace while you&amp;#39;re looking&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;endpoint&lt;/code&gt; is your OTLP collector. Sampling defaults to a level-headed &lt;code&gt;0.1&lt;/code&gt; in
production (you rarely want every trace under real load), so turn it up to &lt;code&gt;1.0&lt;/code&gt;
while you&amp;rsquo;re developing or you&amp;rsquo;ll wonder where yours went. Stand up a collector (a
local Jaeger handles traces and gives you a UI to poke at), start the service, and
push some traffic through the REST side:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl https://localhost:8443/v1/macguffins
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;$ curl -X POST https://localhost:8443/v1/macguffins -d &lt;span class="s1"&gt;&amp;#39;{&amp;#34;name&amp;#34;:&amp;#34;the-grail&amp;#34;,&amp;#34;quantity&amp;#34;:1}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="one-request-the-whole-way-through"&gt;One request, the whole way through
&lt;/h2&gt;&lt;p&gt;Open the collector and there it is, the thing that justifies the exercise: a
single trace for that POST, four spans deep. The HTTP server span on top, the
gRPC call nested inside it, the gRPC handler inside that, and your &lt;code&gt;Store.Create&lt;/code&gt;
span at the bottom, with the macguffin&amp;rsquo;s name and id sat right on it. The REST
request became a gRPC call, crossed the gateway we built in part 4, ran the
handler and went into the store, and OTel followed it the entire way, because the
trace context travels with the request and everything reads from the same
providers. &amp;ldquo;It&amp;rsquo;s slow&amp;rdquo; finally has somewhere to point, and &amp;ldquo;slow where&amp;rdquo; has an
answer.&lt;/p&gt;
&lt;p&gt;&lt;img alt="A single trace in the collector UI: the HTTP span on top, the gRPC call nested inside it, the gRPC handler inside that, and the Store.Create span at the bottom carrying the macguffin’s name and id" class="gallery-image" data-flex-basis="506px" data-flex-grow="210" height="944" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-6/trace-in-jaeger_hu_ff3eed6a54fc0c0.webp" srcset="https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-6/trace-in-jaeger_hu_e9a697a228999d4d.webp 480w, https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-6/trace-in-jaeger_hu_9f41983462809d52.webp 720w, https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-6/trace-in-jaeger_hu_e92993a221f7a357.webp 1080w, https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-6/trace-in-jaeger_hu_e68956e0505f7362.webp 1440w, https://phpboyscout.uk/building-a-web-service-with-go-tool-base-part-6/trace-in-jaeger_hu_ff3eed6a54fc0c0.webp 1991w" width="1991"&gt;
&lt;/p&gt;
&lt;p&gt;The metrics ride the same pipeline: request counts, error counts, a latency
histogram broken down by route and method. And your logs get an upgrade you didn&amp;rsquo;t
ask for. Every &amp;ldquo;request completed&amp;rdquo; line now carries the &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt;
of the request it describes, so a slow trace in the UI and its log line in your
terminal share an id, and you can jump straight from one to the other. That
correlation lands in your stderr logs and in the records shipped to the collector,
both.&lt;/p&gt;
&lt;h2 id="a-word-on-the-other-telemetry"&gt;A word on the other telemetry
&lt;/h2&gt;&lt;p&gt;If you read &lt;a class="link" href="https://phpboyscout.uk/telemetry-that-asks-first/" &gt;Telemetry that asks
first&lt;/a&gt;, you&amp;rsquo;ll know
go-tool-base has a telemetry package for product analytics too: the opt-in kind
that asks a user&amp;rsquo;s permission before it phones anything home. This is not that,
and the difference is the whole point.&lt;/p&gt;
&lt;p&gt;That analytics is about a user, and it&amp;rsquo;s gated behind informed consent: off until
they say yes. This is about your service, emitted by you, to a collector you run,
and it works on implied consent. You switched it on; there&amp;rsquo;s no end user in the
loop to ask. Same package, deliberately separate gates, and never the twain shall
meet. There&amp;rsquo;s a fuller post in that one distinction, and it&amp;rsquo;s the next thing I&amp;rsquo;m
writing.&lt;/p&gt;
&lt;h2 id="where-that-leaves-the-service"&gt;Where that leaves the service
&lt;/h2&gt;&lt;p&gt;Six parts in, the macguffin service starts cleanly, reports its health, serves
gRPC and REST and interactive docs over TLS, shuts down without dropping a
request, and now tells you what it&amp;rsquo;s doing while it runs, all from one annotated
proto and a controller it registers against. Observability was the last missing
sense. The thing was fast and correct before, but you couldn&amp;rsquo;t see inside it.
Now you can, and it cost you about a dozen lines.&lt;/p&gt;</description></item></channel></rss>