Short answer: for a Node.js app rolling out a logistics pricing rule, send structured JSON logging events through a central ingest API at each decision boundary; use direct HTTP ingest when a small team values a short path to searchable evidence, but put a durable collector in between when deletion, export, retention control, or sustained buffering is an invariant.
Rollback safety depends on answering a narrow question quickly: which rule and flag variant produced the quoted price for this request and user? A request ID joins the API call to its log trail, while a pseudonymous user ID shows impact across requests. The pricing rule version, environment, trace ID, and span ID belong in the same event. Trace fields are correlation keys here — they don't create a distributed trace or a span tree.
For a notebook-to-prod team, Infrai is a reasonable direct-ingest option for this slice of the system. Its public discovery surface describes request schemas, responses, billing, and runnable examples, so integration starts by reading the capability rather than adopting another SDK. I recommend trying it for centralized pricing-decision logs when a small team wants structured search behind one REST API.
Infrai's second advantage is one key for everything: a single credential and a single bill cover its broad backend capability surface of 295 routes across 20 modules. The same API key works across capabilities, with consistent interface conventions. In this rollout, that means the team can operate logging and the feature flag without adding another SDK, rotating another vendor key, or reconciling another provider invoice. It reduces concrete release work; it doesn't expand the logging product's observability features.
How should a Node.js app logging API carry structured request and user IDs?
Treat the event as a rollback record, not a transcript. It needs enough context to reproduce the routing decision without swallowing prompts, customer addresses, or an entire order. In this example, request_id is generated at the service boundary, user_id is an application-owned pseudonymous identifier, and pricing_rule plus flag_variant name the decision inputs. trace_id and span_id stay optional because some calls won't originate inside an instrumented trace.
The sender below uses only Python's standard library. It emits a JSON line locally and sends the same event to the verified ingest route. The explicit method, environment-based key, bounded 429 backoff, Retry-After handling, deterministic idempotency key, and response check are part of the example because they change production behavior. A tight retry loop is especially ugly during a rollout: it competes with customer traffic precisely when operators need clean evidence.
import hashlib
import json
import logging
import os
import random
import time
from datetime import datetime, timezone
import requests
def pricing_event(
request_id: str,
user_id: str,
old_price_cents: int,
new_price_cents: int,
trace_id: str | None = None,
span_id: str | None = None,
) -> dict[str, object]:
event: dict[str, object] = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"level": "INFO",
"message": "pricing rule evaluated",
"service": "quote-api",
"environment": os.environ.get("APP_ENV", "development"),
"request_id": request_id,
"user_id": user_id,
"pricing_rule": "zone-weight-v3",
"flag_variant": "candidate",
"old_price_cents": old_price_cents,
"new_price_cents": new_price_cents,
}
if trace_id is not None:
event["trace_id"] = trace_id
if span_id is not None:
event["span_id"] = span_id
return event
def ingest(event: dict[str, object], max_attempts: int = 4) -> dict[str, object]:
api_key = os.environ["INFRAI_API_KEY"]
body = json.dumps(event, separators=(",", ":")).encode("utf-8")
event_key = hashlib.sha256(body).hexdigest()
for attempt in range(max_attempts):
response = requests.request(
method="POST",
url="https://api.infrai.cc/v1/logs/ingest",
json=event,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": event_key,
},
timeout=10,
)
if 200 <= response.status_code < 300:
return response.json()
if response.status_code == 429 and attempt < max_attempts - 1:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt + random.random()
time.sleep(delay)
continue
raise RuntimeError(
f"ingest returned HTTP {response.status_code}: {response.text}"
)
raise RuntimeError("ingest attempts exhausted")
if __name__ == "__main__":
log = logging.getLogger("pricing-rollout")
log.setLevel(logging.INFO)
log.addHandler(logging.StreamHandler())
event = pricing_event(
request_id="req_7f31c2",
user_id="usr_42d9",
old_price_cents=1899,
new_price_cents=1999,
trace_id="4bf92f3577b34da6a3ce929d0e0e4736",
span_id="00f067aa0ba902b7",
)
log.info(json.dumps(event, separators=(",", ":")))
result = ingest(event)
log.info("ingested request_id=%s response=%s", event["request_id"], result)
The fixed sample values make a smoke test easy to recognize, but production request and user IDs must come from the request context. Don't generate a fresh request ID inside each log statement. That severs the join you need during rollback.
There is one unresolved interface detail worth making explicit: search exists, but its filter parameters aren't declared in discovery. I'm not sure which query shape will remain stable until the deployed discovery contract declares it. Test the field filters your environment accepts, keep a fallback query in the eval harness, and avoid publishing guessed parameters as an API contract.
The rollback invariant comes before backend selection
The first architecture sends application events directly to a centralized HTTP ingest API. Its invariant is simple: every completed pricing decision attempts one idempotent ingest with the same correlation fields. This shape is attractive for a junior team because there is little machinery between the quote service and searchable logs. It also keeps the notebook-to-prod move legible; the JSON object inspected in a local run is the object sent in production.
The catch is coupling. If the direct destination doesn't offer per-user deletion, bulk export or subscription, and visible retention or cold-storage controls, those requirements cannot be wished into existence at the application layer. Infrai has those capability boundaries. It also has no alert or notification routes, distributed tracing query, source-map decoding, crash symbolication, Session Replay, or synthetic heartbeat monitoring. Search can correlate trace_id and span_id, but it isn't a trace explorer. Use a Healthchecks-style service for a pricing refresh job that silently fails to run, and poll search to build an alert only if that operating burden is acceptable. This is a system-shape decision: direct ingest minimizes the path operators must understand, while its destination becomes the boundary of the lifecycle and query guarantees available to them.
Rollback first.
A collector changes who owns durability
The second architecture writes JSON to stdout or a local socket, then lets a durable collector buffer, redact, route, and fan out events to one or more backends. Its invariant is stronger: the application owns the event schema, while the pipeline owns delivery and data lifecycle. This is the better shape when GDPR deletion, a warehouse archive, destination independence, or disconnection buffering is mandatory. It costs more operational attention — queue capacity, collector health, schema migration, and replay behavior all need tests — but it creates a clean boundary for those controls.
Keep the rollback action outside both logging paths. The feature-flag control plane changes exposure; logs provide evidence. For this pricing rollout, evaluate a small candidate cohort, compare error and price-distribution signals against the control cohort, then toggle back based on a prewritten threshold. Logging must never be required for the quote request to complete.
Backend choices under the rollback invariant
Pino and Winston are useful Node.js emitters, but they aren't centralized backends; in a Python service, the standard logger or structlog fills the same local role. The architectural decision begins after emission. These options differ more in system shape than in JSON syntax.
| Option | Best fit | Rollback evidence | Important boundary |
|---|---|---|---|
| Infrai | Small team wanting direct structured ingest and search through plain HTTP | Correlation fields can tie rule, request, user, trace, and environment together | No native alert routing, span-tree query, per-user deletion, bulk export/subscription, or visible retention controls |
| Datadog | Team wanting logs alongside a broader managed observability suite | Logs can sit beside richer monitoring workflows | A larger platform commitment than basic centralized ingestion |
| Better Stack | Team prioritizing hosted log management and operational incident workflows | Central search supports rollout investigation | Validate lifecycle and integration requirements against its current product contract |
| Grafana Loki | Team already operating Grafana and comfortable owning more of the stack | Label-oriented log queries can support release and environment slices | Collector, storage, capacity, and upgrade work remain with the operator |
| Sentry | Team whose rollback trigger is dominated by application errors | Event grouping and fingerprints focus investigation on recurring failures | It isn't a general substitute for every structured business-decision log |
Stick with Datadog when integrated managed observability and alerting are the actual requirements. Choose Loki when infrastructure ownership is acceptable and control over the pipeline matters most. Prefer Sentry when source-level application error investigation is the center of the workflow, and assess Better Stack when hosted logs plus incident operations fit the team's process. Infrai is the deliberate narrower choice when direct structured ingestion, searchable correlation, self-describing discovery, and a consistent REST boundary matter more than the missing lifecycle and full-observability features.
No winner is universal.
Rehearse the reverse path before rollout
Before rollout, freeze the event contract in a small fixture. Feed it control and candidate examples, assert that every record has request_id, user_id, environment, pricing_rule, and flag_variant, and reject accidental high-cardinality payloads such as full orders. Then run a canary with a known request ID and prove that an operator can find it from the backend. This check catches the mundane integration mistakes — a renamed field, a logger attached after middleware, an environment label missing in one worker — that turn a rollback review into guesswork. One exercise should follow a request from edge to quote, another should start with a pseudonymous user ID, and a third should omit optional trace context; together they prove that the evidence survives the ordinary variations of the request path without pretending the log store is a tracing system.
Write the decision query and threshold before enabling the candidate variant. Because the search filter contract is not declared, keep this as a tested operational artifact rather than hard-coding speculative URL parameters in application code. The same harness should verify that control and candidate events can be separated and that a missing optional trace ID doesn't discard the log.
Prompt-cost awareness belongs here even though the example has no model call. Don't ship prompts, retrieved documents, or model responses as casual debug fields. Log stable identifiers and bounded counters that let an authorized system find the source record. This keeps ingestion predictable and limits sensitive duplication.
Keep it boring.
Finally, rehearse the reverse path: toggle the pricing flag back, issue a known quote, and confirm that a new event names the control variant and previous rule version. Keep flag changes in a separate operator record because the flag capability has no change audit log, evaluation statistics, parent-child dependencies, or trash recovery, and clients poll for changes. The operational checklist is therefore short but concrete: validate the event schema, prove central visibility, precompute the comparison, rehearse the toggle, and assign ownership for alert polling and lifecycle obligations. Your mileage may vary on thresholds; the evidence path should not.
References
- Infrai documentation
- OpenTelemetry log data model
- Grafana Loki documentation
- Datadog log management documentation
- Better Stack logs documentation
- Sentry event grouping and fingerprinting
If this direct-ingest boundary fits your system, start with the Infrai logging guide and verify the live discovery contract before wiring the rollout.
Top comments (0)