<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Samuel Dahunsi</title>
    <description>The latest articles on DEV Community by Samuel Dahunsi (@psalmuel1st).</description>
    <link>https://dev.to/psalmuel1st</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4032384%2Fa7b6986a-ddb9-41f9-9118-7f5df3e2340d.jpg</url>
      <title>DEV Community: Samuel Dahunsi</title>
      <link>https://dev.to/psalmuel1st</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/psalmuel1st"/>
    <language>en</language>
    <item>
      <title>The Commitment Has to Come First: What My Rust KZG Prototype Taught Me About Binding</title>
      <dc:creator>Samuel Dahunsi</dc:creator>
      <pubDate>Tue, 25 Aug 2026 05:06:54 +0000</pubDate>
      <link>https://dev.to/psalmuel1st/the-commitment-has-to-come-first-what-my-rust-kzg-prototype-taught-me-about-binding-50a2</link>
      <guid>https://dev.to/psalmuel1st/the-commitment-has-to-come-first-what-my-rust-kzg-prototype-taught-me-about-binding-50a2</guid>
      <description>&lt;p&gt;A polynomial-opening proof can pass every pairing check and still fail to prove the statement you care about.&lt;/p&gt;

&lt;p&gt;I ran into that distinction while implementing a multilinear KZG-style commitment scheme in Rust as part of my zero-knowledge cryptography study repository. The prototype could commit to a multilinear polynomial, open it at a point, construct quotient witnesses, and verify the resulting pairing equation over BLS12-381.&lt;/p&gt;

&lt;p&gt;The tests passed. The algebra checked out. But the first version of the API left an important question unanswered:&lt;/p&gt;

&lt;p&gt;Which commitment did the verifier intend to check?&lt;/p&gt;

&lt;p&gt;That question sounds administrative. It is actually the boundary between an opening proof and a self-consistent collection of prover-chosen values.&lt;/p&gt;

&lt;p&gt;This article walks through the implementation, the equation behind it, the mistake, and the design changes I would make before treating the code as anything more than a learning prototype.&lt;/p&gt;

&lt;p&gt;The argument in one sentence&lt;/p&gt;

&lt;p&gt;A polynomial commitment opening is meaningful only relative to a commitment that was fixed before the opening proof was produced.&lt;/p&gt;

&lt;p&gt;If the prover is allowed to supply the commitment inside the proof—and the verifier has no independently trusted copy—then verification only establishes:&lt;/p&gt;

&lt;p&gt;“This value and these witnesses are consistent with this commitment.”&lt;/p&gt;

&lt;p&gt;It does not establish:&lt;/p&gt;

&lt;p&gt;“This is the value of the polynomial that was committed to earlier.”&lt;/p&gt;

&lt;p&gt;The pairing equation cannot recover missing protocol context. The API has to preserve it.&lt;/p&gt;

&lt;p&gt;What I implemented&lt;/p&gt;

&lt;p&gt;The repository builds the relevant pieces from first principles:&lt;/p&gt;

&lt;p&gt;multilinear polynomials represented by their evaluations on the Boolean hypercube;&lt;/p&gt;

&lt;p&gt;partial evaluation and interpolation;&lt;/p&gt;

&lt;p&gt;a structured reference string evaluated at a secret point;&lt;/p&gt;

&lt;p&gt;commitment computation;&lt;/p&gt;

&lt;p&gt;quotient-witness generation for an opening point; and&lt;/p&gt;

&lt;p&gt;pairing-based verification using Arkworks and BLS12-381.&lt;/p&gt;

&lt;p&gt;This is intentionally educational code. It favors making the algebra visible over hiding it behind an optimized polynomial-commitment interface.&lt;/p&gt;

&lt;p&gt;One naming detail matters: the Rust field is called coefficients, but the stored vector behaves as an evaluation table. For an n-variable multilinear polynomial, it contains the values&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;{ f(b) | b ∈ {0,1}&lt;sup&gt;n&lt;/sup&gt; }&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Calling these values “coefficients” is convenient in code, but misleading in an explanation. They are not monomial-basis coefficients.&lt;/p&gt;

&lt;p&gt;From a Boolean evaluation table to any field point&lt;/p&gt;

&lt;p&gt;Let f be a multilinear polynomial in n variables. Its multilinear extension is&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;f̃(x) = Σb ∈ {0,1}&lt;sup&gt;n&lt;/sup&gt; f(b) χb(x)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;where&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;χb(x) = ∏j=1&lt;sup&gt;n&lt;/sup&gt; [bjxj + (1 − bj)(1 − xj)]&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The implementation evaluates the polynomial one variable at a time. If two entries differ only in the variable being fixed, partial evaluation at r is just linear interpolation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;y(r) = y0 + r(y1 − y0)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Rust implementation is the same interpolation written directly:&lt;/p&gt;

&lt;p&gt;fn interpolate(y0: F, y1: F, r: F) -&amp;gt; F {&lt;br&gt;
    y0 + r * (y1 - y0)&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Repeating that fold over all coordinates reduces the full hypercube table to the single value f(r₁, …, rₙ).&lt;/p&gt;

&lt;p&gt;This representation is especially useful for studying sumcheck and GKR because those protocols repeatedly fix variables of multilinear extensions. It also makes the multilinear commitment construction easier to inspect.&lt;/p&gt;

&lt;p&gt;Committing in the multilinear Lagrange basis&lt;/p&gt;

&lt;p&gt;Classic KZG is often introduced for a univariate polynomial in the monomial basis. This prototype instead commits to a multilinear evaluation table in the Lagrange basis over the Boolean hypercube.&lt;/p&gt;

&lt;p&gt;For a secret setup point&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;τ = (τ₁, …, τₙ)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;the setup computes group elements corresponding to the basis evaluations&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;[χb(τ)]1&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;for every Boolean vector b. The commitment is then&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C = Σb ∈ {0,1}&lt;sup&gt;n&lt;/sup&gt; f(b)[χb(τ)]1&lt;br&gt;= [f̃(τ)]1&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The commitment code mirrors that inner product. Each evaluation scales its&lt;br&gt;
matching basis element, and the results are accumulated in G₁:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;g1_basis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;P&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;G1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;evaluations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;P&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;G1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;commitment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;P&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;G1&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;basis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g1_basis&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.enumerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;scalar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;evaluations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.into_bigint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;commitment&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;basis&lt;/span&gt;&lt;span class="nf"&gt;.mul_bigint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scalar&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;commitment&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the things I like about implementing cryptography in Rust with Arkworks: group and field operations stay explicit, while the type system prevents accidentally multiplying unrelated algebraic objects.&lt;/p&gt;

&lt;p&gt;It also exposed a small but real engineering hazard. This loop indexes one slice using the length of another, while the quotient-commitment path uses zip, which stops at the shorter iterator. Without explicit length checks, malformed dimensions can either panic or be silently truncated. The mathematical definition assumes matching dimensions; the implementation must enforce them.&lt;/p&gt;

&lt;p&gt;Opening the polynomial&lt;/p&gt;

&lt;p&gt;Suppose the prover claims&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;v = f̃(r)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;at the point r = (r₁, …, rₙ).&lt;/p&gt;

&lt;p&gt;For multilinear polynomials, the difference from the claimed value can be decomposed as&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;f̃(X) − v = Σi=1&lt;sup&gt;n&lt;/sup&gt; (Xi − ri)Qi(X)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;where each Qᵢ is a quotient polynomial produced as variables are fixed one by one.&lt;/p&gt;

&lt;p&gt;The prototype constructs each quotient from pairs in the current evaluation table. For a variable Xᵢ, the difference between the upper and lower halves gives the slope along that coordinate. After recording that quotient, the current polynomial is partially evaluated at rᵢ, and the process continues.&lt;/p&gt;

&lt;p&gt;At the end, the remainder should be zero. That is the computational form of the identity above evaluated at the opening point.&lt;/p&gt;

&lt;p&gt;The proof contains the claimed value and commitments to the quotient polynomials. The verifier checks a pairing relation equivalent to&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;e(C − v[G1], [G2]) = ∏i=1&lt;sup&gt;n&lt;/sup&gt; e([Qi(τ)]1, [τi − ri]2)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Arkworks represents the target-group wrapper with additive operators. That is&lt;br&gt;
why the implementation uses + even though the conventional equation shows a&lt;br&gt;
product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;claimed_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g1&lt;/span&gt;&lt;span class="nf"&gt;.mul_bigint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="nf"&gt;.into_bigint&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;P&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;pairing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;commitment&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;claimed_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rhs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;PairingOutput&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ZERO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tau_i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g2_taus&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.enumerate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;r_i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g2&lt;/span&gt;&lt;span class="nf"&gt;.mul_bigint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.into_bigint&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tau_minus_r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tau_i&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r_i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;quotient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;quotient_commitments&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;rhs&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nn"&gt;P&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;pairing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quotient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tau_minus_r&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;lhs&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;rhs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, my tests could generate an opening and verify it successfully. That was useful—but not sufficient.&lt;/p&gt;

&lt;p&gt;The API mistake: the proof carried its own commitment&lt;/p&gt;

&lt;p&gt;My first proof type looked conceptually like this. The highlighted design problem&lt;br&gt;
is that the proof carries the commitment it is supposed to be checked against:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;KzgProof&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;G1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;commitment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;G1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Prover-controlled reference&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;quotient_commitments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;G1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier read commitment from the proof and checked the equation against it.&lt;/p&gt;

&lt;p&gt;The equation can be perfectly valid. The problem is that the prover selected every important input to it: the polynomial, the commitment, the claimed value, and the quotient witnesses.&lt;/p&gt;

&lt;p&gt;Imagine a database commits to a polynomial encoding a table at time t₀. Later, a client asks for the value at point r. The client needs proof that the answer opens the commitment stored at t₀. If the server can return a fresh commitment alongside the answer, it can choose a different polynomial that gives any convenient value at r and generate a valid opening for that new polynomial.&lt;/p&gt;

&lt;p&gt;No pairing check detects this, because the new tuple may be internally correct.&lt;/p&gt;

&lt;p&gt;The issue is not broken elliptic-curve arithmetic. It is a broken statement boundary.&lt;/p&gt;

&lt;p&gt;The original KZG work defines a polynomial commitment as a short value against which later evaluation claims can be checked. That earlier commitment is the reference point; without it, there is no binding claim to verify. See Kate, Zaverucha, and Goldberg’s original construction.&lt;/p&gt;

&lt;p&gt;A better interface&lt;/p&gt;

&lt;p&gt;The commitment should not be part of the opening proof’s authority. The verifier should receive the expected commitment from trusted application state, a transcript, a signed object, a consensus layer, or some other previously fixed source.&lt;/p&gt;

&lt;p&gt;A better separation removes that authority from the proof:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;OpeningProof&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;G1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;quotient_commitments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;G1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The verifier receives the expected commitment independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;verify_opening&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Pairing&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;vk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;VerifierKey&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expected_commitment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;P&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;G1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nn"&gt;P&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ScalarField&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;OpeningProof&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;P&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;G1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;P&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ScalarField&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VerificationError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;validate_dimensions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;verify_pairing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_commitment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This API says more than a comment could:&lt;/p&gt;

&lt;p&gt;the proof supplies an opening, not the identity of the committed object;&lt;/p&gt;

&lt;p&gt;the caller supplies the commitment it expects;&lt;/p&gt;

&lt;p&gt;the verifier only needs a verifier key, not the polynomial or prover state; and&lt;/p&gt;

&lt;p&gt;malformed input is distinct from a well-formed but invalid proof.&lt;/p&gt;

&lt;p&gt;For a transcript-based protocol, I would also absorb the commitment, evaluation point, claimed value, and protocol/domain identifier into the transcript before deriving any Fiat–Shamir challenges. For an application protocol, I would bind the commitment to the relevant record ID, chain, version, and context.&lt;/p&gt;

&lt;p&gt;“The math checks” and “the proof is bound to this application statement” are separate properties. Both need to be designed.&lt;/p&gt;

&lt;p&gt;Other changes needed before production&lt;/p&gt;

&lt;p&gt;The commitment-boundary issue was the most instructive finding, but it was not the only gap.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Separate prover and verifier state&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The current KZG object owns the polynomial and the complete setup. A real verifier should not need the witness polynomial. I would split the code into ProverKey, VerifierKey, commitment, and opening-proof types, with the smallest possible verifier surface.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Treat setup generation as a security protocol&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The prototype initializes its setup from explicit, locally known τᵢ values. That is useful for deterministic tests and completely inappropriate as a trusted setup. If the trapdoor is known, binding can fail because forged openings become possible.&lt;/p&gt;

&lt;p&gt;A production design needs a secure ceremony, a setup inherited from a correctly generated system, or a transparent commitment scheme with different assumptions. The setup’s origin, size, validation, and update story are protocol requirements—not deployment details.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate dimensions before doing algebra&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The implementation should reject:&lt;/p&gt;

&lt;p&gt;non-power-of-two evaluation tables;&lt;/p&gt;

&lt;p&gt;a point whose length differs from the polynomial’s variable count;&lt;/p&gt;

&lt;p&gt;setup vectors of the wrong size;&lt;/p&gt;

&lt;p&gt;the wrong number of quotient commitments; and&lt;/p&gt;

&lt;p&gt;invalid or non-canonical group encodings during deserialization.&lt;/p&gt;

&lt;p&gt;Assertions are helpful during development, but public verification APIs should return structured errors and must not panic on adversarial input.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add serialization and transcript rules&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cryptographic types are not a wire protocol by themselves. A production version needs canonical serialization, subgroup checks, versioning, domain separation, and explicit transcript ordering. Two implementations that perform the same abstract operations can still disagree—or become vulnerable—if they encode or hash them differently.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Test failures, not only successes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My happy-path tests use small explicit polynomials and known opening points. The next useful test suite should mutate one component at a time:&lt;/p&gt;

&lt;p&gt;expected commitment;&lt;/p&gt;

&lt;p&gt;opening point;&lt;/p&gt;

&lt;p&gt;claimed value;&lt;/p&gt;

&lt;p&gt;one quotient commitment;&lt;/p&gt;

&lt;p&gt;proof length; and&lt;/p&gt;

&lt;p&gt;serialized group elements.&lt;/p&gt;

&lt;p&gt;Property tests can compare multilinear evaluation against a slow reference implementation. Fuzzing should target parsers and verification entry points. Benchmarks should separate commitment time, witness generation, and verification.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Be precise about what is—and is not—zero knowledge&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A polynomial commitment and an evaluation proof are not automatically zero knowledge. Depending on the scheme and how it is used, the commitment or openings may leak information. Hiding requires additional randomization and a security argument for the complete protocol.&lt;/p&gt;

&lt;p&gt;This prototype studies commitment and opening mechanics; it does not yet provide a production zero-knowledge polynomial-commitment layer.&lt;/p&gt;

&lt;p&gt;Why this lesson matters beyond KZG&lt;/p&gt;

&lt;p&gt;The same design error appears in many forms:&lt;/p&gt;

&lt;p&gt;verifying a Merkle path against a root supplied by the prover instead of the root stored by the application;&lt;/p&gt;

&lt;p&gt;checking a signature while letting the request choose the public key with no identity binding;&lt;/p&gt;

&lt;p&gt;validating a SNARK against a verification key or public input that is not tied to the intended circuit or state;&lt;/p&gt;

&lt;p&gt;accepting an attestation whose issuer identifier is not anchored to a trusted registry.&lt;/p&gt;

&lt;p&gt;Cryptographic verification proves a precise relation over its inputs. It does not decide whether those inputs are the ones your application meant to trust.&lt;/p&gt;

&lt;p&gt;That responsibility lives at the boundary between cryptographic code and protocol code—which is why the types and function signatures matter so much.&lt;/p&gt;

&lt;p&gt;What I learned&lt;/p&gt;

&lt;p&gt;Implementing the scheme from the evaluation table upward made the quotient identity and pairing equation much more concrete. But the more valuable lesson came after the equation worked:&lt;/p&gt;

&lt;p&gt;A verifier does not merely need valid algebra. It needs a valid statement, anchored to prior state.&lt;/p&gt;

&lt;p&gt;For my next iteration, I would start with the protocol roles and trusted inputs, then make the Rust types encode them. Only after that would I optimize multi-scalar multiplication, batch openings, or benchmark curves.&lt;/p&gt;

&lt;p&gt;That ordering is less exciting than making a proof verify for the first time. It is also what turns a cryptographic experiment into dependable infrastructure.&lt;/p&gt;

&lt;p&gt;The implementation discussed here is in the kzg module of my ZK cryptography study repository. The repository is educational and exploratory; it should not be used as production cryptographic software.&lt;/p&gt;

&lt;p&gt;For more of my implementation notes and protocol work, visit samueldahunsi.me/writing.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>cryptography</category>
      <category>zk</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a Drop-in SLO &amp; Error-Budget Pack for SigNoz Across Traces, Metrics, and Logs</title>
      <dc:creator>Samuel Dahunsi</dc:creator>
      <pubDate>Sun, 26 Jul 2026 10:44:58 +0000</pubDate>
      <link>https://dev.to/psalmuel1st/building-a-drop-in-slo-error-budget-pack-for-signoz-across-traces-metrics-and-logs-4321</link>
      <guid>https://dev.to/psalmuel1st/building-a-drop-in-slo-error-budget-pack-for-signoz-across-traces-metrics-and-logs-4321</guid>
      <description>&lt;p&gt;&lt;em&gt;How I built a production-grade observability pack with multi-window burn rate alerts, custom ClickHouse dashboards, and automated fault-injection drills for the Agents of SigNoz Hackathon.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Hook: Why Most SLO Setup Fails in Practice
&lt;/h2&gt;

&lt;p&gt;Setting up Service Level Objectives (SLOs) sounds simple on paper: pick a target like 99.9% availability, track errors, and stop shipping features when your budget burns out. &lt;/p&gt;

&lt;p&gt;In reality, most teams hit a wall. Raw APM tools show request counts and average latencies, but they don’t tell you &lt;strong&gt;how fast you are consuming your monthly error budget&lt;/strong&gt; or whether a 5-minute fault requires waking up an engineer on call. Worse, configuring multi-window burn rate alerts across three separate telemetry signals (Traces, Metrics, and Logs) usually takes days of custom ClickHouse query tuning and YAML wrangling.&lt;/p&gt;

&lt;p&gt;For the &lt;strong&gt;Agents of SigNoz Hackathon (Track 02: Signals &amp;amp; Dashboards)&lt;/strong&gt;, I wanted to solve this permanently by building the &lt;strong&gt;SigNoz SLO &amp;amp; Error-Budget Pack&lt;/strong&gt;: an all-in-one, drop-in observability suite that instruments Node.js/Express applications across all three OpenTelemetry signals and automatically ships three production-grade dashboards and multi-window burn alerts into SigNoz.&lt;/p&gt;

&lt;p&gt;Here is how I built it, the mathematical decisions behind the error budget burn rates, and how to test it end-to-end with live-fire fault injection drills.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: The 3-Signal Telemetry Pipeline
&lt;/h2&gt;

&lt;p&gt;To make SLO calculation accurate, relying on just one signal is a mistake. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metrics&lt;/strong&gt; provide high-frequency RED (Rate, Errors, Duration) counters without sampling overhead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt; expose p99 latency and child-span performance across downstream API calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs&lt;/strong&gt; provide human-readable diagnostic context when an incident occurs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is how data flows through the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  ┌─────────────────────────────────────────┐
                  │          demo-service (Express)         │
                  │  - tracing.js (OTel SDK)                │
                  │  - RED Middleware (http_requests_total) │
                  │  - Winston Logger (JSON + trace_id)     │
                  └────────────────────┬────────────────────┘
                                       │ OTLP / gRPC &amp;amp; HTTP
                                       ▼
                  ┌─────────────────────────────────────────┐
                  │       SigNoz OpenTelemetry Collector    │
                  └────────────────────┬────────────────────┘
                                       │ ClickHouse Tables
                                       ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                             SigNoz Engine                                   │
│  ┌───────────────────────┐ ┌────────────────────────┐ ┌──────────────────┐  │
│  │ Service Health        │ │ SLO &amp;amp; Error Budget     │ │ Cost Efficiency  │  │
│  │ Dashboard             │ │ Dashboard              │ │ Dashboard        │  │
│  └───────────────────────┘ └────────────────────────┘ └──────────────────┘  │
│                                      │                                      │
│                                      ▼                                      │
│                    Multi-Window Alert Rules (API v2)                        │
│                                      │ Webhook Payload                      │
│                                      ▼                                      │
│                     Webhook Catcher Service (:3002)                         │
└─────────────────────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1. Instrumenting the Three Signals
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A. RED Metrics Middleware
&lt;/h3&gt;

&lt;p&gt;Rather than relying solely on trace sampling for request rates, I implemented custom RED metrics in &lt;code&gt;metrics.js&lt;/code&gt; using &lt;code&gt;@opentelemetry/api-metrics&lt;/code&gt;. Every HTTP request is captured by an Express middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// demo-service/index.js&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;finish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;recordRequest&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This records three crucial series into SigNoz’s &lt;code&gt;signoz_metrics.distributed_time_series_v4&lt;/code&gt; table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;http_requests_total&lt;/code&gt;: Counter partitioned by &lt;code&gt;route&lt;/code&gt;, &lt;code&gt;method&lt;/code&gt;, and &lt;code&gt;status_class&lt;/code&gt; (&lt;code&gt;2xx&lt;/code&gt;, &lt;code&gt;4xx&lt;/code&gt;, &lt;code&gt;5xx&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;http_request_duration&lt;/code&gt;: Histogram tracking bucketed latency distributions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;downstream_errors_total&lt;/code&gt;: Counter tracking third-party API dependencies (&lt;code&gt;catfact&lt;/code&gt;, &lt;code&gt;dogimg&lt;/code&gt;, &lt;code&gt;joke&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  B. Nested Traces with Downstream Attributes
&lt;/h3&gt;

&lt;p&gt;For distributed tracing, the &lt;code&gt;/data&lt;/code&gt; endpoint fans out requests to multiple external APIs in parallel. To observe downstream degradation without breaking root trace spans, each downstream call is wrapped in a explicit child span:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;span&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slo-pack-demo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`downstream &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttributes&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downstream.name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http.response.status_code&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downstream.duration_ms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;recordDownstreamError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttributes&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;downstream.name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  C. 100% Trace-Correlated Logs
&lt;/h3&gt;

&lt;p&gt;One of the biggest friction points in debugging alerts is jumping from a metrics spike to log output. By configuring &lt;code&gt;winston&lt;/code&gt; alongside &lt;code&gt;@opentelemetry/instrumentation-winston&lt;/code&gt;, trace context is automatically injected into every stdout log line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"handling /error -&amp;gt; 500"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slo-pack-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trace_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4bf92f3577b34da6a3ce929d0e0e4736"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"span_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"00f067aa0ba902b7"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the SigNoz UI, clicking any log entry instantly renders the exact visual trace waterfall.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Deriving the SLO &amp;amp; Burn Rate Mathematics
&lt;/h2&gt;

&lt;p&gt;Google’s SRE Handbook recommends multi-window, multi-threshold alerting to avoid alert fatigue. Instead of triggering an alert the second error rate hits 0.1%, we measure &lt;strong&gt;how fast the error budget is burning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a 30-day SLO window with a &lt;strong&gt;99.9% Availability Target&lt;/strong&gt;:&lt;br&gt;
$$\text{Allowed Unreliability } (U) = 1 - 0.999 = 0.001 \quad (0.1\%)$$&lt;/p&gt;
&lt;h3&gt;
  
  
  Fast-Burn Alert (Critical)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goal&lt;/strong&gt;: Catch massive outages that would consume 2% of the total monthly error budget in 1 hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burn Rate Formula&lt;/strong&gt;: 
$$\text{Burn Rate} = \frac{\text{Budget Consumed}}{\text{Time Ratio}} = \frac{0.02}{1 \text{ hour} / 720 \text{ hours}} = 14.4$$&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition&lt;/strong&gt;: If the current 1-hour burn rate exceeds &lt;strong&gt;14.4x&lt;/strong&gt;, trigger a &lt;strong&gt;Critical Alert&lt;/strong&gt; immediately.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Slow-Burn Alert (Warning)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Goal&lt;/strong&gt;: Catch persistent, subtle error leaks that consume 5% of the error budget over 6 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burn Rate Formula&lt;/strong&gt;:
$$\text{Burn Rate} = \frac{0.05}{6 \text{ hours} / 720 \text{ hours}} = 6.0$$&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Condition&lt;/strong&gt;: If the 6-hour burn rate exceeds &lt;strong&gt;6.0x&lt;/strong&gt;, trigger a &lt;strong&gt;Warning Alert&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These exact mathematical expressions were converted into SigNoz API v2 alert definitions under &lt;code&gt;alerts/fast-burn.json&lt;/code&gt;, &lt;code&gt;alerts/slow-burn.json&lt;/code&gt;, and &lt;code&gt;alerts/latency-slo.json&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  3. The 3 Custom Dashboards
&lt;/h2&gt;

&lt;p&gt;The pack includes three pre-configured dashboards in &lt;code&gt;dashboards/&lt;/code&gt;:&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Service Health Dashboard
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request Throughput&lt;/strong&gt;: Real-time req/sec grouped by status class (&lt;code&gt;2xx&lt;/code&gt; vs &lt;code&gt;5xx&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency Percentiles&lt;/strong&gt;: p50, p90, and p99 derived directly from trace span durations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Downstream Error Tracker&lt;/strong&gt;: Isolates third-party API failures from internal app errors.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  2. SLO / Error Budget Dashboard
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Availability SLI %&lt;/strong&gt;: Computed in real-time as &lt;code&gt;(1 - (sum(5xx) / sum(total))) * 100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remaining Error Budget %&lt;/strong&gt;: Gauge chart highlighting budget degradation before breach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burn Rate Gauges&lt;/strong&gt;: Live 14.4x fast-burn and 6.0x slow-burn indicators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency SLI %&lt;/strong&gt;: Percentage of requests completing under the 6-second p99 ceiling.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  3. Cost-Efficiency &amp;amp; Waste Dashboard
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Waste Ratio %&lt;/strong&gt;: Quantifies wasted compute spent servicing HTTP 5xx responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Waste Index&lt;/strong&gt;: A hybrid metric ($Latency \times ErrorRatio$) pointing out high-latency, error-prone endpoints that consume disproportionate server resources.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  4. Automated Verification &amp;amp; Live-Fire Alert Drills
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Automated Test Harness
&lt;/h3&gt;

&lt;p&gt;To ensure the pack remains reliable across environments, I wrote a verification script (&lt;code&gt;scripts/verify-p1.sh&lt;/code&gt;) that queries both the SigNoz REST API and ClickHouse database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SIGNOZ_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"dahunsisamuel1st@gmail.com"&lt;/span&gt; &lt;span class="nv"&gt;SIGNOZ_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt; ./scripts/verify-p1.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== A. Stack &amp;amp; reproducibility (PRD §7.6)
PASS  A1   SigNoz UI + API healthy
PASS  A2   OTLP HTTP ingest endpoint accepting
PASS  A3   demo service /healthz responds
PASS  A4   webhook catcher up
PASS  A5   casting.yaml + lock committed
== B. All three signals w/ required shape
PASS  B1   traces landing (fresh &amp;lt;2m)
PASS  B2   downstream child spans carry custom attrs
PASS  B3   http_requests_total with route/status_class labels
PASS  B4   http_request_duration histogram series present
PASS  B5   downstream_errors_total with downstream label
PASS  B6   metric samples fresh (&amp;lt;2m)
PASS  B7   logs landing (fresh &amp;lt;2m)
PASS  B8   &amp;gt;=95% of app logs trace-correlated
== C. Alert pack
PASS  C1   webhook notification channel registered
PASS  C2   rule 'SLO fast burn' exists w/ severity=critical
PASS  C3   rule 'SLO slow burn' exists w/ severity=warning
PASS  C4   rule 'Latency SLO breach' exists w/ severity=warning
PASS  C5   no rule stuck without state
== D. Paper requirements
PASS  D1   AI_USAGE.md present
PASS  D2   alert payloads committed
PASS  D3   SLO math documented

passed: 21  failed: 0  skipped-sections: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Live-Fire Fault Injection
&lt;/h3&gt;

&lt;p&gt;To test alerting end-to-end without waiting for natural outages, I built environment knobs into the containerized service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Inject 90% error rate — forces burn rate to ~18x&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;demo-service
&lt;span class="nv"&gt;ERROR_RATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.9 docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; demo-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within minutes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;Availability SLI&lt;/strong&gt; dropped on the SLO Dashboard.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;SLO Fast-Burn Rule&lt;/strong&gt; turned red (&lt;code&gt;FIRING&lt;/code&gt;) in SigNoz.&lt;/li&gt;
&lt;li&gt;The standalone &lt;strong&gt;Webhook Catcher&lt;/strong&gt; (&lt;code&gt;http://localhost:3002/&lt;/code&gt;) logged the incoming incident JSON payload:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"firing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alerts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"labels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"alertname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SLO fast burn — availability error budget"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slo-pack-demo"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"annotations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"High availability error budget burn rate detected"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resetting the container restores normal operation immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ERROR_RATE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.1 &lt;span class="nv"&gt;SLOW_MAX_MS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000 docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; demo-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Lessons Learned &amp;amp; Gotchas
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ClickHouse JSON Ingestion Behavior&lt;/strong&gt;: When querying &lt;code&gt;signoz_logs.distributed_logs_v2&lt;/code&gt;, &lt;code&gt;resources_string['service.name']&lt;/code&gt; must match exact string casing. Using &lt;code&gt;LOWER()&lt;/code&gt; or un-indexed metadata queries can degrade query times during dashboard renders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bash Execution &amp;amp; &lt;code&gt;curl -sf&lt;/code&gt;&lt;/strong&gt;: When automating API provisioning scripts under &lt;code&gt;set -e&lt;/code&gt;, avoid using &lt;code&gt;curl -sf&lt;/code&gt; on authentication endpoints. Standard HTTP 401 responses will cause &lt;code&gt;curl&lt;/code&gt; to exit non-zero silently without outputting SigNoz's JSON error response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace-Log Context Ingestion&lt;/strong&gt;: OpenTelemetry Winston instrumentation requires standard stdout JSON formatting to reliably extract &lt;code&gt;trace_id&lt;/code&gt; and &lt;code&gt;span_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; Links
&lt;/h2&gt;

&lt;p&gt;Building this pack demonstrated how powerful OpenTelemetry and SigNoz are when all three signals work in unison. Instead of staring at disconnected graphs during an incident, engineering teams get a single pane of glass showing &lt;strong&gt;budget remaining, exact firing alerts, and trace-correlated logs&lt;/strong&gt; within seconds.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/Psalmuel01/signoz-slo-pack" rel="noopener noreferrer"&gt;signoz-slo-pack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SigNoz Documentation&lt;/strong&gt;: &lt;a href="https://signoz.io/docs" rel="noopener noreferrer"&gt;https://signoz.io/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry Documentation&lt;/strong&gt;: &lt;a href="https://opentelemetry.io/docs" rel="noopener noreferrer"&gt;https://opentelemetry.io/docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>signoz</category>
      <category>agents</category>
      <category>programming</category>
    </item>
    <item>
      <title>What a Corrupted ClickHouse Binary Taught Me About Self-Hosting SigNoz</title>
      <dc:creator>Samuel Dahunsi</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:58:33 +0000</pubDate>
      <link>https://dev.to/psalmuel1st/what-a-corrupted-clickhouse-binary-taught-me-about-self-hosting-signoz-15f2</link>
      <guid>https://dev.to/psalmuel1st/what-a-corrupted-clickhouse-binary-taught-me-about-self-hosting-signoz-15f2</guid>
      <description>&lt;p&gt;&lt;strong&gt;Three crash-looping containers, one corrupted Docker image, and a trace waterfall that made the entire detour worthwhile.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My SigNoz containers kept crash-looping with &lt;code&gt;CANNOT_PARSE_ELF&lt;/code&gt; and &lt;code&gt;Segmentation fault&lt;/code&gt;. The install guide doesn't mention either error, because neither is SigNoz's fault. Both are what happens when your machine dies mid &lt;code&gt;docker pull&lt;/code&gt;. This is the full trail: what broke, why, and how I got traces flowing anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I was doing this
&lt;/h2&gt;

&lt;p&gt;I wanted to actually understand observability, not just read about it. The plan was simple: self-host SigNoz, point a real app at it, and poke at traces, logs, and dashboards until the concepts clicked.&lt;/p&gt;

&lt;p&gt;Quick vocabulary, since these words get thrown around a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Telemetry&lt;/strong&gt; is the data an app emits about what it's doing while it runs, essentially a flight recorder for software.&lt;/li&gt;
&lt;li&gt;It comes in three flavors: &lt;strong&gt;traces&lt;/strong&gt; (the journey of one request through your system), &lt;strong&gt;metrics&lt;/strong&gt; (numbers over time, like requests/sec), and &lt;strong&gt;logs&lt;/strong&gt; (the text your app prints).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry (OTel)&lt;/strong&gt; is the vendor-neutral standard for formatting and shipping that data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SigNoz&lt;/strong&gt; receives all of it, stores it, and gives you dashboards, search, and alerts so you can make sense of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My setup: a Mac, Docker Desktop, and SigNoz's newer installer, Foundry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clean install (this part is easy)
&lt;/h2&gt;

&lt;p&gt;SigNoz has moved off the old &lt;code&gt;docker-compose&lt;/code&gt; instructions to a CLI called Foundry. Three steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Install the Foundry CLI (I downloaded and read it before running it)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://signoz.io/foundry.sh &lt;span class="nt"&gt;-o&lt;/span&gt; foundry.sh
bash foundry.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. A tiny config file, casting.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Installation&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;signoz&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;flavor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;compose&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 3. Deploy&lt;/span&gt;
foundryctl cast &lt;span class="nt"&gt;-f&lt;/span&gt; casting.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That generates a real Compose file under &lt;code&gt;pours/deployment/&lt;/code&gt; and starts every container SigNoz needs: ClickHouse (the database), ClickHouse Keeper (coordination), Postgres (metadata), an OTel Collector (the thing your app sends data to), and the SigNoz app itself.&lt;/p&gt;

&lt;p&gt;That part is in the docs. Here's the part that isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it broke
&lt;/h2&gt;

&lt;p&gt;My first deploy got interrupted while Docker was still pulling images. When I brought the stack back up, three containers went into a crash-loop. The logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Code: 464. DB::Exception: The ELF '/usr/bin/clickhouse' doesn't have
string table with section names. (CANNOT_PARSE_ELF)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Segmentation fault
Segmentation fault
Segmentation fault
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;initdb: error: directory "/var/lib/postgresql/data" exists but is not empty
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three different services, three different errors. It looked like SigNoz was badly broken. It wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnosis
&lt;/h2&gt;

&lt;p&gt;The common thread is that all three failures are about &lt;em&gt;half-written files&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Docker Desktop on a Mac doesn't run containers directly on macOS. It runs a hidden Linux VM, and every image, container, and database volume lives on one big virtual disk inside that VM. When Docker (or the whole machine) is killed abruptly instead of shut down cleanly, whatever was mid-write gets left truncated, the same way a file can end up corrupted if you yank a USB stick out while it's still saving.&lt;/p&gt;

&lt;p&gt;Mapping the errors back:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;What actually happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CANNOT_PARSE_ELF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The ClickHouse binary inside the image was written only partway, so Linux can't parse it as an ELF file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Segmentation fault&lt;/code&gt; (collector)&lt;/td&gt;
&lt;td&gt;Same story: an incomplete binary crashing the instant it runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Postgres &lt;code&gt;directory exists but is not empty&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Postgres got interrupted mid-initialization and left a half-built data folder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of this is a SigNoz bug. It's what an interrupted write looks like at three different layers of the stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The recovery recipe is the same every time: delete the corrupted artifact and recreate it from scratch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stop everything&lt;/span&gt;
docker compose down

&lt;span class="c"&gt;# Remove the corrupted images so they re-pull cleanly&lt;/span&gt;
docker rmi &lt;span class="nt"&gt;-f&lt;/span&gt; clickhouse/clickhouse-server:25.12.5
docker rmi &lt;span class="nt"&gt;-f&lt;/span&gt; signoz/signoz-otel-collector:latest

&lt;span class="c"&gt;# Remove the half-initialized Postgres volume&lt;/span&gt;
docker volume &lt;span class="nb"&gt;rm &lt;/span&gt;signoz-metastore-postgres-0-data

&lt;span class="c"&gt;# Re-pull and verify the binary is actually intact this time&lt;/span&gt;
docker compose pull
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; clickhouse &lt;span class="se"&gt;\&lt;/span&gt;
  clickhouse/clickhouse-server:25.12.5 &lt;span class="nb"&gt;local&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# -&amp;gt; ClickHouse local version 25.12.5.44 (official build)   ✅&lt;/span&gt;

&lt;span class="c"&gt;# Bring it back up&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last verify step is the one I'd skip in a hurry, and the one I'd regret skipping. Checking the binary before starting the stack turns a 20-minute crash-loop mystery into a five-second yes/no.&lt;/p&gt;

&lt;p&gt;After that, every container came up healthy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;signoz-signoz-0                            Up (healthy)
signoz-telemetrystore-clickhouse-0-0       Up (healthy)
signoz-metastore-postgres-0                Up (healthy)
signoz-telemetrykeeper-clickhousekeeper-0  Up (healthy)
signoz-ingester-1                          Up
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SigNoz UI came up at &lt;code&gt;http://localhost:8080&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;the SigNoz home dashboard showing traces and metrics ingestion both active&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F43zfrg57jvf1p359cs8m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F43zfrg57jvf1p359cs8m.png" alt="home dashboard" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting real data in
&lt;/h2&gt;

&lt;p&gt;An empty observability tool is useless, so I grabbed SigNoz's Node.js sample app, which is already OpenTelemetry-instrumented. It has one endpoint, &lt;code&gt;/data&lt;/code&gt;, that calls three public APIs in parallel: a cat fact, a dog photo, and a joke.&lt;/p&gt;

&lt;p&gt;The only change needed was pointing it at my local collector instead of SigNoz Cloud, in &lt;code&gt;tracing.js&lt;/code&gt;. Hitting the endpoint directly confirms what it returns:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;the /data endpoint's JSON response showing catFact, dogImage, and joke fields&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi3n19xj7iggpq9bnqwcz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi3n19xj7iggpq9bnqwcz.png" alt="data endpoint response" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exporterOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://host.docker.internal:4318/v1/traces&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;host.docker.internal&lt;/code&gt; is how a container reaches a service running on the host machine, in this case the OTel Collector listening on port 4318.&lt;/p&gt;

&lt;p&gt;One gotcha worth saving you the hour I lost: on my network, the app's outbound calls kept failing with &lt;code&gt;connect ENETUNREACH&lt;/code&gt; because Node was resolving the APIs to IPv6 addresses my router wasn't routing. The fix was one line in the Compose file to prefer IPv4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NODE_OPTIONS=--dns-result-order=ipv4first&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a build, and a burst of traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 20&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:3000/data&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The feature I liked most: the trace waterfall
&lt;/h2&gt;

&lt;p&gt;This is where tracing earned its keep. A burst of traffic against &lt;code&gt;/data&lt;/code&gt; lands as a flat list first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;the Traces Explorer list view showing spans from a burst of traffic against /data&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftl5fsnssn78z1s23rkqh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftl5fsnssn78z1s23rkqh.png" alt="traces list" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click into one trace and the &lt;code&gt;/data&lt;/code&gt; endpoint fires three API calls at once with &lt;code&gt;Promise.all&lt;/code&gt;, and the trace waterfall shows exactly that: a parent &lt;code&gt;GET /data&lt;/code&gt; span with three child spans running side by side.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;the /data trace waterfall showing a parent span with three parallel child spans&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8h42njhstl8l5lgp2r8r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8h42njhstl8l5lgp2r8r.png" alt="trace waterfall" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The insight lands in one glance. Because the three calls are parallel, the total response time isn't their sum, it's whichever single call was slowest. When a request was slow, I could see which of the three APIs dragged it, not guess. That's the whole pitch of distributed tracing, made concrete in one screen.&lt;/p&gt;

&lt;p&gt;The part that genuinely surprised me: I wrote &lt;strong&gt;zero&lt;/strong&gt; tracing code to get this. No manual spans, no timers. &lt;code&gt;getNodeAutoInstrumentations()&lt;/code&gt; wrapped Express and axios automatically and produced the entire waterfall, including each outbound call's URL, method, and status code as span attributes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;a single GET span expanded, showing auto-captured HTTP attributes like http.method, http.url, and http.status_code&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdphjfebnevrdkflp8556.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdphjfebnevrdkflp8556.png" alt="span attributes" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked, what didn't
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Worked:&lt;/strong&gt; traces, immediately and impressively. 218 spans from one burst of traffic, waterfalls rendering with parallel child spans and full HTTP attributes, no custom instrumentation code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also worked:&lt;/strong&gt; metrics. &lt;code&gt;http.client.duration.bucket&lt;/code&gt; and &lt;code&gt;http.server.duration.bucket&lt;/code&gt; both showed up in the Metrics Explorer with thousands of samples, no extra config beyond what Foundry sets up by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;[the Metrics Explorer showing http.client.duration.bucket and http.server.duration.bucket with 12.1K+ and 3.2K+ samples]&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxo1b47ko1x0224crzdnu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxo1b47ko1x0224crzdnu.png" alt="metrics explorer" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Didn't (yet):&lt;/strong&gt; logs. I wired up an OTel log exporter too, but it didn't land in SigNoz on the first pass while traces and metrics did. Traces, logs, and metrics are three separate export pipelines, and getting two working doesn't mean the third is, a lesson I'd rather learn on a toy app than in production. That's my next debugging session.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The big one:&lt;/strong&gt; an abrupt shutdown can corrupt Docker's VM disk, and the fix is always the same: delete the broken image or volume and recreate it. If your machine has ever hung and forced you to hard-quit, this is worth filing away for next time.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The install is genuinely three commands. The real learning was in the failure: reading &lt;code&gt;CANNOT_PARSE_ELF&lt;/code&gt;, a segfault, and a Postgres init error, and realizing they were all the same problem wearing different masks. Traces made my toy app's behavior obvious at a glance, and I got there without writing a line of instrumentation. Metrics came through cleanly too. Logs are the unfinished piece, which feels like the right note to end a first-week-with-a-new-tool post on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SigNoz docs: &lt;a href="https://signoz.io/docs" rel="noopener noreferrer"&gt;https://signoz.io/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Foundry install guide: &lt;a href="https://signoz.io/docs/install/docker/" rel="noopener noreferrer"&gt;https://signoz.io/docs/install/docker/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenTelemetry: &lt;a href="https://opentelemetry.io" rel="noopener noreferrer"&gt;https://opentelemetry.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;SigNoz Node.js sample app: &lt;a href="https://github.com/SigNoz/sample-nodejs-app" rel="noopener noreferrer"&gt;https://github.com/SigNoz/sample-nodejs-app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>signoz</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
