and runs during HTML parse, but hubspotutk is set by js.hs-analytics.net, measured loading at ~834ms. On a first visit the cookie does not exist yet, so a single read returns nothing and the visitor gets no external ID at all — and a first visit is exactly what an ad click is. So poll briefly until HubSpot sets it. The SDK loads lazily (~4.5s) and replays a queue, so an init queued a second or two in still lands before any events are transmitted. */ (function attachExternalId() { var attempts = 0; var MAX_ATTEMPTS = 40; /* 40 x 200ms = 8s, then give up quietly */ var timer = null; function tryAttach() { var raw = getExternalId(); if (!raw) { if (++attempts >= MAX_ATTEMPTS && timer) clearInterval(timer); return; } if (timer) clearInterval(timer); sha256Hex(raw).then(function (hash) { oaiq("init", { user: { external_id_sha256: hash } }); }).catch(function () {}); } tryAttach(); /* returning visitor: immediate */ if (!getExternalId()) timer = setInterval(tryAttach, 200); })(); /* --------------------------------------------------------------------------- 2. Load the SDK off the critical path Whichever comes first: the main thread going idle after load, or the first sign of real user intent. No preconnect on purpose — a TLS handshake during LCP for a script we deliberately deferred is a net loss. Measured on clickpost.ai: load event 3614ms, SDK fetched 4508ms. ------------------------------------------------------------------------ */ var loaded = false; var INTENT = ["pointerdown", "keydown", "scroll"]; function loadSdk() { if (loaded) return; loaded = true; INTENT.forEach(function (t) { removeEventListener(t, loadSdk, { passive: true }); }); var s = document.createElement("script"); s.src = SDK; s.async = true; s.fetchPriority = "low"; document.head.appendChild(s); } INTENT.forEach(function (t) { addEventListener(t, loadSdk, { passive: true, once: true }); }); addEventListener("load", function () { window.requestIdleCallback ? requestIdleCallback(loadSdk, { timeout: 2500 }) : setTimeout(loadSdk, 1500); }, { once: true }); /* --------------------------------------------------------------------------- 3. Conversion id sha256(lowercase(email) + "|" + formId + "|" + ISO-hour), first 20 hex, "lead_" prefix. - email lowercased/trimmed so casing does not fork the id - formId keeps two different forms in the same hour from colliding - the hour bucket is only the server's fallback; the id computed here is sent to the relay verbatim, so the two paths cannot disagree even when a submit straddles an hour boundary Ads Manager's own sample omits event_id. We send it so the pixel event and the relay event collapse into one conversion instead of counting twice. ------------------------------------------------------------------------ */ function buildEventId(email, formId) { var seed = [ String(email || "").trim().toLowerCase(), String(formId || ""), new Date().toISOString().slice(0, 13) ].join("|"); return crypto.subtle.digest("SHA-256", new TextEncoder().encode(seed)).then(function (buf) { var hex = Array.prototype.map.call(new Uint8Array(buf), function (b) { return b.toString(16).padStart(2, "0"); }).join(""); return "lead_" + hex.slice(0, 20); }); } /* Guards a re-fire from the history.pushState that book-a-demo-core.js runs after submit, and from back-navigation onto the same state. */ function firedOnce(id) { try { if (sessionStorage.getItem("oaiq_" + id)) return true; sessionStorage.setItem("oaiq_" + id, "1"); return false; } catch (e) { return false; /* private mode / blocked storage: fire rather than lose it */ } } /* Path 2. keepalive lets the request finish even if the page navigates away. Failures are swallowed deliberately: a reporting call must never surface an error to someone who just submitted a form. The relay hashes the email server-side into user.emails_sha256 and adds the caller's IP and user agent, so this event carries the same match signals the pixel gets for free. */ function sendToRelay(email, formId, eventType, eventId) { try { var payload = { token: RELAY_TOKEN, email: email, form_id: formId || "", event_type: eventType, event_id: eventId, /* verbatim, so both paths always match */ source_url: location.href }; var oppref = getOppref(); if (oppref) payload.oppref = oppref; var externalId = getExternalId(); if (externalId) payload.external_id = externalId; fetch(RELAY_URL, { method: "POST", keepalive: true, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }).catch(function () {}); } catch (e) {} } /* Capture the email while the form is still on the page. onFormSubmitted does not reliably carry submissionValues, and by then the form may be removed. */ var lastEmail = ""; document.addEventListener("submit", function (e) { var f = e.target; if (!f || !f.querySelector) return; var i = f.querySelector('input[name="email"], input[type="email"]'); if (i && i.value) lastEmail = i.value; }, true); /* Every HubSpot form on the site reports through this one message event, so no module or theme JS needs editing to add coverage. */ window.addEventListener("message", function (e) { var d = e.data; if (!d || d.type !== "hsFormCallback" || d.eventName !== "onFormSubmitted") return; var email = (d.data && d.data.submissionValues && d.data.submissionValues.email) || lastEmail; if (!email) return; /* without an email there is no stable id to dedupe on */ var eventType = EVENT_BY_FORM[d.id] || EVENT_DEFAULT; buildEventId(email, d.id).then(function (id) { if (firedOnce(id)) return; oaiq("measure", eventType, { type: DATA_TYPE }, { event_id: id }); /* path 1 */ sendToRelay(email, d.id, eventType, id); /* path 2 */ }); }); })();

91ÊÓƵ

See where your logistics stands on automation Start assessment

Home > Blog >

How We Rebuilt Observability From Scratch (and Saved a Fortune)

How We Rebuilt Observability From Scratch (and Saved a Fortune)

Amit Bhushan
By Amit Bhushan

Ready to ship smarter? Talk to our logistics experts.

Trusted by 600+ brands

In this blog

    “Monitoring tells you when something is wrong. Observability helps you understand why.”

    The “Why”: From Monitoring to Observability

    You might wonder, “Don’t we already monitor our services?”
    While monitoring is a standard practice, it’s reactive. It answers basic questions like “Is my API returning 200s?” or “Is CPU usage high?”, but when issues arise, you’re left sifting through logs and metrics. 

    Observability, on the other hand, is about understanding your system deeply enough to answer new, unanticipated questions quickly. It provides insights before problems arise, rather than just tracking them after. That’s the journey we started at ClickPost.

    The Challenge: High Cost and Low Control

    We started with New Relic, but over time, it became clear it wasn’t right for us. The costs were high especially after estimating usage through the and we lacked control over critical aspects like trace retention and data residency. Additionally, its lack of flexibility made it difficult to customize trace ingestion. We needed a solution that was affordable, customizable, and scalable. Hence, we decided to build our own.

    Here’s what we faced:

    • Expensive: New Relic’s pricing didn’t scale with our growth.
    • Opaque: No control over trace retention, processing rules, or data residency.
    • Lacked Flexibility: We couldn’t tailor trace ingestion or instrument batch flows.

    We needed answers quickly, not just more dashboards. So, we built our own solution.

    The Stack: Open-Source Observability at Scale

    To create a solution that met our needs, we turned to open-source tools. The foundation of our system is OpenTelemetry, combined with tools like Grafana for visualization and Prometheus for metrics. We deconstructed a $100k+ observability SaaS offering and rebuilt it in-house; for a fraction of the cost, and full control.

    Here’s what powers our custom stack:

    • Tracing: OpenTelemetry + Grafana Tempo (S3 as backend)

    • Trace-to-Metrics: OpenTelemetry Collector with spanmetrics

    • Metrics: Prometheus + Grafana Mimir (S3 as backend)

    • Visualization: Grafana OSS

    • Logs: (in progress) Grafana Loki

    • Customization: Our own OpenTelemetry Collector build in Go

    • Instrumentation: OpenTelemetry SDKs and agents for Java and Python

    The How: Instrument Everything

    Instrumentation was our first step. We used OpenTelemetry’s auto-instrumentation to track data from Java and Python services. For our highly-used Kafka service, we manually instrumented it to get an end-to-end trace of our transactions. This setup ensures we can track and visualize every critical aspect of our system.

    Here’s how we did it:

    • Java services: Auto-instrumentation using the opentelemetry-javaagent.jar

    • Python apps: Auto-instrumentation using opentelemetry-instrument for Django

    • Nomad jobs: Manual-instrumentation with an init_tracing script.

    • Kafka Consumer: init_tracing script and custom consumer span

    We used OTLP GRPC exporters to forward trace data to our custom local OpenTelemetry Collector. The Collector was configured with a memory limiter and batch processor to control resource usage and avoid overwhelming future components like Prometheus and Tempo.

    The Real Magic: Collector + Trace-to-Metrics

    Once we started sending the traces to the Collector, it worked its magic by converting spans into Prometheus metrics. This gave us the ability to easily track metrics like request counts, latencies, and error rates without needing to write custom metric code.

    At the same time, we used the default Language for simple transformations on the received OTel data in the collector. This enabled custom views without adding complexity. Now, we can create customized dashboards that fit our needs.

    Going Deeper: Tailoring Observability to Our Needs

    While OpenTelemetry provided a strong foundation, we needed more. Two pain points stood out:

    1. Breaking Down Parent Traces 

    Large parent traces often collapsed important details. We couldn’t easily segment them by consumer name, trace parent name, or other dimensions. To solve this, we built a custom Go-based processor plugin inside our own distribution of the custom OpenTelemetry Collector. This plugin injected extra metadata into spans, allowing us to slice traces more intelligently. Suddenly, instead of staring at a flat parent trace, we could break it down into meaningful segments and analyse them with query languages and beautiful dashboards.

    2. Fixing Noisy Span Names

    Out-of-the-box instrumentation often gives you generic span names like HTTP GET or SELECT. That works for basic tracing, but when you’re viewing them at a glance, it becomes noisy and hard to interpret. We wanted spans that were human-readable and directly tied to business logic. In Python, we forked , modified the span naming logic, and deployed our fork across services. In Java, we achieved the same using OpenTelemetry extensions, overriding default span names for HTTP operations. The result: our traces told a clear story instead of drowning us in noise.

    These two changes — segmentation and human-readable span names — gave us clarity beyond most commercial APMs. Developers could debug faster, understand flows at a glance, and trust what the traces were saying.

    Dashboards That Tell Stories

    Metrics and traces are only as good as the insights you can derive from them. For us, Grafana wasn’t just a place to dump numbers — it became the canvas where system behavior turned into stories that engineers could act on. Here’s how we structured our dashboards:

    Traces in Grafana

    We started with the foundation: traces.
    With Tempo’s out-of-the-box integration in Grafana, we could drill into a single request — from the incoming API call all the way to the database query.

    That meant developers could see:

    • Latency at the span level

    • The exact SQL queries being executed

    • How slowdowns mapped directly to code paths

    Instead of chasing logs in the dark, traces lit up the system’s flow like a map.

    trace

    APM Dashboards

    On top of traces, we layered APM-style dashboards to get a high-level view of application health. These boards became our quick pulse check:

     

    • Request rates, error distributions, latency percentiles

    • Heatmaps catching tail latencies that averages usually hide

    • Request distribution charts showing which routes were really driving system load

    This way, we could quickly tell if a spike in latency was because of DB queries… or just uneven traffic.

    APM-Dashboards

    apm-dashboard-3

    Segment Breakdown Dashboards

    But sometimes, a parent trace was just too broad to be useful.
    So, we built segment breakdown dashboards from our custom collector.

    This gave us the power to slice traces by:

     

    • API route

    • Consumer

    • Or other attributes we cared about

    From there, we had span-level insights of a particular trace along with pie charts of load share and latency distributions (P5/P50/P95).

     

    • Which DB operations were the real bottlenecks

    • Which consumer was causing latency spikes right now

    In short — it gave us the “why” behind the trace.

    apm-dashboard-4

    JVM Metrics Dashboards: 

    Finally, we complement traces with JVM-level dashboards for our Java/Kotlin services. These cover CPU utilization, heap memory trends, thread states, and garbage collection behavior. Instead of waiting for “Out of Memory” errors, we can proactively watch heap usage patterns. Thread breakdowns help us identify bottlenecks like blocked threads or runaway pools before they escalate.

    JVM-Metrics-Dashboards

    Together, these dashboards create a multi-layered view — from a single slow query in a trace, to its impact on latency distributions, to JVM resource pressure on the host. They don’t just surface metrics; they let us connect dots, explain behavior, and make performance actionable.

    Why We Invested

    As Andrew Mallaband puts it in his excellent piece:

    “The cost of neglecting system visibility far outweighs the investment in it. This cost is not static; it is a compounding financial risk that escalates as your services scale and your business becomes more dependent on its digital infrastructure.”

     

    We saw this first-hand. Outages don’t just drain revenue — they pull engineers away from building new features, frustrate customers, and create cascading risks.

    That’s why we invested the time to build observability right, in-house.

    Cost vs Power: Why This Was Worth It

    Yes — building our system took time and effort. But the benefits far outweighed the challenges. Beyond saving costs, we gained flexibility, transparency, and long-term control.

    Here’s what we control now:

    • No Vendor Lock-In – fully open-source, evolving at our pace

    • Cost Control – no surprise bills; we define spend via infra & retention policies

    • Data Ownership – security & compliance in our hands

    • Storage Flexibility – choose retention period, granularity, and storage strategy

    • Customisation – enrich spans with attributes, build custom aggregations

    • Custom Dashboards & Alerts – tuned exactly to our business and tech needs

    • Future-Proofing – with OpenTelemetry, we’re ready for whatever comes next

    This isn’t just an observability platform. It’s an investment in resilience and cost predictability.

    What’s Next

    We’re not stopping here. As our system evolves, we’ll continue to add new features. Here’s what’s on the roadmap:

     

    • Frontend Telemetry: Introduce browser-side tracing with OpenTelemetry JS to capture page loads, API calls, and user interactions—giving us end-to-end visibility from click to backend.

    • EDA Instrumentation: Extending instrumentation to other event-driven architectures flows like RabbitMQ.

    • Logging Pipelines: Integrate Loki for advanced logging and correlation with traces and metrics.

    • Custom Flows: Perfecting flows to comprehensively visualize entire transactional journeys across services.

    • AI/ML-assisted Detection: Experiment with Grafana Machine Learning plugins for anomaly detection and forecasting in metrics.

    We’ll keep improving our observability system to stay ahead of our needs.

    Why You Should Care

    If you’re at a startup or scaling business, you know how critical observability is. But observability doesn’t have to break the bank; it can be tailored to your business needs

     

    • Don’t overspend on external tools with limited control.

    • Build a system that fits your specific needs.

    • Own your data, own your insights.

    Take it from us: observability is not optional.

    And when you build it yourself, you don’t just see your system — you own your visibility.

    The Logistics Automation Platform for E-commerce

    Momentum Leader - G2 Users Most Likely To Recommend - G2 Best Results Mid-Market - G2
    . window.disableHubSpotCookieBanner = true; function killNative() { var n = document.getElementById("hs-eu-cookie-confirmation"); if (n && n.parentNode) { n.parentNode.removeChild(n); return true; } return false; } killNative(); if (window.MutationObserver) { var mo = new MutationObserver(function () { killNative(); }); mo.observe(document.documentElement, { childList: true, subtree: true }); setTimeout(function () { mo.disconnect(); }, 15000); } var COOKIE = "cp_cookie_consent"; var EU = ["at","be","bg","hr","cy","cz","dk","ee","fi","fr","de","gr","hu","ie","it","lv","lt","lu","mt","nl","pl","pt","ro","sk","si","es","se","gb","uk","is","li","no","ch"]; function readCookie(nm) { var m = document.cookie.match(new RegExp("(?:^|; )" + nm + "=([^;]*)")); return m ? decodeURIComponent(m[1]) : null; } function writeCookie(nm, val, days) { var d = new Date(); d.setTime(d.getTime() + (days * 864e5)); document.cookie = nm + "=" + encodeURIComponent(val) + ";expires=" + d.toUTCString() + ";path=/;SameSite=Lax" + (location.protocol === "https:" ? ";Secure" : ""); } function stored() { var raw = readCookie(COOKIE); if (!raw) return null; try { var o = JSON.parse(raw); if (typeof o.analytics === "boolean") return o; } catch (e) {} return null; } function apply(state, revoke) { window._hsp = window._hsp || []; window._hsp.push(["setHubSpotConsent", { analytics: !!state.analytics, advertisement: !!state.advertisement, functionality: !!state.functionality }]); if (revoke && !state.analytics && !state.advertisement && !state.functionality) { window._hsp.push(["revokeCookieConsent"]); } window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: "cp_cookie_consent", cp_consent_analytics: !!state.analytics, cp_consent_advertisement: !!state.advertisement, cp_consent_functionality: !!state.functionality }); document.documentElement.setAttribute("data-cp-consent", "set"); } function inScope(banner) { if (banner.getAttribute("data-scope") !== "eu_only") return true; var geo = (document.body.getAttribute("data-geo_location") || "").toLowerCase(); if (!geo) return true; return EU.indexOf(geo) !== -1; } function init(banner) { var days = parseInt(banner.getAttribute("data-days"), 10) || 180; var prefs = banner.querySelector("[data-cpcb-prefs]"); var compact = banner.querySelector('[data-cpcb-actions="compact"]'); var expanded = banner.querySelector('[data-cpcb-actions="expanded"]'); var toggles = banner.querySelectorAll("[data-cpcb-toggle]"); function show() { banner.hidden = false; requestAnimationFrame(function () { banner.classList.add("is-visible"); }); } function hide() { banner.classList.remove("is-visible"); setTimeout(function () { banner.hidden = true; }, 300); } function collapse() { if (!prefs) return; prefs.hidden = true; if (expanded) expanded.hidden = true; if (compact) compact.hidden = false; } function expand() { if (!prefs) return; prefs.hidden = false; if (compact) compact.hidden = true; if (expanded) expanded.hidden = false; } function paint(state) { Array.prototype.forEach.call(toggles, function (t) { var key = t.getAttribute("data-cpcb-toggle"); if (key === "necessary") { t.setAttribute("aria-checked", "true"); return; } t.setAttribute("aria-checked", state[key] ? "true" : "false"); }); } function fromToggles() { var state = { analytics: false, advertisement: false, functionality: false }; Array.prototype.forEach.call(toggles, function (t) { var key = t.getAttribute("data-cpcb-toggle"); if (key in state) state[key] = t.getAttribute("aria-checked") === "true"; }); return state; } function commit(state, revoke) { writeCookie(COOKIE, JSON.stringify({ analytics: !!state.analytics, advertisement: !!state.advertisement, functionality: !!state.functionality, v: 1, ts: Date.now() }), days); apply(state, revoke); hide(); } Array.prototype.forEach.call(toggles, function (t) { if (t.disabled) return; t.addEventListener("click", function () { t.setAttribute("aria-checked", t.getAttribute("aria-checked") === "true" ? "false" : "true"); }); }); banner.addEventListener("click", function (e) { if (!e.target || !e.target.closest) return; var btn = e.target.closest("[data-cpcb-action]"); if (!btn) return; var action = btn.getAttribute("data-cpcb-action"); if (action === "customise") { expand(); return; } if (action === "accept") { commit({ analytics: true, advertisement: true, functionality: true }, false); return; } if (action === "reject") { commit({ analytics: false, advertisement: false, functionality: false }, true); return; } if (action === "save") { commit(fromToggles(), true); return; } }); var existing = stored(); if (existing) { apply(existing, false); paint(existing); collapse(); } else if (!inScope(banner)) { // Out of the consent-required region: implied consent, banner never shown. commitSilently(); } else { collapse(); show(); } function commitSilently() { var all = { analytics: true, advertisement: true, functionality: true }; writeCookie(COOKIE, JSON.stringify({ analytics: true, advertisement: true, functionality: true, v: 1, ts: Date.now(), implied: true }), days); apply(all, false); } // Public re-open hook for a footer "Cookie settings" link. window.cpCookieBanner = window.cpCookieBanner || {}; window.cpCookieBanner.open = function () { var s = stored() || { analytics: false, advertisement: false, functionality: false }; paint(s); expand(); show(); }; } function boot() { var banners = document.querySelectorAll("[data-cpcb]"); if (!banners.length) return; init(banners[0]); document.addEventListener("click", function (e) { if (!e.target || !e.target.closest) return; var trigger = e.target.closest("[data-cp-cookie-settings]"); if (!trigger) return; e.preventDefault(); if (window.cpCookieBanner && window.cpCookieBanner.open) window.cpCookieBanner.open(); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", boot); } else { boot(); } })();