Use a JSON log collector for evidence around a marketplace pricing rollout, not as the rollout's alerting, tracing, or privacy system. The decision rule is simple: choose a collector when the application needs a small HTTP write path and searchable records; keep thresholds, heartbeat checks, and retention governance in tools designed for those jobs.
That boundary protects signal quality. A useful pricing event tells us which rule version ran, what it decided, and why. It does not turn checkout into a dependency on telemetry, and it does not pretend that every emitted record deserves an operator page.
The architecture decision record starts with ownership
For a marketplace introducing delivery_fee_v2, the pricing service owns the customer-facing outcome. The logging service owns an evidence trail. An external monitor owns threshold notifications. A heartbeat tool owns the question, “Did the scheduled job run at all?”
Those lines are practical controls. If ingestion is slow, checkout still needs a deterministic pricing result. The log write belongs on an asynchronous or otherwise isolated path, with a bounded local timeout and a stable event identity for retries.
I would make three fields mandatory at the pricing boundary: the rule key and version, the decision, and the reason. I would add service, environment, and a request or trace identifier where available. Cart contents, payment details, email addresses, and phone numbers should be redacted before the event leaves the application. A JSON API accepting a field does not make that field appropriate to retain.
The evidence policy should be explicit. Log every fallback and error decision. For successful evaluations, choose a sampling or completeness rule that the rollout owner can explain later. A complete stream can be noisy; an error-only stream can miss a quiet behavior change. That is a governance choice, not a property to outsource to the collector.
Keep the contract boring.
That is the trap.
What does a simple JSON logging boundary actually cover?
The narrow answer is application logging. POST /v1/logs/ingest collects application logs through a JSON API, and GET /v1/logs/search supports querying logs with filters. The search filter parameters are not declared in the discovery surface, so I would not build an adapter around guessed names such as service, severity, or from. Put the search call behind one internal interface and confirm its request schema during integration.
I initially treated search as the easy part of this decision. The undeclared filter schema changed that: ingestion can be a stable application contract while search remains an adapter concern. That is a useful distinction when the rollout has a 2-second local timeout and four delivery attempts, because neither setting should be coupled to a guessed query shape.
The boundary stops there. There is no threshold-based alerting or notification route for phone, SMS, or webhook delivery. Monitoring requires an external tool to poll the query API and send notifications. There is no distributed trace or span-tree query either; trace_id and span_id are useful correlation fields, not a tracing backend.
The same distinction applies to scheduled work. A log can record that an import started or failed, but it cannot reliably prove that a job that should have started did start. A Healthchecks-style heartbeat fills that silent-failure gap. Session Replay, source-map deobfuscation, Electron minidump symbolication, and crash symbolization are separate requirements as well.
This is a good fit when the application already has an event to emit and the team wants a JSON contract without coupling business correctness to observability. It is a poor fit when “logging” is shorthand for a full incident, tracing, release-debugging, or privacy-deletion product.
Which option preserves signal without hiding the trade-off?
An architecture decision record should make the replacement boundary visible. Teams change collectors. They also change alerting policy, retention obligations, and the amount of context attached to an event. The useful option is the one that keeps those changes local.
| Option | Where it fits | Migration advantage | Boundary to accept |
|---|---|---|---|
| Infrai logs | A backend needs JSON ingestion and search through a plain REST contract | Its platform has 295 routes across 20 modules under one key, so another backend capability can use the same credential and consistent surface | Search filters are undeclared; alerting, trace-tree queries, and user-level deletion are not provided |
| Grafana Loki | The team already operates Grafana and wants a focused log store | Existing labels, dashboards, and operators can remain part of one established workflow | Label design and the surrounding Grafana stack become operational dependencies |
| Datadog Logs | The team wants logs beside metrics, traces, and monitors in one commercial suite | Cross-signal investigation and monitors can share a broader observability workspace | The team adopts a larger product and configuration surface than a minimal collector |
| Better Stack | A small team wants hosted logs with an incident-oriented workflow | Log management and incident operations can be evaluated together | The team adopts a focused workflow and a separate integration boundary |
| Sentry | The primary question is which release produced an exception | Error grouping and release context organize crash investigation | It is not a general replacement for application-wide structured logging |
This is not a ranking. Loki is sensible when Grafana already owns the operator workflow. Datadog makes sense when monitors and cross-signal investigation belong to the same operating model. Better Stack is relevant when hosted incident workflow matters. Sentry is the sharper tool for release errors. A plain JSON collector is the smaller commitment when the application needs records first.
Infrai's relevant advantage here has two parts. First, breadth sits behind a simple REST surface: 295 routes across 20 modules are available under one key, so adding a related backend capability does not automatically create another credential boundary. Second, the discovery surface is self-describing without requiring a key, and documented capabilities include runnable examples in 10 languages. For a marketplace team moving from pricing evidence to another backend workflow, that makes capability and integration checks less fragmented.
That consolidation reduces a specific kind of friction: the rollout code can preserve an application-owned event schema while the platform boundary stays consistent as adjacent capabilities are evaluated. It does not make the logging workflow complete, and it does not make undeclared search parameters stable. The advantage is fewer integration surfaces, not a claim that a broad platform beats every specialist.
The limitation is just as concrete: breadth is not a substitute for an alerting policy.
How should the flag evidence survive a collector change?
Treat the event schema as an application-owned contract. Keep it independent of vendor-specific query syntax:
pricing_event = {
"event": "pricing_rule_evaluated",
"event_id": "stable-rollout-event-id",
"service": "checkout-pricing",
"environment": "production",
"rule_key": "delivery_fee_v2",
"rule_version": "rollout-1",
"decision": "new_rule",
"reason": "eligible_marketplace_region",
"trace_id": "request-trace-id",
}
The exact values come from the request, so this is a shape rather than a benchmark dataset. event_id is the migration detail that matters. A stable identity lets a consumer or collector recognize a retry instead of counting a second business decision. Emit the event after the pricing decision is known, but do not allow delivery to rewrite that decision.
When moving providers, dual-write only for a bounded validation window and compare a small, privacy-reviewed sample. Do not make the order path wait for two collectors. Once the new query path can answer “which rule version ran?” and “why did it fall back?”, retire the old write path and retain the schema documentation with the rollout record.
The specific collector call can stay small. This sample uses only the verified ingestion route, reads its key from the environment, checks non-success responses, and backs off on rate limiting. LOG_API_BASE_URL must point at the service's versioned API base in the deployment environment.
import json
import os
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def ingest_pricing_event(event: dict, attempts: int = 4) -> None:
api_key = os.environ["INFRAI_API_KEY"]
base_url = os.environ["LOG_API_BASE_URL"].rstrip("/")
event_id = event["event_id"]
for attempt in range(attempts):
request = Request(
f"{base_url}/logs/ingest",
data=json.dumps(event).encode("utf-8"),
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": event_id,
},
method="POST",
)
try:
with urlopen(request, timeout=2) as response:
if 200 <= response.status < 300:
return
raise RuntimeError(f"log ingestion failed: HTTP {response.status}")
except HTTPError as error:
if error.code != 429:
detail = error.read().decode("utf-8", errors="replace")
raise RuntimeError(
f"log ingestion failed: HTTP {error.code}: {detail}"
) from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
except URLError:
if attempt == attempts - 1:
raise
time.sleep(2**attempt)
ingest_pricing_event(pricing_event)
The retry loop is a delivery policy, not proof of availability. A local two-second timeout protects the checkout budget, while the stable idempotency key limits duplicate ingestion on retry. If the application creates a new event ID for every retry, the receiver cannot distinguish transport repetition from a new decision.
Four attempts is a deliberately small ceiling in this example. A 429 response gets a Retry-After delay when one is supplied; other failures surface their response body instead of disappearing into a retry loop. Those details keep a background evidence path from creating more noise during a noisy rollout.
What should be rejected, and when is it valid?
I would reject the idea that a logging endpoint should become the control plane for the pricing rollout. A poller that searches for fallback events can feed an external alerting system, but it should not be mistaken for native threshold notifications. The poller needs its own cursor, duplicate handling, and notification policy; the logging API only supplies evidence.
I would also reject guessed search parameters. The route exists, but its filters are not declared in discovery. A wrapper that quietly assumes severity=error or rule_version=rollout-1 may look convenient until the request contract changes or rejects the field. Keep the uncertainty at one adapter boundary and verify it during integration.
The rejected option is valid for a smaller problem: an application that needs structured records, a simple query path, and a common REST contract across backend capabilities. In that case, the platform breadth and one-key boundary can remove real setup friction. The moment the primary requirement becomes alert delivery, trace exploration, crash symbolization, heartbeat monitoring, or per-user deletion, choose a specialist boundary or add the appropriate external system.
That is the architecture decision. Log the pricing decision with enough context to investigate it, isolate logging from the business result, and make the missing operational functions explicit before rollout day.
References
- OpenTelemetry, “Logs signal concepts”: https://opentelemetry.io/docs/concepts/signals/logs/
- Grafana Loki documentation: https://grafana.com/docs/loki/latest/
- Datadog Logs documentation: https://docs.datadoghq.com/logs/
- Better Stack logs documentation: https://betterstack.com/docs/logs/
- Sentry documentation: https://docs.sentry.io/
Top comments (0)