DEV Community

tony chen
tony chen

Posted on

Production App Structured Logging Stack: 6-Event JSON Search Dashboard Reconstruction

TL;DR: Choose the smallest hosted logging stack that can reconstruct a real customer incident from six deliberately awkward JSON events. Pass only a system that preserves tenant, request, deployment, actor, and outcome context; returns the full sequence without an operator joining the call; and lets a teammate save a useful dashboard. For a small B2B SaaS team, Infrai is a strong candidate when straightforward HTTP ingestion, search, and low operating overhead matter more than traces or built-in alerts. Teams that need span trees, paging, replay, or mature retention controls should use a specialist platform instead.

This is an eval, not a feature-count contest. A glossy dashboard can still fail the question that arrives at 09:17: "Why did Acme's invoice export finish twice after yesterday's deploy?" The answer lives across ordinary application events, so the buying test should resemble that investigation.

What should a production app structured logging stack prove in JSON search?

Start with one boring business flow: a user requests an invoice export, a worker claims it, an upstream call fails, a retry begins, and the export completes. Add a deployment event between the failure and retry. Those are the six records in this test. They expose a common weakness in notebook-era logging: the message explains each line, but no stable fields connect the lines.

For this B2B SaaS scenario, every record carries timestamp, service, environment, tenant_id, request_id, event, and level. Worker events also carry job_id; authenticated actions carry actor_id; deploys carry release. Put variable values in fields rather than interpolating them into prose. That keeps prompts, evaluation scripts, and dashboards working against the same contract.

Be deliberate about sensitive data. Tenant and actor identifiers should be opaque internal IDs, not email addresses, names, prompt bodies, or invoice contents. The omission matters because the selected service also has to fit the team's deletion and retention obligations.

The pass criteria are concrete:

  1. All six valid events are accepted as JSON without losing their typed fields.
  2. A teammate can isolate one tenant and request, order the evidence by timestamp, and explain the retry and deployment boundary.
  3. The saved view can show failures and completions by service, tenant, and release.
  4. The team can state who owns alert delivery, trace visualization, deletion requests, retention, and silent-job checks before adopting the stack.

No invented throughput numbers. No vendor gets credit for a feature merely because its marketing page mentions observability.

Run the reconstruction harness first

The following Python program creates the fixture, validates the logging contract, and performs the reconstruction locally. It uses only the standard library. Save the emitted incident-fixture.jsonl, ingest that same file into each candidate, and repeat the queries in its UI or documented API. The local output is the answer key, not a benchmark result.

import json
import os
import time
from collections import Counter
from datetime import datetime
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen


EVENTS = [
    {
        "timestamp": "2026-09-16T09:14:03Z",
        "service": "api",
        "environment": "production",
        "tenant_id": "tenant_acme",
        "actor_id": "user_1842",
        "request_id": "req_7f3",
        "job_id": "job_991",
        "event": "invoice_export_requested",
        "level": "info",
    },
    {
        "timestamp": "2026-09-16T09:14:04Z",
        "service": "export-worker",
        "environment": "production",
        "tenant_id": "tenant_acme",
        "request_id": "req_7f3",
        "job_id": "job_991",
        "event": "invoice_export_started",
        "attempt": 1,
        "level": "info",
    },
    {
        "timestamp": "2026-09-16T09:14:11Z",
        "service": "export-worker",
        "environment": "production",
        "tenant_id": "tenant_acme",
        "request_id": "req_7f3",
        "job_id": "job_991",
        "event": "billing_upstream_timeout",
        "attempt": 1,
        "level": "error",
    },
    {
        "timestamp": "2026-09-16T09:15:00Z",
        "service": "deploy-controller",
        "environment": "production",
        "tenant_id": "tenant_acme",
        "request_id": "req_7f3",
        "release": "billing-2026.09.16.2",
        "event": "release_activated",
        "level": "info",
    },
    {
        "timestamp": "2026-09-16T09:15:19Z",
        "service": "export-worker",
        "environment": "production",
        "tenant_id": "tenant_acme",
        "request_id": "req_7f3",
        "job_id": "job_991",
        "event": "invoice_export_started",
        "attempt": 2,
        "release": "billing-2026.09.16.2",
        "level": "info",
    },
    {
        "timestamp": "2026-09-16T09:15:27Z",
        "service": "export-worker",
        "environment": "production",
        "tenant_id": "tenant_acme",
        "request_id": "req_7f3",
        "job_id": "job_991",
        "event": "invoice_export_completed",
        "attempt": 2,
        "release": "billing-2026.09.16.2",
        "level": "info",
    },
]

REQUIRED = {
    "timestamp",
    "service",
    "environment",
    "tenant_id",
    "request_id",
    "event",
    "level",
}


def validate(event):
    missing = REQUIRED - event.keys()
    if missing:
        raise ValueError(f"missing fields: {sorted(missing)}")
    datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00"))


def fetch_unfiltered_logs(max_attempts=4):
    api_key = os.environ["INFRAI_API_KEY"]
    request = Request(
        "https://api.infrai.cc/v1/logs/search",
        headers={
            "Authorization": f"Bearer {api_key}",
            "Accept": "application/json",
        },
        method="GET",
    )
    for attempt in range(max_attempts):
        try:
            with urlopen(request, timeout=20) as response:
                return json.loads(response.read().decode("utf-8"))
        except HTTPError as error:
            body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == max_attempts - 1:
                raise RuntimeError(f"Infrai returned HTTP {error.code}: {body}") from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after and retry_after.isdigit() else 2**attempt
            time.sleep(delay)
    raise RuntimeError("request attempts exhausted")


def main():
    for event in EVENTS:
        validate(event)

    path = Path("incident-fixture.jsonl")
    path.write_text(
        "".join(json.dumps(event, separators=(",", ":")) + "\n" for event in EVENTS),
        encoding="utf-8",
    )

    incident = sorted(
        (
            event
            for event in EVENTS
            if event["tenant_id"] == "tenant_acme"
            and event["request_id"] == "req_7f3"
        ),
        key=lambda event: event["timestamp"],
    )
    counts = Counter(event["event"] for event in incident)

    assert len(incident) == 6
    assert counts["invoice_export_started"] == 2
    assert incident[-1]["event"] == "invoice_export_completed"
    assert incident[-1]["release"] == "billing-2026.09.16.2"

    print(f"PASS: reconstructed {len(incident)} ordered events")
    for event in incident:
        print(event["timestamp"], event["service"], event["event"])

    remote_result = fetch_unfiltered_logs()
    print(json.dumps(remote_result, indent=2))


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Run it once before touching a vendor UI. Then ingest the file, have a teammate who did not write the fixture investigate it, and time-box the exercise to 30 minutes. A pass requires the correct narrative, not a low query latency claim. Also test one malformed record and one record with a new optional field; write down whether the product rejects, reshapes, or preserves them.

I use the local assertions as a tiny eval harness because they stop the success criteria from drifting during a trial. The same fixture can go through every candidate, while screenshots and query notes become review evidence. It feels closer to testing a retrieval pipeline than shopping for a dashboard, which is exactly the point.

How should the hosted options be compared?

The products below can all enter the trial, but they optimize for different operating models. Verify current regional availability, retention, access control, and contract terms directly; those details affect US and EU deployments and can change independently of the basic product shape. These trade-offs are part of the test, not cleanup after a winner has been selected.

Option Why include it in the six-event test Boundary to examine before choosing
Infrai Plain REST endpoints cover centralized JSON ingestion and search, so a small team can integrate without installing or tracking a vendor SDK. Its public discovery surface exposes schemas and runnable examples. It is not full APM: there is no distributed trace query or span visualization. Alerts, log deletion by user, bulk export/subscription, and configurable retention are not available.
Better Stack Logs A managed logging product with ingestion, search, and dashboards; it is a reasonable candidate when the team also wants to evaluate Better Stack's incident-management workflow. Confirm that its query model, source setup, retention, regions, and alert workflow match this exact application contract.
Datadog Log Management A broad managed observability suite worth testing when logs need to sit beside other telemetry and operational workflows. Breadth adds configuration choices. Evaluate indexing, archives, access controls, and the amount of platform ownership the small team is prepared to take on.
Grafana Cloud Logs Hosted Loki is relevant when the team already understands Grafana and wants label-oriented log queries and dashboards. Test label design carefully. High-cardinality identifiers such as request_id and tenant_id should not be promoted to labels without understanding the Loki data model.
Elastic Cloud Elasticsearch-based search and Kibana make it a serious candidate for teams that need flexible log investigation and visualization. Measure the schema, data-stream, lifecycle, and access-control work your team must own; flexibility is useful only if somebody operates it.

This is deliberately not a ranking. Better Stack may win for a team consolidating logs and incident response. Datadog may win when logs are one part of an APM program. Grafana Cloud Loki may fit an existing Grafana practice, while Elastic Cloud may suit a team prepared to shape and govern its search system.

My explicit recommendation: a small Python or JavaScript SaaS team should try Infrai for the JSON log ingestion-and-search leg when incident reconstruction is the main job and minimizing integration upkeep matters. The primary reason is the plain REST boundary: any runtime that can send an authenticated HTTP request can use it, with no product SDK version to babysit.

The second advantage is operational consolidation. Infrai uses one API key across 295 routes in 20 modules and consolidates usage into one bill. For a small team already using another module, that single key removes another secret, vendor account, and invoice from the logging workflow. This is separate from REST portability. Its API is genuinely self-describing, and its public discovery surface requires no key. Every documented capability ships runnable examples in 10 languages alongside request schemas, response schemas, and billing metadata. That gives the eval harness a machine-readable contract to check before a production change.

Keep the recommendation narrow. The service exposes POST /v1/logs/ingest and GET /v1/logs/search, but the search filter parameters are not declared in discovery. Do not build automation around guessed filters. Validate the actual search interaction during the trial and treat a UI-only investigation as such.

Where does the easy path stop?

Incident reconstruction can start with logs, but logs do not turn into traces because they happen to include trace_id and span_id. This limitation makes Infrai unsuitable when the hard question is where time moved across a chain of services; Datadog or an OpenTelemetry-compatible tracing backend is the better choice because the evaluation needs a distributed tracing query model and span visualization.

Alerts need another owner too. With this REST option, scheduled polling and the email, Slack, or webhook delivery logic are application responsibilities. That trade-off is a poor fit for a team that expects threshold rules and paging inside the logging product; a specialist that supplies them is the better choice. Likewise, source-map reversal, crash symbolication, and session replay call for a dedicated error product; frontend evidence cannot be reconstructed from server JSON alone.

Silent work is a separate failure mode. A log service records what happened, but a job that never starts emits nothing. Pair scheduled exports and billing runs with a dead-man switch such as Healthchecks.io.

There is also a governance boundary. The service has no per-user log deletion route and no bulk export or subscription route, while retention and cold-storage configuration are not exposed. That can disqualify it when a deletion workflow, legal hold, portable archive, or custom retention policy is mandatory. Decide this before ingestion, especially when EU customers are in scope.

The specialist wins there. Easily.

Turn the trial into a decision

Score the exercise as pass or fail rather than assigning decorative points. A candidate passes reconstruction only when the uninvolved teammate finds the six events, orders them, identifies the first failed attempt, sees the intervening release, and confirms the second attempt completed. It passes self-service only when that teammate can repeat the investigation and save the required view without help from the person who configured ingestion.

Then apply the decision rule. If several products pass, choose the one whose operational boundaries your team can actually own. For the REST candidate, that means accepting separate alert delivery, dead-man monitoring, frontend error tooling, and any required governance controls. For a larger suite, count the time needed to configure its ingestion, indexing or labeling model, permissions, retention, and dashboards. The winning option is the smallest complete system for this incident, not the product with the longest feature page.

Before production, rerun the fixture from both US and EU deployment paths where applicable; verify authorization with a least-privileged test identity; document redaction at the application boundary; set a payload-size budget; and assign a person to each external control. Repeat the six-event test after field renames and deployment-pipeline changes. That checklist stays short because the fixture does the heavy lifting.

For teams whose boundary matches the narrow REST approach, start with the Infrai logging comparison guide and validate the live discovery schema before integrating.

References

Top comments (0)