<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Observability | Law Zava</title><link>https://lawzava.com/topics/observability/</link><description>Tracing, logging, and instrumentation aimed at answering questions you did not anticipate.</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Thu, 13 Aug 2026 07:51:57 +0000</lastBuildDate><atom:link href="https://lawzava.com/topics/observability/index.xml" rel="self" type="application/rss+xml"/><item><title>Testing AI Where It Actually Runs</title><link>https://lawzava.com/blog/2025-04-14-ai-testing-production/</link><pubDate>Mon, 14 Apr 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-04-14-ai-testing-production/</guid><description>Offline evals are necessary but not sufficient. Here&amp;amp;rsquo;s how I test AI features in production with shadow mode, canaries, and rollback automation &amp;amp;ndash; with Go code.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Your eval suite passes. Your staging environment looks good. Your AI feature will still break in production because real users do things your test set never imagined. Shadow it, canary it, measure it, and make every rollout reversible. Evidence before confidence.</p>
<hr>
<p>I wrote about  <a href="/blog/2019-06-03-testing-in-production/"
   
   >testing in production</a>
 back in 2019. The core thesis hasn&rsquo;t changed: staging lies to you. What has changed is that AI makes the lying worse.</p>
<p>Traditional software either works or it doesn&rsquo;t. The test passes or fails. The API returns the right data or throws an error. AI features exist in a gray zone where the output is almost always plausible, sometimes correct, and occasionally dangerous. Your test suite can&rsquo;t cover this space. Production can.</p>
<h2 id="why-offline-evals-arent-enough">Why offline evals aren&rsquo;t enough</h2>
<p>Every AI project should have an  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >eval suite</a>
. I&rsquo;ve been saying this for over a year. But evals test known scenarios. Production surfaces the unknown ones.</p>
<p>Real users send inputs your test set never imagined. They misspell things. They paste in multi-language text. They include personally identifiable information that triggers different model behavior. They ask questions that are ambiguous in ways your eval prompts aren&rsquo;t.</p>
<p>At one company, their AI support agent passed every eval with flying colors. In production, users started treating it like a search engine &ndash; pasting in order numbers and expecting it to look up status. The model happily hallucinated order details instead of saying &ldquo;I can&rsquo;t do that.&rdquo; The eval suite had no test case for &ldquo;user treats chatbot like a database query tool.&rdquo; Production found it in the first hour.</p>
<h2 id="shadow-mode-first">Shadow mode first</h2>
<p>Before any AI change touches a real user, shadow it. Run the new version in parallel with the current one, compare outputs, and log everything. The user only sees the current version.</p>
<p>Here&rsquo;s the pattern I use in Go:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">ShadowRunner</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">current</span>   <span style="color:#a6e22e">ModelClient</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">candidate</span> <span style="color:#a6e22e">ModelClient</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">logger</span>    <span style="color:#f92672">*</span><span style="color:#a6e22e">ShadowLogger</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ShadowRunner</span>) <span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Current model serves the user</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">current</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#75715e">// Candidate runs in background -- never blocks the user</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">candidateCtx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Background</span>(), <span style="color:#ae81ff">30</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">candidateResp</span>, <span style="color:#a6e22e">candidateErr</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">candidate</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">candidateCtx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">logger</span>.<span style="color:#a6e22e">LogComparison</span>(<span style="color:#a6e22e">ShadowResult</span>{
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">RequestID</span>:      <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">ID</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">CurrentOutput</span>:  <span style="color:#a6e22e">resp</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">CandidateOutput</span>: <span style="color:#a6e22e">candidateResp</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">CandidateErr</span>:   <span style="color:#a6e22e">candidateErr</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">Match</span>:          <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">compareOutputs</span>(<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">candidateResp</span>),
</span></span><span style="display:flex;"><span>		})
</span></span><span style="display:flex;"><span>	}()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The shadow logger captures every comparison. I review divergences daily during the shadow period. If the candidate produces different outputs, I want to understand whether those differences are improvements, regressions, or neutral changes.</p>
<p>The shadow period should last at least a week. Longer for high-traffic services. The goal is to see enough real-world input diversity to have confidence in the change.</p>
<h2 id="canary-with-kill-switches">Canary with kill switches</h2>
<p>Once shadow results look good, move to a  <a href="/blog/2021-02-08-gitops-progressive-delivery/"
   
   >canary deployment</a>
. Route a small percentage of real traffic to the new version and  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >monitor closely</a>
.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">CanaryRouter</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">current</span>     <span style="color:#a6e22e">ModelClient</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">candidate</span>   <span style="color:#a6e22e">ModelClient</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">percentage</span>  <span style="color:#a6e22e">atomic</span>.<span style="color:#a6e22e">Int32</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">qualityGate</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">QualityGate</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CanaryRouter</span>) <span style="color:#a6e22e">Route</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">shouldCanary</span>(<span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">UserID</span>) {
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">candidate</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">||</span> !<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">qualityGate</span>.<span style="color:#a6e22e">Check</span>(<span style="color:#a6e22e">resp</span>) {
</span></span><span style="display:flex;"><span>			<span style="color:#75715e">// Automatic fallback to current</span>
</span></span><span style="display:flex;"><span>			<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">current</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">current</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">c</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">CanaryRouter</span>) <span style="color:#a6e22e">shouldCanary</span>(<span style="color:#a6e22e">userID</span> <span style="color:#66d9ef">string</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">hash</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fnv</span>.<span style="color:#a6e22e">New32a</span>()
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">hash</span>.<span style="color:#a6e22e">Write</span>([]byte(<span style="color:#a6e22e">userID</span>))
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> int(<span style="color:#a6e22e">hash</span>.<span style="color:#a6e22e">Sum32</span>()<span style="color:#f92672">%</span><span style="color:#ae81ff">100</span>) &lt; int(<span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">percentage</span>.<span style="color:#a6e22e">Load</span>())
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>QualityGate</code> is the part most teams skip. It checks the candidate response against basic quality criteria before serving it. If the response fails the gate, the user gets the current version transparently. No harm done.</p>
<p>I start at 1%. Watch for a day. If quality signals hold, move to 5%. Then 25%. Then 100%. Each step gets at least a few hours of observation. If anything looks off at any step, roll back to the previous percentage. No drama.</p>
<p>The hash-based routing is important: the same user always gets the same version within a rollout step. This prevents confusing experiences where the same user gets different quality outputs on consecutive requests.</p>
<h2 id="what-to-measure-during-rollout">What to measure during rollout</h2>
<p>Three categories of signals, checked at every rollout step:</p>
<p><strong>Quality signals.</strong> Task success rate on your eval set. But also: user re-prompts (did they have to ask again?), abandonment rate (did they give up?), explicit negative feedback. These are the signals your eval suite can&rsquo;t give you.</p>
<p><strong>Safety signals.</strong> Refusal rate. Policy trigger count. Anything flagged by your content filters. If the candidate model refuses more or fewer requests than the current one, investigate before expanding.</p>
<p><strong>Operational signals.</strong> Latency p50 and p95 by workflow. Token usage. Cost per request. Error rates. A model change that improves quality but doubles cost might not be a net win. Make that trade-off explicit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">RolloutMetrics</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Version</span>         <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">QualityScore</span>    <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">RefusalRate</span>     <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">P50Latency</span>      <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">P95Latency</span>      <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">CostPerRequest</span>  <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ErrorRate</span>       <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">UserRepromptRate</span> <span style="color:#66d9ef">float64</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">m</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">RolloutMetrics</span>) <span style="color:#a6e22e">PassesGate</span>(<span style="color:#a6e22e">baseline</span> <span style="color:#a6e22e">RolloutMetrics</span>) <span style="color:#66d9ef">bool</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">QualityScore</span> &lt; <span style="color:#a6e22e">baseline</span>.<span style="color:#a6e22e">QualityScore</span><span style="color:#f92672">*</span><span style="color:#ae81ff">0.95</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span> <span style="color:#75715e">// quality regression &gt; 5%</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">ErrorRate</span> &gt; <span style="color:#a6e22e">baseline</span>.<span style="color:#a6e22e">ErrorRate</span><span style="color:#f92672">*</span><span style="color:#ae81ff">1.5</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span> <span style="color:#75715e">// error rate increase &gt; 50%</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">m</span>.<span style="color:#a6e22e">P95Latency</span> &gt; <span style="color:#a6e22e">baseline</span>.<span style="color:#a6e22e">P95Latency</span><span style="color:#f92672">*</span><span style="color:#ae81ff">2</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">false</span> <span style="color:#75715e">// latency doubled</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>These thresholds aren&rsquo;t magic numbers. They&rsquo;re product decisions. A 5% quality regression might be acceptable if cost drops by 40%. A latency doubling might be fine for a background task but fatal for a chat interface. Define them before the rollout starts, not during.</p>
<h2 id="the-one-change-rule">The one-change rule</h2>
<p>Never change the model and the prompt at the same time. If quality drops, you won&rsquo;t know which change caused it. This sounds obvious. I&rsquo;ve watched four different teams make this mistake in the last three months.</p>
<p>Ship the prompt change. Measure. Ship the model change. Measure. If you must change both, do the prompt first because it&rsquo;s cheaper to roll back.</p>
<p>Same goes for retrieval changes, system message changes, and tool configuration changes. One variable at a time. Anything else is debugging in the dark.</p>
<h2 id="holdout-baselines">Holdout baselines</h2>
<p>Keep a small, stable slice of traffic permanently on a known-good version. This is your holdout. It tells you whether quality changes are due to your changes or due to shifts in user behavior, input distribution, or upstream data.</p>
<p>Without a holdout, slow regressions look like normal variance. You won&rsquo;t notice a 2% quality drop per week because no individual week looks bad. But your holdout will show the cumulative drift loud and clear.</p>
<h2 id="what-matters">What matters</h2>
<p>Testing AI in production isn&rsquo;t reckless. Shipping AI without testing it in production is reckless. Offline evals give you a baseline. Shadow mode gives you confidence. Canaries give you safety. Holdouts give you ground truth.</p>
<p>Every rollout should be reversible, measurable, and attributable to a single change. That isn&rsquo;t a testing philosophy. That&rsquo;s  <a href="/blog/2024-01-08-ai-engineering-discipline/"
   
   >engineering discipline</a>
 applied to a system that fails in ways your test suite can&rsquo;t anticipate.</p>
]]></content:encoded></item><item><title>Your AI System Looks Healthy. It Is Not.</title><link>https://lawzava.com/blog/2025-03-31-ai-observability-deep/</link><pubDate>Mon, 31 Mar 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-03-31-ai-observability-deep/</guid><description>Traditional monitoring will tell you your AI service is up. It won&amp;amp;rsquo;t tell you it&amp;amp;rsquo;s returning confident garbage. Here&amp;amp;rsquo;s what observability actually looks like for AI.</description><content:encoded><![CDATA[<p>Here&rsquo;s a scenario I&rsquo;ve seen three times this year.</p>
<p>An AI-powered feature is in production. Uptime: 99.9%. Latency: nominal. Error rate: near zero. Dashboards are green. Everyone is happy.</p>
<p>Except the answers are wrong 15% of the time, and nobody knows because nothing is measuring answer quality. The system is healthy. The outputs are not.</p>
<p>This is the fundamental gap in  <a href="/blog/2023-08-21-llm-observability/"
   
   >AI observability</a>
.  <a href="/blog/2017-03-20-why-observability-matters-more-than-monitoring/"
   
   >Traditional monitoring</a>
 tells you whether the service is running. It does not tell you whether the service is useful.</p>
<h2 id="why-ai-systems-fail-silently">Why AI systems fail silently</h2>
<p>A classic API returns structured data. If the response is malformed, you get a parse error. If the logic is wrong, a test catches it. The failure modes are usually loud and obvious.</p>
<p>AI systems fail quietly. The model returns a perfectly formatted response with a confident tone and completely wrong content. The HTTP status is 200. The latency is fine. The JSON is valid. And the user just got told that their refund was processed when it wasn&rsquo;t.</p>
<p>At a fintech startup, we had a similar problem with our financial news summarization pipeline, long before the current AI wave. The summaries looked plausible but occasionally attributed quotes to the wrong CEO or mixed up fiscal quarters. The system was &ldquo;working&rdquo; by every operational metric. The outputs were unreliable. We caught it only because a user complained, not because monitoring flagged it.</p>
<p>The lesson stuck with me. You can&rsquo;t monitor AI like you monitor a REST API. You need different signals.</p>
<h2 id="the-signals-that-actually-matter">The signals that actually matter</h2>
<p>I use a simple framework with five categories. If you are not tracking all five, you have blind spots.</p>
<p><strong>Traceability.</strong> For every response, you need to know: which model, which prompt version, which retrieved context, which tool calls. If you can&rsquo;t reconstruct why the model said what it said, you can&rsquo;t debug a bad answer. You&rsquo;re just guessing. I store a trace object alongside every response that includes model ID, prompt hash, retrieval IDs, and tool call logs. When something goes wrong, the trace is the first thing I pull.</p>
<p><strong>Quality signals.</strong> This is the hard one. You need some measure of whether the output was good. Heuristic checks catch obvious failures: empty responses, responses that are too long or too short, and responses that contain known-bad patterns. Sampled evaluation catches the subtle failures: a human or a second model scores a random slice of outputs against a rubric. Neither is perfect. Together they cover enough ground.</p>
<p><strong>Cost per outcome.</strong> Not cost per request, cost per successful outcome. A system that gets it right on the first try costs less than one that needs three retries and a human escalation. Track the full cost of getting to a good answer, including retries, fallbacks, and human review. This number will surprise you.</p>
<p><strong>Safety and policy.</strong> Refusal rates, blocked content, policy trigger counts. If your refusal rate spikes, something changed &ndash; either the inputs or the model behavior. If it drops to zero, something might be wrong too. These are canary signals.</p>
<p><strong>Operational basics.</strong> Latency percentiles by workflow (not globally &ndash; global averages hide everything), error rates with reason codes, token usage trends. The same stuff you track for any API, but broken down by the AI-specific dimensions that matter.</p>
<h2 id="the-prompt-versioning-problem">The prompt versioning problem</h2>
<p>Here is something that bites almost every team. Someone changes a prompt. Quality drops. Nobody connects the two events because the prompt change was not tracked alongside the quality metrics.</p>
<p>Treat prompts as production code. Version them. Deploy them through your normal release process. Tag every response with the prompt version that produced it. When quality dips, the first question should be: what changed since the last known-good state?</p>
<p>I version prompts in the same repo as the service code. A prompt change gets a PR, a review, and a run against  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >the eval suite</a>
 before it hits production. It sounds like overkill until the first time it prevents a regression. Then it sounds obvious.</p>
<h2 id="keep-it-lean">Keep it lean</h2>
<p>The temptation is to build a dashboard for everything. Do not. Start with the minimum set of signals that lets you answer one question: &ldquo;A user reported a bad answer. Can I explain why it happened and prevent it from happening again?&rdquo;</p>
<p>If you can answer that question end-to-end, your observability is good enough. If you can&rsquo;t, no amount of dashboards will save you.</p>
<p>Log the trace. Track quality. Version your prompts. Measure cost per outcome, not cost per request. That&rsquo;s the baseline. Everything else is optimization.</p>
]]></content:encoded></item><item><title>LLM Observability: Your Existing Monitoring Is Not Enough</title><link>https://lawzava.com/blog/2023-08-21-llm-observability/</link><pubDate>Mon, 21 Aug 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-08-21-llm-observability/</guid><description>Traditional monitoring says the service is up. It won&amp;amp;rsquo;t tell you the model started returning garbage last Tuesday. How to actually observe LLM systems.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Treat LLM calls like you treat database calls in OpenTelemetry: trace them, measure them, and alert on quality drift &ndash; not just errors. Your Grafana dashboard showing 200 OK and low latency means nothing if the model is hallucinating.</p>
<p>Two weeks after we shipped the transaction categorization feature at a fintech company, OpenAI quietly changed something in their model inference. Our API calls still returned 200. Latency was normal. The logs looked clean. But accuracy dropped from 89% to 86% overnight, and the confidence score distribution shifted in a way that pushed more borderline results above our threshold.</p>
<p>We caught it because we had quality monitoring in place. If we&rsquo;d been relying on traditional application monitoring &ndash; error rates, latency percentiles, throughput &ndash; we&rsquo;d have missed it entirely. The system was &ldquo;healthy&rdquo; by every standard metric. It was just less correct.</p>
<p>This is the fundamental problem with LLM observability. The output can be fluent, well-formatted, and completely wrong. Your existing monitoring stack doesn&rsquo;t know the difference.</p>
<h2 id="what-llm-observability-actually-means">What LLM observability actually means</h2>
<p>For traditional services, observability is the RED metrics: Rate, Errors, Duration. For LLMs, you need those plus an entire quality dimension that doesn&rsquo;t exist in normal software.</p>
<p>I think about it in three layers:</p>
<p><strong>Infrastructure layer.</strong> The basics. API availability, latency (p50, p95, p99), error rates by type (rate limits, timeouts, 500s), token usage, cost. If you&rsquo;re using OpenTelemetry &ndash; and you should be &ndash; this maps cleanly onto standard span attributes.</p>
<p><strong>Model behavior layer.</strong> This is the new part. Output quality, confidence distributions, format compliance, fallback rates, and drift detection. These signals require domain-specific instrumentation, not just HTTP metrics.</p>
<p><strong>Product outcome layer.</strong> User actions on model output: acceptance, edits, rejections, re-prompts. This is where you learn whether the model is actually helping or just producing plausible noise.</p>
<h2 id="how-we-instrumented-it">How we instrumented it</h2>
<p>I&rsquo;m a big OpenTelemetry advocate. We use it at a fintech company for everything, and extending it to LLM calls was a natural fit. The key insight: treat each LLM call as a span in your trace, with custom attributes for the things you need to measure.</p>
<p>Here&rsquo;s the shape of what we log for every LLM call:</p>
<pre tabindex="0"><code>span.name: &#34;llm.completion&#34;
span.attributes:
  llm.model: &#34;gpt-3.5-turbo-0613&#34;
  llm.prompt_template: &#34;tx-categorize-v3&#34;
  llm.prompt_tokens: 847
  llm.completion_tokens: 124
  llm.total_tokens: 971
  llm.temperature: 0.0
  llm.confidence_score: 0.91
  llm.output_valid: true
  llm.fallback_used: false
  llm.cost_usd: 0.0015
</code></pre><p>The prompt template version is critical. Without it, you can&rsquo;t correlate quality changes with prompt changes. We version our prompts like we version our API: <code>tx-categorize-v3</code> tells us exactly which instructions the model received.</p>
<p>We also log a hashed version of the input (for cardinality analysis) and the full output (for sampling and review). The full input gets logged to a separate, access-controlled store because transaction data is sensitive. Don&rsquo;t dump PII into your standard telemetry pipeline. I&rsquo;ve seen teams do this. Don&rsquo;t be that team.</p>
<h2 id="the-quality-metrics-that-actually-matter">The quality metrics that actually matter</h2>
<p>After running this for a couple of months, here&rsquo;s what I actually look at:</p>
<p><strong>Accuracy on the eval set.</strong> We run our 200-transaction eval set daily against the live model. This is the canary. If this number drops, something changed &ndash; either in our code, our prompts, or the model itself. We alert at a 2-point drop sustained for two consecutive runs.</p>
<p><strong>Confidence score distribution.</strong> Not the average &ndash; the distribution. A shift in the shape of the histogram tells you more than the mean. When the model gets less confident on average, it means the input distribution has changed or the model itself has drifted.</p>
<p><strong>Fallback rate.</strong> What percentage of requests hit our rules-based fallback instead of using the model output? This is a compound signal: it reflects both model quality and input quality. A spike in fallback rate is always worth investigating.</p>
<p><strong>User correction rate.</strong> How often do users change the model&rsquo;s output? This is the ground truth. Automated metrics are approximations. User corrections are direct feedback. We track this weekly and use it to update our eval set.</p>
<p><strong>Cost per successful categorization.</strong> Not cost per API call. Cost per result that the user accepted without correction. This metric keeps us honest about whether quality improvements are actually cost-effective.</p>
<h2 id="dashboards-for-different-audiences">Dashboards for different audiences</h2>
<p>I built three dashboards. Each one serves a different question.</p>
<p><strong>The ops dashboard</strong> shows error rates, latency, rate limit hits, and cost. This is for on-call. It answers: &ldquo;Is the LLM integration working?&rdquo; Standard SRE stuff, just with LLM-specific dimensions.</p>
<p><strong>The quality dashboard</strong> shows accuracy trends, confidence distributions, fallback rates, and eval set results. This is for the engineering team. It answers: &ldquo;Is the model producing good results?&rdquo; This is the one we check every morning.</p>
<p><strong>The product dashboard</strong> shows user correction rates, acceptance rates, and task completion metrics. This is for the PM. It answers: &ldquo;Is this feature helping users?&rdquo; This is the one that justifies the feature&rsquo;s existence.</p>
<p>The temptation is to build one mega-dashboard. Resist it. Different audiences need different views, and combining them creates noise that ensures nobody looks at any of it.</p>
<h2 id="alerting-without-drowning">Alerting without drowning</h2>
<p>The biggest mistake I see: alerting on every metric at static thresholds. LLM behavior is inherently variable. You&rsquo;ll page yourself into exhaustion.</p>
<p>What works better: alert on sustained deviations from a rolling baseline. Our eval accuracy fluctuates between 87% and 91% day to day. That&rsquo;s normal. An alert at 85% absolute threshold would fire once and be ignored. An alert on &ldquo;3+ consecutive days below the 7-day moving average minus 2 points&rdquo; catches real regressions while ignoring noise.</p>
<p>For cost, we alert on day-over-day percentage increases above 50%. This catches both sudden spikes (runaway retry loops) and gradual creep (prompt bloat) depending on the time window.</p>
<h2 id="the-honest-summary">The honest summary</h2>
<p>LLM observability isn&rsquo;t a new discipline. It&rsquo;s regular observability plus quality measurement. If you&rsquo;re already doing OpenTelemetry tracing, adding LLM spans is straightforward. If you&rsquo;re already running eval sets, automating them on a daily cadence is a small step.</p>
<p>The hard part isn&rsquo;t the tooling. The hard part is accepting that &ldquo;the service is up&rdquo; isn&rsquo;t the same as &ldquo;the service is working.&rdquo; For LLMs, the gap between those two statements is where all the interesting failures live.</p>
<p>Monitor the quality. Version your prompts. Run your eval set daily. Everything else is optimization.</p>
]]></content:encoded></item><item><title>OpenTelemetry in Late 2021: What's Ready and What's Not</title><link>https://lawzava.com/blog/2021-11-15-opentelemetry-adoption/</link><pubDate>Mon, 15 Nov 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-11-15-opentelemetry-adoption/</guid><description>Tracing is ready. Metrics are getting there. Logs are not. Here&amp;amp;rsquo;s a practical adoption path and the code to back it up.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>OpenTelemetry tracing hit 1.0 this year and it&rsquo;s the real deal. Adopt it now for tracing. Be cautious with metrics &ndash; the API isn&rsquo;t finalized. Ignore logs for now. Deploy the Collector as your telemetry gateway, standardize your resource attributes on day one, and configure sampling before you drown in data. The vendor lock-in argument alone makes this worth the migration effort.</p>
<hr>
<p>I&rsquo;m tired of observability vendor lock-in. Every organization I work with has a different combination of Datadog, New Relic, Jaeger, Prometheus, and three logging tools. Switching backends means rewriting instrumentation across dozens of services. Correlating traces with metrics requires duct tape and prayer.</p>
<p>OpenTelemetry fixes this. One instrumentation standard, any backend. It&rsquo;s the most important infrastructure project nobody is talking about enough.</p>
<h2 id="what-is-actually-stable-right-now">What Is Actually Stable Right Now</h2>
<p>As of November 2021, here is the honest state:</p>
<p><strong>Tracing: production-ready.</strong> The API and SDKs hit 1.0. Go, Java, Python, JavaScript, .NET all have stable implementations. Context propagation works across HTTP,  <a href="/blog/2020-05-11-grpc-best-practices/"
   
   >gRPC</a>
, and most messaging systems. This is safe to adopt today.</p>
<p><strong>Metrics: getting there.</strong> The metrics API isn&rsquo;t finalized. SDKs are in various stages of beta. You can start experimenting but I wouldn&rsquo;t bet a production monitoring pipeline on it yet. Give it six months.</p>
<p><strong>Logs: early.</strong> On the roadmap. Not a reason to adopt OTel today. Keep your existing log pipeline.</p>
<p>This maturity gap matters for planning. Don&rsquo;t try to adopt all three signals at once. Start with tracing, add metrics when the API stabilizes, leave logs alone.</p>
<h2 id="start-with-the-collector">Start With the Collector</h2>
<p>The single best decision you can make is deploying the OpenTelemetry Collector before you instrument a single service. The Collector sits between your applications and your backends. Applications export to it via OTLP. It forwards to whatever backend you use.</p>
<p>Why this matters: when you inevitably switch observability vendors (and you&rsquo;ll), you change the Collector config. Not your application code. Not a hundred services. One config file.</p>
<p>A basic Collector config for forwarding traces to Jaeger:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">receivers</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">otlp</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">protocols</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">grpc</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">endpoint</span>: <span style="color:#ae81ff">0.0.0.0</span>:<span style="color:#ae81ff">4317</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">http</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">endpoint</span>: <span style="color:#ae81ff">0.0.0.0</span>:<span style="color:#ae81ff">4318</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">processors</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">batch</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">5s</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">send_batch_size</span>: <span style="color:#ae81ff">1024</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">exporters</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">jaeger</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">endpoint</span>: <span style="color:#ae81ff">jaeger-collector:14250</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">tls</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">insecure</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">service</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">pipelines</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">traces</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">receivers</span>: [<span style="color:#ae81ff">otlp]</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">processors</span>: [<span style="color:#ae81ff">batch]</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">exporters</span>: [<span style="color:#ae81ff">jaeger]</span>
</span></span></code></pre></div><p>Deploy this as a sidecar or a standalone service. I prefer standalone in most cases &ndash; easier to manage, easier to scale, and you avoid coupling the Collector lifecycle to your application pods.</p>
<h2 id="instrumenting-a-go-service">Instrumenting a Go Service</h2>
<p>Here is what basic OTel tracing looks like in Go. I use this as my starting template for new projects.</p>
<p>Set up the trace provider at application startup:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">initTracer</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">sdktrace</span>.<span style="color:#a6e22e">TracerProvider</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">exporter</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">otlptracegrpc</span>.<span style="color:#a6e22e">New</span>(<span style="color:#a6e22e">ctx</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">otlptracegrpc</span>.<span style="color:#a6e22e">WithEndpoint</span>(<span style="color:#e6db74">&#34;otel-collector:4317&#34;</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">otlptracegrpc</span>.<span style="color:#a6e22e">WithInsecure</span>(),
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;creating OTLP exporter: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tp</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">sdktrace</span>.<span style="color:#a6e22e">NewTracerProvider</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">sdktrace</span>.<span style="color:#a6e22e">WithBatcher</span>(<span style="color:#a6e22e">exporter</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">sdktrace</span>.<span style="color:#a6e22e">WithResource</span>(<span style="color:#a6e22e">resource</span>.<span style="color:#a6e22e">NewWithAttributes</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">semconv</span>.<span style="color:#a6e22e">SchemaURL</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">semconv</span>.<span style="color:#a6e22e">ServiceNameKey</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;orders-api&#34;</span>),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">semconv</span>.<span style="color:#a6e22e">ServiceVersionKey</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;1.4.2&#34;</span>),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">attribute</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;environment&#34;</span>, <span style="color:#e6db74">&#34;production&#34;</span>),
</span></span><span style="display:flex;"><span>        )),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">sdktrace</span>.<span style="color:#a6e22e">WithSampler</span>(
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">sdktrace</span>.<span style="color:#a6e22e">ParentBased</span>(<span style="color:#a6e22e">sdktrace</span>.<span style="color:#a6e22e">TraceIDRatioBased</span>(<span style="color:#ae81ff">0.1</span>)),
</span></span><span style="display:flex;"><span>        ),
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">otel</span>.<span style="color:#a6e22e">SetTracerProvider</span>(<span style="color:#a6e22e">tp</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">otel</span>.<span style="color:#a6e22e">SetTextMapPropagator</span>(<span style="color:#a6e22e">propagation</span>.<span style="color:#a6e22e">TraceContext</span>{})
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">tp</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then instrument your HTTP handlers:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">handleOrder</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">span</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">otel</span>.<span style="color:#a6e22e">Tracer</span>(<span style="color:#e6db74">&#34;orders-api&#34;</span>).<span style="color:#a6e22e">Start</span>(<span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Context</span>(), <span style="color:#e6db74">&#34;handleOrder&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">span</span>.<span style="color:#a6e22e">End</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">span</span>.<span style="color:#a6e22e">SetAttributes</span>(
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">attribute</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;order.customer_id&#34;</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Header</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#e6db74">&#34;X-Customer-ID&#34;</span>)),
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">order</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">processOrder</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">r</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">span</span>.<span style="color:#a6e22e">RecordError</span>(<span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">span</span>.<span style="color:#a6e22e">SetStatus</span>(<span style="color:#a6e22e">codes</span>.<span style="color:#a6e22e">Error</span>, <span style="color:#a6e22e">err</span>.<span style="color:#a6e22e">Error</span>())
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Error</span>(<span style="color:#a6e22e">w</span>, <span style="color:#e6db74">&#34;order processing failed&#34;</span>, <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">StatusInternalServerError</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">span</span>.<span style="color:#a6e22e">SetAttributes</span>(<span style="color:#a6e22e">attribute</span>.<span style="color:#a6e22e">String</span>(<span style="color:#e6db74">&#34;order.id&#34;</span>, <span style="color:#a6e22e">order</span>.<span style="color:#a6e22e">ID</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">NewEncoder</span>(<span style="color:#a6e22e">w</span>).<span style="color:#a6e22e">Encode</span>(<span style="color:#a6e22e">order</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>ctx</code> parameter carries trace context through your call stack. Pass it everywhere. Every downstream call that receives this context becomes part of the same trace. This is how you get  <a href="/blog/2018-07-09-observability-beyond-monitoring/"
   
   >end-to-end visibility across services</a>
.</p>
<h2 id="sampling-configure-this-on-day-one">Sampling: Configure This on Day One</h2>
<p>I can&rsquo;t stress this enough. Default sampling is 100% &ndash; every request gets traced. That&rsquo;s fine for development. In production with any real traffic, you&rsquo;ll generate terabytes of trace data and your observability bill will make your CFO cry.</p>
<p>Set up parent-based sampling with a ratio. 10% is a good starting point for most services. Critical paths can be sampled at higher rates.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>OTEL_TRACES_SAMPLER<span style="color:#f92672">=</span>parentbased_traceidratio
</span></span><span style="display:flex;"><span>OTEL_TRACES_SAMPLER_ARG<span style="color:#f92672">=</span>0.1
</span></span></code></pre></div><p>Parent-based means if an upstream service already decided to sample this request, downstream services honor that decision. This keeps traces complete instead of fragmented.</p>
<h2 id="resource-attributes-the-thing-everyone-gets-wrong">Resource Attributes: The Thing Everyone Gets Wrong</h2>
<p>Resource attributes are metadata attached to every span your service produces. Service name, environment, version. They&rsquo;re how you filter and correlate traces across your entire fleet.</p>
<p>Get these right from day one. I&rsquo;ve seen migrations stall for months because teams used inconsistent service names and nobody could query across services.</p>
<p>Standardize at minimum:</p>
<ul>
<li><code>service.name</code> &ndash; unique, lowercase, hyphenated</li>
<li><code>service.version</code> &ndash; semver, from your build</li>
<li><code>deployment.environment</code> &ndash; production, staging, development</li>
<li><code>service.namespace</code> &ndash; team or domain grouping</li>
</ul>
<p>Enforce these through the Collector. Use the <code>resource</code> processor to inject defaults and reject spans missing required attributes.</p>
<h2 id="the-migration-path">The Migration Path</h2>
<p>Don&rsquo;t try to migrate everything at once. This is the sequence I follow:</p>
<p><strong>Week 1-2:</strong> Deploy the Collector. Configure it to export to your current backend. No application changes yet.</p>
<p><strong>Week 3-4:</strong> Instrument one new service (or a non-critical existing one) with OTel. Verify traces show up in your backend. Fix any context propagation gaps at service boundaries.</p>
<p><strong>Month 2:</strong> Migrate 2-3 critical services from vendor SDKs to OTel. Run both in parallel for a week to verify data parity.</p>
<p><strong>Month 3+:</strong> Expand coverage. Set up dashboards and alerts on the new pipeline. Once confident, remove vendor SDK instrumentation.</p>
<p>This sequence keeps your existing observability intact while you build confidence in the new pipeline. Nobody loses visibility during the migration.</p>
<h2 id="pitfalls-ive-hit">Pitfalls I&rsquo;ve Hit</h2>
<p><strong>Inconsistent service naming.</strong> One team calls it <code>orders-api</code>, another calls it <code>OrdersAPI</code>, a third calls it <code>orders</code>. Now you can&rsquo;t query across services. Solve this with a naming convention doc and Collector-level enforcement.</p>
<p><strong>Missing context propagation at  <a href="/blog/2019-09-09-message-queues-patterns/"
   
   >message queues</a>
.</strong> HTTP propagation works out of the box. Kafka, RabbitMQ, SQS &ndash; you need to manually inject and extract trace context from message headers. If you skip this, your traces end at the queue boundary and you lose visibility into async processing.</p>
<p><strong>High-cardinality attributes.</strong> Putting user IDs, request IDs, or full URLs as span attributes sounds useful until your trace backend is indexing millions of unique values and your storage costs explode. Use low-cardinality attributes for filtering. Put high-cardinality data in span events or logs.</p>
<p>OpenTelemetry is the right bet for 2021 and beyond. The tracing story is solid. The Collector architecture is sound. Adopt it incrementally, get your conventions right early, and you&rsquo;ll never have to rewrite instrumentation for a vendor switch again.</p>
]]></content:encoded></item><item><title>Observability-Driven Development Is Just Instrumenting Your Code</title><link>https://lawzava.com/blog/2021-06-14-observability-driven-development/</link><pubDate>Mon, 14 Jun 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-06-14-observability-driven-development/</guid><description>ODD sounds fancy. It&amp;amp;rsquo;s not. It means writing logs, metrics, and traces before you ship, not after your first outage.</description><content:encoded><![CDATA[<p>&ldquo;Observability-Driven Development&rdquo; has entered the conference talk circuit and I already hate the name. It sounds like a methodology. Like you need a certification or a Slack channel or a retro format.</p>
<p>It&rsquo;s not a methodology. It&rsquo;s just: instrument your code before you ship it. That&rsquo;s it. That&rsquo;s the whole thing.</p>
<p>And yet I keep walking into systems &ndash; at startups, at enterprises, everywhere &ndash; where observability was bolted on after the first production fire. The logs are a mess. Every service logs differently. Metrics exist for some endpoints but not others. Nobody has traces. The dashboards are either empty or full of vanity charts that nobody looks at.</p>
<h2 id="the-actual-problem">The actual problem</h2>
<p>When observability comes last, every team invents their own approach. Service A logs JSON with <code>request_id</code>. Service B logs plain text with <code>req_id</code>. Service C doesn&rsquo;t log request IDs at all. You find this out at 2am during an outage while trying to correlate a failure across three services.</p>
<p>I&rsquo;ve lived this. At the fintech startup we had a period where our financial data pipeline logs were useless for debugging cross-service issues because every team had picked their own field names. Fixing that retroactively took weeks. If we&rsquo;d agreed on a format before writing the services, it would&rsquo;ve taken an afternoon.</p>
<h2 id="what-odd-actually-means-in-practice">What &ldquo;ODD&rdquo; actually means in practice</h2>
<p>Before you write a feature, answer three questions:</p>
<ol>
<li>How will I know this is working correctly in production?</li>
<li>How will I know this is slow?</li>
<li>How will I know this is broken?</li>
</ol>
<p>If you can&rsquo;t answer those, you&rsquo;re not ready to write the code. That&rsquo;s the whole framework. No acronym needed.</p>
<h2 id="structured-logs-or-nothing">Structured logs or nothing</h2>
<p>Your logs should be structured JSON with consistent field names across every service. Non-negotiable.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;level&#34;</span>:<span style="color:#e6db74">&#34;info&#34;</span>,<span style="color:#f92672">&#34;event&#34;</span>:<span style="color:#e6db74">&#34;order_created&#34;</span>,<span style="color:#f92672">&#34;request_id&#34;</span>:<span style="color:#e6db74">&#34;abc-123&#34;</span>,<span style="color:#f92672">&#34;order_id&#34;</span>:<span style="color:#e6db74">&#34;ord-456&#34;</span>,<span style="color:#f92672">&#34;duration_ms&#34;</span>:<span style="color:#ae81ff">42</span>}
</span></span></code></pre></div><p>Every log line gets a <code>request_id</code>. Every log line gets an <code>event</code> name. Every log line gets a <code>level</code>. If you&rsquo;re logging <code>fmt.Println(&quot;something happened&quot;)</code> in production Go code, we need to talk.</p>
<p>Pick your field names once. Write them down. Enforce them in code review. This is boring work. It pays off every single incident.</p>
<h2 id="metrics-red-and-stop">Metrics: RED and stop</h2>
<p>For services, use RED metrics. Rate, Errors, Duration. For every endpoint. That&rsquo;s your baseline.</p>
<p>I see teams go wild with custom metrics on day one. Thirty metrics per service, half of them never queried. Meanwhile they&rsquo;re missing basic error rate tracking on their most critical endpoint. Start with RED. Add custom metrics when you have a specific question that RED can&rsquo;t answer.</p>
<p>One thing that will absolutely burn you: high-cardinality labels. If you&rsquo;re putting user IDs or full URLs into metric labels, you&rsquo;re building a cost bomb. I saw one team&rsquo;s Prometheus storage costs triple in a month because someone added a <code>path</code> label that included query parameters. Keep labels to things like <code>method</code>, <code>status_code</code>, <code>service</code>. Low and predictable.</p>
<h2 id="traces-arent-optional">Traces aren&rsquo;t optional</h2>
<p>Distributed tracing used to feel like a luxury. It&rsquo;s not. If you&rsquo;re running more than two services, you need traces. Full stop.</p>
<p>Every inbound request starts a trace. Every outbound call propagates the trace context. This is a few lines of middleware in Go. It&rsquo;s trivial. And it&rsquo;s the difference between &ldquo;I think the problem is in the payment service&rdquo; and &ldquo;I can see the exact call that took 4 seconds.&rdquo;</p>
<p>Sample if you need to for cost reasons. But sample consistently &ndash; don&rsquo;t sample 100% on staging and 1% on production and then wonder why you can never find the trace you need.</p>
<h2 id="make-it-part-of-code-review">Make it part of code review</h2>
<p>This is where it sticks or falls apart. If observability isn&rsquo;t in your code review checklist, it won&rsquo;t happen.</p>
<p>When I review a PR that adds a new endpoint, I look for:</p>
<ul>
<li>Does the handler emit RED metrics?</li>
<li>Are key events logged with stable fields?</li>
<li>Does the trace propagate to downstream calls?</li>
<li>Are the labels safe?</li>
</ul>
<p>If the answer is no, the PR isn&rsquo;t ready. Same as missing tests. Same as missing error handling. Observability isn&rsquo;t a follow-up ticket. It ships with the feature.</p>
<h2 id="alerts-that-dont-suck">Alerts that don&rsquo;t suck</h2>
<p>Most alerts are terrible. They fire on every blip, train everyone to ignore them, and then nobody notices when something actually breaks.</p>
<p>Alert on symptoms, not causes. Alert on &ldquo;error rate is above X% for Y minutes,&rdquo; not &ldquo;one request returned a 500.&rdquo; Better yet, use SLO-based alerts. Set an error budget. Alert when you&rsquo;re burning through it too fast. This single change cut our alert noise at Decloud by something like 80%.</p>
<h2 id="stop-making-this-complicated">Stop making this complicated</h2>
<p>The observability vendor ecosystem wants you to believe this is complex. It&rsquo;s not. Structured logs, RED metrics, distributed traces, and alerts that fire on actual problems. Agree on conventions. Enforce them in review. Ship them with every feature.</p>
<p>That&rsquo;s observability-driven development. No manifesto required.</p>
]]></content:encoded></item><item><title>eBPF Is Interesting. I Am Not Sold Yet.</title><link>https://lawzava.com/blog/2021-01-25-ebpf-observability/</link><pubDate>Mon, 25 Jan 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-01-25-ebpf-observability/</guid><description>eBPF promises kernel-level observability without the pain of kernel modules. The tech is real. The hype-to-adoption ratio concerns me.</description><content:encoded><![CDATA[<p>eBPF is the most overhyped technology in the observability space right now, and it might also be the most important.</p>
<p>That&rsquo;s not a contradiction. I&rsquo;ve been running Linux in production since before containers were a thing. The idea of safely running custom programs inside the kernel &ndash; attaching to tracepoints, kprobes, uprobes, without writing a kernel module or rebooting anything &ndash; is genuinely exciting. When I first ran <code>bpftrace</code> against a production system and got per-process syscall counts in real time with near-zero overhead, I understood the appeal immediately.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>bpftrace -e <span style="color:#e6db74">&#39;tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }&#39;</span>
</span></span></code></pre></div><p>That one-liner gives you more insight into what your system is actually doing than most monitoring stacks costing six figures a year. The kernel verifier checks your program for safety. Data flows through maps and perf buffers. No agent bloat. No sampling artifacts. Just direct observation at the source.</p>
<p>So why am I skeptical?</p>
<p>Because the gap between &ldquo;this is technically possible&rdquo; and &ldquo;my team can operate this in production&rdquo; is enormous. And the eBPF community seems uninterested in acknowledging that gap.</p>
<h2 id="the-promise-is-real">The promise is real</h2>
<p>I don&rsquo;t want to undersell what eBPF enables. Traditional monitoring gives you counters, logs, and coarse sampling. Fine for dashboards. Terrible for understanding why a specific request took 800ms when the p50 is 12ms. eBPF lets you attach instrumentation at the exact point where something happens. Syscall latency. TCP retransmits by destination. Filesystem I/O by process. All with filtering done in-kernel so you aren&rsquo;t drowning user space in data.</p>
<p>For container-dense environments &ndash; which is everything I work with these days &ndash; the ability to map kernel events to cgroups and namespaces is a game changer. Short-lived processes that vanish before your log collector notices? eBPF sees them.</p>
<h2 id="the-reality-check">The reality check</h2>
<p>Here&rsquo;s my problem. Every conference talk shows eBPF solving elegant debugging puzzles. Nobody talks about the operational burden.</p>
<p>Kernel version compatibility is a real issue. eBPF features vary across kernel versions, and the enterprise Linux distributions I see in production aren&rsquo;t exactly bleeding edge. A program that works on kernel 5.10 might not work on 4.18. BTF (BPF Type Format) availability is inconsistent. CO-RE (Compile Once, Run Everywhere) helps but isn&rsquo;t universally supported yet.</p>
<p>Then there&rsquo;s the expertise problem. Writing eBPF programs isn&rsquo;t like writing application code. You need to understand kernel internals, verifier constraints, and the performance implications of your hook points. Most engineering teams I work with can&rsquo;t spare someone to become the eBPF specialist. They need tools that work out of the box.</p>
<p>BCC, bpftrace, and the growing ecosystem of pre-built tools help. Brendan Gregg&rsquo;s work has been invaluable. But &ldquo;install bcc-tools and run execsnoop&rdquo; is a long way from &ldquo;build a production observability pipeline backed by eBPF.&rdquo;</p>
<h2 id="where-i-land">Where I land</h2>
<p>eBPF is infrastructure technology. It&rsquo;ll become the foundation that observability vendors build on. Cilium is already proving this for networking. The profiling tools are getting there. Give it two or three more years and it will be invisible plumbing that powers your monitoring stack.</p>
<p>But right now, in early 2021, if someone tells me they&rsquo;re building their observability strategy around eBPF, I ask two questions: what kernel version are you running, and who on your team understands the verifier? If they can&rsquo;t answer both, they should start with existing tools &ndash; <code>opensnoop</code>, <code>tcpconnect</code>, <code>biolatency</code> &ndash; and build intuition before writing custom programs.</p>
<p>The technology is sound. The ecosystem is maturing. I&rsquo;m watching closely. I&rsquo;m just not rewriting my monitoring stack around it today.</p>
]]></content:encoded></item><item><title>Observability for Small Distributed Teams (What Actually Works)</title><link>https://lawzava.com/blog/2020-09-14-observability-distributed-teams/</link><pubDate>Mon, 14 Sep 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-09-14-observability-distributed-teams/</guid><description>Most observability advice is written for 500-engineer orgs. Here&amp;amp;rsquo;s what actually matters when you&amp;amp;rsquo;re a small distributed team trying not to drown in dashboards.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>You don&rsquo;t need Datadog&rsquo;s enterprise tier. You need structured logs, one good dashboard per service, alerts that don&rsquo;t cry wolf, and a <code>request_id</code> on everything. That&rsquo;s 80% of it.</p>
<hr>
<p>I&rsquo;ve been working with distributed teams for most of this year. Some are five people across three time zones. Some are thirty across eight. The pattern I keep seeing: they either have zero observability or they went full enterprise cargo cult and now nobody can find anything.</p>
<p>There&rsquo;s a middle ground. I want to talk about that.</p>
<h2 id="the-actual-problem-with-distributed-teams">The actual problem with distributed teams</h2>
<p>In an office, someone notices something is slow. They say it out loud. Someone else goes &ldquo;oh yeah, I deployed ten minutes ago.&rdquo; Problem found in under a minute.</p>
<p>Remote? That same issue sits in a Slack thread for 45 minutes while people in different time zones wake up, read context, and try to figure out what changed. I&rsquo;ve watched this happen. Repeatedly.</p>
<p>The fix isn&rsquo;t more tools. It&rsquo;s making your systems capable of answering basic questions without requiring a human to be online at the right moment.</p>
<p>Three questions. That&rsquo;s it:</p>
<ol>
<li>Is this thing broken right now?</li>
<li>What changed recently?</li>
<li>Where do I look next?</li>
</ol>
<p>If your setup can answer those, you&rsquo;re ahead of most teams I&rsquo;ve worked with.</p>
<h2 id="enterprise-observability-isnt-your-observability">Enterprise observability isn&rsquo;t your observability</h2>
<p>Google has 10,000 SREs. They built custom everything. When you read their SRE book and try to implement the same stack with your team of eight, you end up with:</p>
<ul>
<li>A Prometheus instance nobody configured alerts for</li>
<li>Grafana dashboards copied from a blog post that don&rsquo;t match your services</li>
<li>Jaeger running but with 0.1% sampling so traces are useless when you actually need them</li>
<li>An ELK stack eating 40% of your infrastructure budget</li>
</ul>
<p>I&rsquo;ve seen this exact setup at three different companies this year. Not exaggerating.</p>
<h2 id="what-you-actually-need">What you actually need</h2>
<p>Here&rsquo;s my stack recommendation for a small distributed team. Opinionated, yes. But it works.</p>
<p><strong>Logs: Structured JSON to a managed service.</strong> Loki if you&rsquo;re cheap. Papertrail if you want simple. The key is structured, not the tool. Every log line should look roughly like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;level&#34;</span>: <span style="color:#e6db74">&#34;error&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;msg&#34;</span>: <span style="color:#e6db74">&#34;payment failed&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;checkout&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;request_id&#34;</span>: <span style="color:#e6db74">&#34;7f3c2c4d&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;user_id&#34;</span>: <span style="color:#e6db74">&#34;u_123&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;error&#34;</span>: <span style="color:#e6db74">&#34;card_declined&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;duration_ms&#34;</span>: <span style="color:#ae81ff">340</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Same field names. Every service. No exceptions. The <code>request_id</code> alone will save you hours of debugging per incident. I can&rsquo;t stress this enough. Propagate it through HTTP headers, queue messages, background jobs. Everywhere.</p>
<p><strong>Metrics: Prometheus + Grafana.</strong> Still the best bang for buck. But here&rsquo;s the thing &ndash; don&rsquo;t build 30 dashboards. Build one per service. Three panels:</p>
<ol>
<li>Request rate and error rate (tells you if something is broken)</li>
<li>Latency percentiles (tells you if it&rsquo;s degrading)</li>
<li>Recent deploys and config changes overlaid on the graphs (tells you what caused it)</li>
</ol>
<p>That&rsquo;s your dashboard. If a new engineer can&rsquo;t look at it and understand the health of the service in 30 seconds, strip it down further.</p>
<p><strong>Traces: Jaeger or Zipkin, but only if you have more than two services.</strong> If you&rsquo;re a monolith with a database and a cache, traces are overhead you don&rsquo;t need yet. Just use request IDs in your logs. Seriously.</p>
<p>When you do need traces, bump sampling to at least 10% on critical paths. 0.1% default sampling means you&rsquo;ll never have a trace for the request that actually broke.</p>
<p><strong>Alerts: Less is more.</strong> Every alert that pages someone at 3am and isn&rsquo;t actionable is erosion of trust. Once your team stops trusting alerts, you&rsquo;ve lost. They&rsquo;ll start muting channels. I&rsquo;ve seen it happen.</p>
<p>My rule: every alert needs three things.</p>
<ul>
<li>A condition that&rsquo;s actually abnormal (not &ldquo;CPU above 60%&rdquo;)</li>
<li>A link to the relevant dashboard</li>
<li>A link to a runbook that says what to do first</li>
</ul>
<p>If you can&rsquo;t write those three things for an alert, the alert shouldn&rsquo;t exist.</p>
<h2 id="the-request_id-sermon">The request_id sermon</h2>
<p>I keep coming back to this because it&rsquo;s the single highest-leverage thing you can do.</p>
<p>At the fintech startup we had services talking to services talking to queues talking to workers. When something went wrong without correlation IDs, the debugging process was: check this log, then that log, then maybe this other log, hope the timestamps line up, piece it together manually. Took forever.</p>
<p>After we standardized on a single <code>request_id</code> header propagated everywhere? Same investigation. One search. Done.</p>
<p>The implementation is trivial. Middleware that reads <code>X-Request-ID</code> from incoming requests. Generates a UUID if missing. Passes it along. Logs it on every line. Takes an afternoon to implement across your whole stack.</p>
<p>An afternoon of work for months of saved debugging time. That&rsquo;s the kind of trade I like.</p>
<h2 id="runbooks-the-unsexy-high-leverage-tool">Runbooks: the unsexy high-leverage tool</h2>
<p>Nobody wants to write runbooks. I get it. But here&rsquo;s the scenario: it&rsquo;s 2am in your time zone. The alert fires. The person on call is in a different country. They&rsquo;ve been on the team for three weeks.</p>
<p>Without a runbook, they&rsquo;re messaging people, waiting for responses, guessing. With a runbook, they open it, follow the steps, and either fix it or know exactly who to escalate to.</p>
<p>Keep them short. Keep them next to the code. Update them after every incident. A runbook that says &ldquo;check the database connection pool, then check Redis, then check the upstream API timeout&rdquo; is worth more than a 50-page incident response process document nobody has read.</p>
<h2 id="mistakes-i-keep-seeing">Mistakes I keep seeing</h2>
<p><strong>Collecting everything.</strong> Storage is cheap. Cardinality explosions aren&rsquo;t. I watched a team&rsquo;s Prometheus instance fall over because they added a <code>user_id</code> label to a counter. Millions of time series. Dead monitoring system. During an outage. Ironic.</p>
<p><strong>Dashboard graveyards.</strong> Thirty dashboards, twenty-eight of which nobody has looked at in months. Two of which are actually useful but you can&rsquo;t remember which ones. Delete aggressively.</p>
<p><strong>Happy path instrumentation only.</strong> Your error paths need more instrumentation than your happy paths. The happy path works. You know this because nobody is complaining. The error paths are where surprises live.</p>
<p><strong>Separate conventions per team.</strong> One team calls it <code>user_id</code>, another calls it <code>userId</code>, a third calls it <code>uid</code>. Now your cross-service queries are a mess. Pick a convention. Enforce it in code review. This is boring work that pays off enormously.</p>
<h2 id="what-to-measure-about-your-observability-itself">What to measure about your observability itself</h2>
<p>One meta-metric I track: time from &ldquo;something seems wrong&rdquo; to &ldquo;I know what changed and where to look.&rdquo; If that number is going down over time, your observability is working. If it&rsquo;s flat or going up, you&rsquo;re adding complexity without adding clarity.</p>
<p>The other one: how many alerts fired this week that didn&rsquo;t need a human response? If it&rsquo;s more than 20%, you have a noise problem.</p>
<h2 id="start-here">Start here</h2>
<p>If you&rsquo;re starting from scratch with a small distributed team, do this in order:</p>
<ol>
<li>Structured JSON logs with a shared <code>request_id</code>. One week of work, max.</li>
<li>One Grafana dashboard per service with the three panels I mentioned. Another week.</li>
<li>Three to five alerts that are actually actionable. A few days.</li>
<li>Short runbooks for those alerts. A day.</li>
</ol>
<p>That&rsquo;s a month of work spread across your team. After that, you have a system that answers the three questions. Everything else &ndash; traces, SLOs, error budgets, custom metrics &ndash; layer it on when you feel the pain, not before.</p>
<p>Don&rsquo;t let perfect be the enemy of &ldquo;I can actually debug production at 2am without waking up three people.&rdquo;</p>
]]></content:encoded></item><item><title>Your SLOs Are Probably Useless (Here's How to Fix Them)</title><link>https://lawzava.com/blog/2019-05-20-effective-slos/</link><pubDate>Mon, 20 May 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-05-20-effective-slos/</guid><description>Most SLOs are dashboards nobody acts on. Pick indicators that reflect real users, set targets from data, and make error budgets change how your team ships.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>If burning through your error budget doesn&rsquo;t change how your team ships, you don&rsquo;t have SLOs. You have decorative charts.</p>
<hr>
<p>I&rsquo;ve watched three different teams adopt SLOs in the past year. Two of them ended up with beautiful Grafana dashboards that nobody looked at after the first sprint. The third team actually used their error budget to cancel a feature release and fix a checkout regression instead. Guess which team had fewer incidents in Q4.</p>
<p>The difference wasn&rsquo;t tooling. It was whether the SLO changed behavior or just measured things.</p>
<h2 id="slos-are-decisions-not-dashboards">SLOs are decisions, not dashboards</h2>
<p>At the fintech startup, we tracked uptime for our financial data API. 99.9% availability on a fancy status page. Looked great. The problem? Our SLI was measuring HTTP 200s from a health endpoint. Meanwhile, users were getting stale stock prices because our data pipeline was silently lagging by 30 minutes. By our SLO, everything was fine. By our users&rsquo; experience, the product was broken.</p>
<p>An effective SLO is a contract between reliability and velocity. It answers one question: can we ship this week, or do we owe the users some stability work first? If it doesn&rsquo;t influence your sprint planning, kill it.</p>
<h2 id="measure-what-your-users-feel-not-what-your-infra-reports">Measure what your users feel, not what your infra reports</h2>
<p>Start with the user journey. Not the Kubernetes dashboard.</p>
<p><strong>These aren&rsquo;t user-facing indicators:</strong></p>
<ul>
<li>CPU utilization</li>
<li>Pod restart counts</li>
<li>Database connection pool size</li>
</ul>
<p><strong>These are:</strong></p>
<ul>
<li>Successful checkout completions</li>
<li>Search results returned under 400ms</li>
<li>API responses with correct, fresh data</li>
</ul>
<p>The distinction seems obvious written down, but I still see teams default to infrastructure metrics because they&rsquo;re easier to collect. Easier isn&rsquo;t the point. Accurate is.</p>
<h3 id="start-with-four-signals-then-get-specific">Start with four signals, then get specific</h3>
<p>For most services, you can begin with availability, latency, throughput, and error rate. The classic golden signals. But don&rsquo;t stop there. Availability that counts health check pings the same as checkout requests is lying to you.</p>
<p>A useful SLI is brutally specific:</p>
<ul>
<li><strong>The request:</strong> <code>POST /api/v1/checkout</code> from authenticated users</li>
<li><strong>What counts as success:</strong> HTTP status &lt; 500 AND order confirmation generated</li>
<li><strong>The population:</strong> production traffic only, excluding synthetic monitors</li>
<li><strong>The window:</strong> rolling 28 days</li>
</ul>
<p>That specificity is the difference between an SLI that catches real problems and one that hides them in averages.</p>
<h2 id="set-targets-from-data-not-ambition">Set targets from data, not ambition</h2>
<p>I see this constantly: a team picks 99.99% availability because it sounds professional. They&rsquo;ve been running at 99.2% for six months. The gap between target and reality is so large that the error budget is permanently exhausted, which means the policy attached to it&rsquo;s permanently triggered, which means everyone ignores it.</p>
<p>A target that&rsquo;s never met is noise. A target that&rsquo;s always met is invisible. Neither changes behavior.</p>
<p>Here&rsquo;s what actually works:</p>
<ol>
<li><strong>Measure your current performance for 2-4 weeks.</strong> No changes, just observation.</li>
<li><strong>Set the target slightly tighter than current reality.</strong> If you&rsquo;re running at 99.5%, try 99.7%.</li>
<li><strong>Adjust quarterly based on data and user feedback.</strong> Not based on what the VP saw at a conference.</li>
</ol>
<p>Different services deserve different targets. Your payment processing endpoint can justify 99.95%. Your internal admin dashboard? 99% is probably generous. During the early Decloud days at a deep-tech founder program, we ran our dev tooling at targets that would horrify a payments team &ndash; and that was the right call. We needed to ship fast, not polish internal tools.</p>
<h3 id="windows-matter-more-than-you-think">Windows matter more than you think</h3>
<p>A 99.9% SLO over 30 days gives you about 43 minutes of allowed downtime. The same target over 7 days gives you about 10 minutes. Choose a window that matches how fast your team can actually detect and respond to problems. If your mean time to detect is 20 minutes, a 7-day window at 99.9% is a trap.</p>
<h2 id="error-budgets-the-part-everyone-gets-wrong">Error budgets: the part everyone gets wrong</h2>
<p>The math is simple:</p>
<pre tabindex="0"><code>error_budget = 1 - SLO_target
</code></pre><p>A 99.9% target over 30 days means you can tolerate roughly 43 minutes of downtime. A 99.5% target gives you about 3.6 hours. These numbers aren&rsquo;t interesting by themselves. What makes them powerful is the policy.</p>
<p><strong>Budget healthy (&gt; 50% remaining):</strong> Ship normally. Take calculated risks. Run that migration you&rsquo;ve been planning.</p>
<p><strong>Budget tight (10-50% remaining):</strong> Slow down releases. Require extra review on risky changes. Maybe skip the experimental feature flag rollout this week.</p>
<p><strong>Budget burned (&lt; 10% remaining):</strong> Stop feature work. The entire team focuses on reliability until the budget recovers.</p>
<p>That third state is where most teams fail. They write the policy, then when the budget actually burns, some product manager argues that the feature is too important to delay. If leadership won&rsquo;t enforce the budget policy, you don&rsquo;t have SLOs. You have aspirations.</p>
<h3 id="track-burn-rate-not-just-remaining-budget">Track burn rate, not just remaining budget</h3>
<p>A single bad deployment can eat your monthly budget in an hour. By the time you notice the remaining budget is low, the damage is done.</p>
<pre tabindex="0"><code>burn_rate = errors_in_window / budget_for_window
</code></pre><p>Alert on burn rate. If you&rsquo;re consuming budget at 10x the sustainable rate, you want to know in minutes, not at the Monday standup.</p>
<h2 id="keep-it-minimal">Keep it minimal</h2>
<p>You don&rsquo;t need an SLO for every endpoint. Pick the 3-5 user journeys that define whether your product is working. For most B2B SaaS, that&rsquo;s: login, core workflow, data export, and billing. Everything else is noise at this stage.</p>
<p>Instrumentation comes first. An SLO is just a query on top of good metrics. If you don&rsquo;t have request counts, status codes, and latency histograms, start there. A YAML definition can be as simple as:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">slo</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">checkout-availability</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">objective</span>: <span style="color:#ae81ff">99.9</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">window</span>: <span style="color:#ae81ff">28d</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">indicator</span>: <span style="color:#ae81ff">success_rate</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">filter</span>: <span style="color:#e6db74">&#34;route = /checkout AND source = production&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">success</span>: <span style="color:#e6db74">&#34;status_code &lt; 500 AND order_confirmed = true&#34;</span>
</span></span></code></pre></div><p>Your dashboard should answer three questions and nothing else: Are we meeting the SLO right now? How much budget is left? How fast are we burning it?</p>
<h2 id="proof-it-works">Proof it works</h2>
<p>Here&rsquo;s how you know if your SLOs are working: the last time your error budget got tight, did anything actually change? Did a release get delayed? Did someone shift from feature work to fixing that flaky dependency? Did the on-call rotation get extra support?</p>
<p>If the answer is no, go back to the error budget policy and make it real. Get sign-off from engineering leadership. Write it into your sprint process. Make the consequences automatic, not optional.</p>
<p>SLOs are a decision framework disguised as monitoring. The monitoring part is easy. The decision part is where most teams give up.</p>
<p>Don&rsquo;t be most teams.</p>
]]></content:encoded></item><item><title>Why Monitoring Wasn't Enough and How We Built Observability at a Fintech Startup</title><link>https://lawzava.com/blog/2018-07-09-observability-beyond-monitoring/</link><pubDate>Mon, 09 Jul 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-07-09-observability-beyond-monitoring/</guid><description>After a mystery outage that our dashboards couldn&amp;amp;rsquo;t explain, I rebuilt the fintech startup&amp;amp;rsquo;s telemetry stack around metrics, logs, and traces. Here&amp;amp;rsquo;s what I learned.</description><content:encoded><![CDATA[<p>It was 2 AM on a Wednesday, and our news ingestion pipeline at the fintech startup had gone silent. No errors in the logs. No alerts firing. CPU fine, memory fine, disk fine. Every dashboard said the system was healthy. But users were seeing stale financial news, some of it hours old.</p>
<p>I spent forty minutes SSH-ing into boxes, tailing logs, and grepping for exceptions. Nothing. The pipeline just&hellip; stopped processing. It took me another hour to discover the root cause: a third-party API we depended on had started returning empty 200 responses instead of actual data. Our monitoring checked for errors. It checked for timeouts. It never checked for &ldquo;success that contains nothing useful.&rdquo;</p>
<p>That night changed how I think about production systems.</p>
<h2 id="monitoring-checks-boxes-observability-answers-questions">Monitoring Checks Boxes. Observability Answers Questions.</h2>
<p>Monitoring is built around things you already know can go wrong. Threshold crossed, alert fires, runbook engaged. It works great for predictable problems: disk filling up, CPU pegged, error rate spiking. We had all of that at the fintech startup. Grafana dashboards everywhere. PagerDuty wired up. It felt safe.</p>
<p>But distributed systems don&rsquo;t fail in predictable ways. They fail in weird, combinatorial, never-seen-this-before ways. And that&rsquo;s where monitoring falls apart. You can&rsquo;t write an alert for a failure mode you haven&rsquo;t imagined yet.</p>
<p>Observability flips the model. Instead of predefining what questions the system can answer, you instrument it richly enough that you can ask <em>new</em> questions on the fly. During an incident. At 2 AM. Without deploying anything.</p>
<p>The difference: monitoring tells you something is wrong. Observability helps you figure out <em>why</em>.</p>
<h2 id="three-signals-one-story">Three Signals, One Story</h2>
<p>After the empty-200 incident, I started rebuilding our telemetry around three pillars.</p>
<p><strong>Metrics</strong> give you the bird&rsquo;s-eye view. Aggregated numbers over time. At the fintech startup, we track request rates, error rates, and latency distributions for every service using Prometheus.</p>
<pre tabindex="0"><code>http_requests_total{method=&#34;GET&#34;, endpoint=&#34;/api/stories&#34;, status=&#34;200&#34;} 15234
http_request_duration_seconds_bucket{endpoint=&#34;/api/stories&#34;, le=&#34;0.5&#34;} 421
</code></pre><p>Metrics are cheap to store and great for alerting. But they can&rsquo;t tell you why a specific request failed or what user it affected. They&rsquo;re the smoke detector, not the fire investigator.</p>
<p><strong>Logs</strong> capture the details. Each event, each error, each decision the code made. We moved early to structured JSON logs because free-form strings are nearly useless at scale.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;timestamp&#34;</span>:<span style="color:#e6db74">&#34;2018-07-09T10:30:45Z&#34;</span>,<span style="color:#f92672">&#34;level&#34;</span>:<span style="color:#e6db74">&#34;error&#34;</span>,<span style="color:#f92672">&#34;service&#34;</span>:<span style="color:#e6db74">&#34;ingestion&#34;</span>,<span style="color:#f92672">&#34;request_id&#34;</span>:<span style="color:#e6db74">&#34;abc123&#34;</span>,<span style="color:#f92672">&#34;message&#34;</span>:<span style="color:#e6db74">&#34;Empty response from provider&#34;</span>,<span style="color:#f92672">&#34;provider&#34;</span>:<span style="color:#e6db74">&#34;reuters&#34;</span>,<span style="color:#f92672">&#34;duration_ms&#34;</span>:<span style="color:#ae81ff">340</span>}
</span></span></code></pre></div><p>That structured format meant we could query logs in Kibana instead of grepping through them. Game changer for incident response.</p>
<p><strong>Traces</strong> show you the journey of a single request across services. This was the missing piece for us. The fintech startup&rsquo;s architecture involves an ingestion service talking to NLP processors talking to a ranking engine talking to the API layer. When something is slow, you need to see the whole chain.</p>
<pre tabindex="0"><code>Trace abc123
- POST /ingest/batch (120ms)
  - NLP enrichment (45ms)
  - Relevance scoring (30ms)
  - db.write stories (35ms)
</code></pre><p>We started with Zipkin. Not perfect, but suddenly I could see that our NLP service was adding 200ms of latency on certain content types. That was invisible before.</p>
<h2 id="the-glue-correlation-ids">The Glue: Correlation IDs</h2>
<p>Each signal alone is useful. Together, connected by a shared identifier, they&rsquo;re powerful.</p>
<p>Here&rsquo;s the workflow that actually matters: a latency alert fires from metrics. You pull up the trace for a slow request. The trace shows a specific span taking too long. You jump to the logs for that span using the same request ID and find the exact query and error.</p>
<pre tabindex="0"><code>X-Request-ID: 5f3c9e86c2c84e1b
X-B3-TraceId: 4d1e00a3b9bd1d42
X-B3-SpanId: 6df3a1c2b93f6b1a
</code></pre><p>We made it a rule: every log line includes the request ID, and every trace propagates context headers. No exceptions. It took weeks to retrofit across all our services, but the payoff during the next incident was immediate. Instead of an hour of archaeology, I had a clear thread to pull.</p>
<h2 id="instrument-the-critical-path-first">Instrument the Critical Path First</h2>
<p>When I started adding instrumentation at the fintech startup, the temptation was to instrument everything. Don&rsquo;t do that. You&rsquo;ll drown in data and your storage costs will spike.</p>
<p>Start with what matters: the critical path your users depend on.</p>
<p>For us that meant inbound API handlers, the news ingestion pipeline, database calls, and every external API dependency. We used RED (rate, errors, duration) for our services and USE (utilization, saturation, errors) for infrastructure resources.</p>
<p>Tracing has a sampling problem. Capturing every single request is expensive. We settled on keeping 100% of errors and sampling successful requests at about 5%. During incidents, we crank sampling up. Good enough for debugging, affordable enough to run continuously.</p>
<h2 id="designing-systems-that-can-be-debugged">Designing Systems That Can Be Debugged</h2>
<p>Observability isn&rsquo;t something you bolt on after the fact. It&rsquo;s a design choice.</p>
<p>Structured logs over free-form strings. Always. If you can&rsquo;t query it, it&rsquo;s useless when you&rsquo;re under pressure at 2 AM.</p>
<p>Watch your cardinality. Metrics with unbounded label values (like user IDs) will destroy your Prometheus instance. High-cardinality data belongs in logs and traces, not metrics.</p>
<p>Add context that matters. A trace that only shows timings is half the story. Include the request type, the tenant, the result count. When something goes wrong, that context is the difference between a five-minute fix and a two-hour hunt.</p>
<h2 id="our-stack-in-2018">Our Stack in 2018</h2>
<p>For anyone building this out now, here&rsquo;s what we were running:</p>
<ul>
<li><strong>Metrics</strong>: Prometheus with Alertmanager, Grafana for dashboards</li>
<li><strong>Logs</strong>: Fluentd piping into Elasticsearch, Kibana for exploration</li>
<li><strong>Tracing</strong>: Zipkin with OpenTracing instrumentation</li>
</ul>
<p>OpenTracing is getting solid adoption and keeps us from being locked into one vendor. OpenCensus is emerging as an alternative worth watching. Both are pushing toward a world where trace context propagation just works out of the box.</p>
<p>There are commercial options that bundle everything behind one query layer. We looked at them. For our scale, the open source stack made more sense. That calculus changes depending on team size and how much operational overhead you can absorb.</p>
<h2 id="what-that-2-am-incident-taught-me">What That 2 AM Incident Taught Me</h2>
<p>That silent pipeline failure was a gift. It exposed a blind spot in how I thought about production readiness. Having dashboards isn&rsquo;t the same as having understanding. Monitoring answers the questions you thought to ask. Observability gives you the ability to investigate the questions you didn&rsquo;t.</p>
<p>We still have incidents at the fintech startup. But now when something breaks in a way we&rsquo;ve never seen before, we have the telemetry to figure it out fast. That&rsquo;s the whole point.</p>
]]></content:encoded></item><item><title>Monitoring Is Not Enough</title><link>https://lawzava.com/blog/2017-03-20-why-observability-matters-more-than-monitoring/</link><pubDate>Mon, 20 Mar 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-03-20-why-observability-matters-more-than-monitoring/</guid><description>Your dashboards look green. Your users say the site is broken. That gap is the whole problem.</description><content:encoded><![CDATA[<p>Most teams think they have monitoring figured out. Dashboards, thresholds, PagerDuty. Something spikes, someone gets paged, someone fixes it. Works great when you have a monolith and three endpoints.</p>
<p>It falls apart the second you split into services.</p>
<p>I learned this the hard way at the fintech startup. We had decent Grafana dashboards. Reasonable alerts. Then we started breaking things into microservices and deploying multiple times a day. A request would fail somewhere in a chain of five services and our dashboards would just&hellip; look fine. Every individual service reported healthy metrics. The problem lived in the gaps between them.</p>
<p>That&rsquo;s the core issue with monitoring. It answers questions you already thought to ask. Latency on this endpoint? Sure. Error rate on that queue? Got it. But the failure you actually hit in production is the one you never predicted. Your dashboards have no panel for it.</p>
<h3 id="observability-is-a-different-mindset">Observability is a different mindset</h3>
<p>Observability means you instrument your system so you can ask <em>new</em> questions after something breaks. Not just &ldquo;is it up&rdquo; but &ldquo;why did this specific user&rsquo;s request take 8 seconds at 3am on Tuesday.&rdquo;</p>
<p>Three signals make this work: metrics for trends and alerts, logs for detail, and traces for stitching a single request across every service it touches. Separately they are useful. Together they are a debugging superpower.</p>
<p>Structured logging is the foundation. Stop writing free-text log lines. Make every log entry a JSON object with a trace ID, service name, version, and whatever fields you actually need to filter on.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;timestamp&#34;</span>: <span style="color:#e6db74">&#34;2017-03-20T10:23:45Z&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;level&#34;</span>: <span style="color:#e6db74">&#34;error&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;Payment processing failed&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;trace_id&#34;</span>: <span style="color:#e6db74">&#34;abc123&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;payment-service&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;2.1.3&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then propagate context. Generate a trace ID at the edge and carry it through every downstream call. This is the single most important thing you can do. Without it you&rsquo;re just grepping logs and praying.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>X-Trace-Id: abc123
</span></span><span style="display:flex;"><span>X-Span-Id: def456
</span></span><span style="display:flex;"><span>X-Parent-Span-Id: ghi789
</span></span></code></pre></div><h3 id="how-incidents-actually-change">How incidents actually change</h3>
<p>Alert fires. Pull the trace. Follow the slow span to the source. Read the logs for that trace. Confirm with metrics whether it&rsquo;s one user or everyone. Done. Five minutes instead of forty-five minutes of bouncing between dashboards while your Slack channel fills with &ldquo;any update?&rdquo;</p>
<p>Same thing with performance. User says it&rsquo;s slow. Pull their trace. Compare to a healthy one. See the difference &ndash; a cache miss, an extra database round-trip, a third-party call timing out. You fix the actual cause instead of guessing.</p>
<h3 id="the-tools-dont-matter-that-much">The tools don&rsquo;t matter that much</h3>
<p>Prometheus, InfluxDB, ELK, Jaeger, Zipkin &ndash; pick whatever fits your stack. Commercial platforms that bundle all three signals save time. But the tooling isn&rsquo;t the hard part. The hard part is disciplined instrumentation. Consistent field names. Trace IDs everywhere. Every team following the same conventions.</p>
<h3 id="what-actually-matters">What actually matters</h3>
<p>Observability isn&rsquo;t a product you buy. It&rsquo;s a practice you build. You stop staring at dashboards waiting for red. You start asking questions about behavior you didn&rsquo;t expect. That shift &ndash; from reactive to exploratory &ndash; is the entire point. And in a world where every team is shipping services independently, it&rsquo;s the only way to stay sane.</p>
]]></content:encoded></item><item><title>Why We Deleted 42 Grafana Panels</title><link>https://lawzava.com/blog/2016-12-12-production-monitoring-metrics-that-matter/</link><pubDate>Mon, 12 Dec 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-12-12-production-monitoring-metrics-that-matter/</guid><description>Most teams monitor too much and alert on the wrong things. Five metrics are enough to run a startup backend.</description><content:encoded><![CDATA[<p>At a mobility startup we went through the classic monitoring arc. Started with nothing, panicked after an outage, installed StatsD and Grafana, then spent two weeks shipping every metric we could find into dashboards nobody looked at. We had 47 Grafana panels. Forty-seven. And when the next incident hit, we still couldn&rsquo;t figure out what was broken for twenty minutes.</p>
<p>The problem wasn&rsquo;t tooling. The problem was noise. So we deleted 42 of those panels. Kept five metrics. Built real alerts around them. Our on-call engineer started sleeping again.</p>
<h3 id="most-monitoring-is-waste">Most monitoring is waste</h3>
<p>Here is what I&rsquo;ve learned running production systems: if you&rsquo;re watching more than a handful of metrics, you&rsquo;re watching none of them. Human attention doesn&rsquo;t scale. Twenty dashboards with eight panels each means you have zero dashboards, because nobody is actually reading them under pressure.</p>
<p>The instinct after an outage is always &ldquo;we need more visibility.&rdquo; Wrong. You need better visibility. Those are different things.</p>
<h3 id="five-numbers-nothing-else">Five numbers, nothing else</h3>
<p>For a startup backend serving real users, these are the only metrics I care about on a daily basis:</p>
<p><strong>1. Request latency at p95 and p99.</strong> Not average. Averages lie. A service averaging 40ms with a p99 at 3 seconds is broken for one in a hundred users, and that one user is filing the support ticket. We tracked this per-service in Prometheus and set alerts at the Grafana layer.</p>
<p><strong>2. Error rate by type.</strong> Not just &ldquo;5xx count.&rdquo; Break it down. A spike in 401s is a different problem than a spike in 503s. One is probably a bad deploy or a client bug. The other is your database falling over. The distinction matters because the response is completely different.</p>
<p><strong>3. Saturation of your bottleneck resource.</strong> For us that was PostgreSQL connection pool utilization. For you it might be memory, disk I/O, or worker threads. The point is: know which resource will kill you first and watch that one. Not all resources. That one.</p>
<p><strong>4. Request throughput.</strong> Traffic going up is good. Traffic dropping to zero at 2pm on a Tuesday is very bad. This metric is less about the number and more about detecting sudden changes. A 50% drop in requests is an incident whether or not errors are firing.</p>
<p><strong>5. Deployment markers.</strong> Not a metric in the traditional sense, but I overlay every deploy on our Grafana dashboards. Half the incidents I&rsquo;ve investigated started with &ldquo;someone shipped something.&rdquo; Correlating metrics with deploys cuts your mean time to diagnosis in half.</p>
<p>That&rsquo;s it. Five things.</p>
<h3 id="alerts-should-wake-you-up-for-a-reason">Alerts should wake you up for a reason</h3>
<p>We had a rule: if an alert fires and the on-call engineer can&rsquo;t take a meaningful action within five minutes, delete the alert. Brief CPU spikes? Not actionable. Disk at 80%? Only actionable if there&rsquo;s a runbook. &ldquo;Something might be wrong&rdquo; isn&rsquo;t an alert. It&rsquo;s anxiety.</p>
<p>Our paging alerts were:</p>
<ul>
<li>p99 latency above threshold for 5 minutes</li>
<li>Error rate above 1% for 3 minutes</li>
<li>Primary database connection pool above 90%</li>
</ul>
<p>Three alerts. That was it for paging. Everything else went to a Slack channel that people checked during business hours.</p>
<h3 id="the-discipline-is-in-what-you-remove">The discipline is in what you remove</h3>
<p>After I stripped our monitoring down, the team pushed back. &ldquo;What if we miss something?&rdquo; My answer: we were already missing things. Forty-seven panels meant nobody looked at any of them carefully. Five panels meant the on-call engineer actually understood the state of the system at a glance.</p>
<p>Monitoring isn&rsquo;t a collection problem. It&rsquo;s an attention problem. Treat it that way.</p>
<h3 id="five-metrics-three-alerts-zero-noise">Five metrics, three alerts, zero noise</h3>
<p>Pick the five metrics that reflect what your users experience. Build alerts only for conditions where someone can take immediate action. Delete everything else. Discipline over dashboards.</p>
]]></content:encoded></item><item><title>Log Aggregation at Scale: ELK vs Alternatives</title><link>https://lawzava.com/blog/2016-09-05-log-aggregation-at-scale-elk-vs-alternatives/</link><pubDate>Mon, 05 Sep 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-09-05-log-aggregation-at-scale-elk-vs-alternatives/</guid><description>ELK is powerful. It&amp;amp;rsquo;s also a second full-time job. Here&amp;amp;rsquo;s what I learned running it at a mobility startup, and what I&amp;amp;rsquo;d consider instead.</description><content:encoded><![CDATA[<p>Once you have more than a handful of services, SSH-and-grep stops working. A single user request at a mobility startup touches the mobile API, the fleet service, the payment layer, and at least two background workers. When something breaks, I need to search one place for all the related events. That isn&rsquo;t optional. That&rsquo;s the baseline.</p>
<p>So we set up ELK. Elasticsearch, Logstash, Kibana. The pitch was compelling: open source, flexible, great full-text search, a plugin for everything. We stood up a three-node cluster, pointed Logstash at it, gave the team Kibana dashboards. For the first few weeks, it felt like a superpower.</p>
<p>Then the cluster started misbehaving. And I spent the next several months learning a painful lesson about the gap between &ldquo;powerful&rdquo; and &ldquo;worth the operational cost.&rdquo;</p>
<p><strong>If you don&rsquo;t have someone who wants to babysit Elasticsearch full time, don&rsquo;t run ELK yourself.</strong> Hosted Elasticsearch, Graylog, or even Splunk will save you more engineering hours than they cost.</p>
<h3 id="the-operational-tax-nobody-warns-you-about">The Operational Tax Nobody Warns You About</h3>
<p>Elasticsearch isn&rsquo;t a database you deploy and forget. It&rsquo;s a distributed system that demands constant attention. Shard allocation, index lifecycle, JVM heap tuning, split-brain prevention, disk watermarks. Every one of these will bite you, and they will bite you at 3 AM.</p>
<p>At the mobility startup, our log volume was moderate. Maybe a few gigabytes a day. Nothing that should stress a three-node cluster. But Elasticsearch doesn&rsquo;t care about your expectations. It cares about index design, merge policies, and whether you remembered to set <code>bootstrap.memory_lock</code>. We spent more time keeping the logging infrastructure healthy than we spent on some of our actual product services.</p>
<p>The worst part is that when your logging system goes down, you lose visibility into everything else at the same time. Your safety net disappears exactly when you need it most.</p>
<h3 id="what-elk-actually-gets-right">What ELK Actually Gets Right</h3>
<p>I&rsquo;m not going to pretend it&rsquo;s all bad. Elasticsearch&rsquo;s search is genuinely excellent. When the cluster is healthy, the ability to run arbitrary queries across millions of log lines with sub-second response times is hard to match. Logstash can parse almost any log format into structured fields. Kibana dashboards gave our product team visibility they never had before.</p>
<p>The ecosystem is real. Beats shippers are lightweight, community plugins cover most integrations, and the documentation is solid. If you have the operational muscle, ELK is the most flexible open source logging stack available.</p>
<h3 id="what-id-do-differently">What I&rsquo;d Do Differently</h3>
<p>If I were starting over, I wouldn&rsquo;t self-host Elasticsearch for logging. Full stop.</p>
<p><strong>Hosted Elasticsearch</strong> removes the worst of the operational burden. You keep the same query model, the same Kibana dashboards, the same integrations. You lose some control over cluster configuration and you pay more per gigabyte, but you gain back the engineering hours you were burning on cluster babysitting. For most teams, that tradeoff is obvious.</p>
<p><strong>Graylog</strong> is worth a look if you want Elasticsearch search without the full DIY build. It wraps Elasticsearch in a more opinionated log management experience with built-in alerting and stream routing. Less flexible than raw ELK, but faster to get running and easier to keep running.</p>
<p><strong>Splunk</strong> is the enterprise answer. Powerful, mature, battle-tested. Also expensive enough to make your finance team flinch. If budget isn&rsquo;t the constraint, Splunk is a safe bet. For a startup, it rarely makes sense.</p>
<p><strong>Cloud provider logging</strong> is the lowest-effort option. AWS CloudWatch Logs, for example, integrates deeply with everything else in AWS and requires zero operational overhead. The query capabilities are basic compared to Elasticsearch, but basic is often enough. You can always export to something more powerful later.</p>
<h3 id="structured-logging-is-the-real-win">Structured Logging Is the Real Win</h3>
<p>Regardless of which aggregation tool you pick, the single best investment is structured logging. A JSON log line with consistent fields turns debugging from archaeology into search.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;timestamp&#34;</span>: <span style="color:#e6db74">&#34;2016-09-05T10:23:45Z&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;level&#34;</span>: <span style="color:#e6db74">&#34;error&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;payment-api&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;trace_id&#34;</span>: <span style="color:#e6db74">&#34;abc123&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;Payment processing failed&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;error&#34;</span>: <span style="color:#e6db74">&#34;Connection timeout&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;customer_id&#34;</span>: <span style="color:#e6db74">&#34;cust_456&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Carry a trace ID through every service. Use consistent field names. Emit JSON instead of free-form text. These decisions pay off no matter what sits behind the ingestion pipeline. If you do nothing else, do this.</p>
<h3 id="put-a-buffer-in-front-of-ingestion">Put a Buffer in Front of Ingestion</h3>
<p>One more thing I learned the hard way: put a queue between your log shippers and your aggregation layer. Kafka, Redis, even a simple file buffer. Traffic spikes will happen. Deploys will happen. If your pipeline has no buffer, you drop logs during the exact moments you need them most.</p>
<h3 id="pick-your-logging-battles">Pick your logging battles</h3>
<p>ELK is powerful software with brutal operational costs. Most teams underestimate how much work Elasticsearch is to run, and they find out at the worst possible time. If you can afford someone who genuinely enjoys tuning JVM garbage collection and shard allocation, go for it. Otherwise, pay for a hosted solution or pick a simpler tool. Your on-call engineers will thank you.</p>
]]></content:encoded></item></channel></rss>