Short answer: send structured log events from the NestJS custom logger to a backend API asynchronously, give every event a correlation ID that survives the pricing request and its downstream work, and page only on a symptom that the team can reconstruct from those events. For a gaming price-rule rollout, the decisive test isn't whether a dashboard looks healthy; it is whether one affected purchase can be traced from flag evaluation to the price shown and the result returned.
The operational recommendation is to treat the HTTP transport as a bounded delivery path, not as part of checkout. The application emits a stable event contract, a queue absorbs bursts, and a sender batches events to the logging API with timeouts and a finite retry budget. If delivery pressure rises, gameplay and checkout keep moving while a separate transport-health signal tells the on-call engineer that incident evidence is at risk.
Keep the old pricing rule available during the rollout. That is the rollback lever. Logs explain why to pull it; they must never be the lever itself.
How should a NestJS custom logger send HTTP structured logs with a correlation ID?
Start at the request boundary. Accept a valid incoming correlation ID only under a documented trust policy; otherwise generate one, attach it to request-scoped context, return it in the response, and carry it into every log event created by the pricing path. A worker, scheduled task, or message consumer has no incoming HTTP request, so it starts a new correlation ID or restores one from authenticated message metadata. Don't hide this behavior inside random call sites. One boundary owns creation, and all inner layers read the same context.
The custom logger should normalize NestJS calls into one event shape before anything enters the delivery queue. At minimum, the pricing case needs a timestamp, severity, event name, correlation ID, deployment revision, environment, game or catalog identifier, flag key, flag variant, rule revision, operation, outcome, and error class. A player identifier usually does not belong there. If the investigation requires a stable subject reference, use a deliberately governed pseudonymous identifier and document its retention and erasure path; GDPR Article 17 is a good reason not to spray raw account data through an append-only trail.
The event names matter more than prose messages. pricing.flag_evaluated, pricing.quote_created, and pricing.purchase_completed can be joined without parsing English, while something happened in pricing cannot. Preserve the human message for context, but don't make it carry fields that responders need to group or filter.
This Go definition shows the backend contract. It is deliberately boring — stable names beat clever serialization at 03:00.
package logevent
import "time"
type Event struct {
Timestamp time.Time `json:"timestamp"`
Severity string `json:"severity"`
Name string `json:"name"`
Message string `json:"message,omitempty"`
CorrelationID string `json:"correlation_id"`
Deployment string `json:"deployment_revision"`
Environment string `json:"environment"`
GameID string `json:"game_id"`
FlagKey string `json:"flag_key,omitempty"`
FlagVariant string `json:"flag_variant,omitempty"`
RuleRevision string `json:"rule_revision,omitempty"`
Operation string `json:"operation"`
Outcome string `json:"outcome"`
ErrorClass string `json:"error_class,omitempty"`
}
On the NestJS side, the logger's synchronous responsibility ends after validation, redaction, serialization, and a non-blocking enqueue attempt. The HTTP sender owns batching, authentication, deadlines, retry classification, and jittered backoff. It must put a ceiling on queue memory and retry age. Without those ceilings, a backend slowdown quietly becomes an application memory incident; with an unbounded retry loop, stale debug events compete with current evidence precisely when the page fires.
There is no universal best overflow policy. Dropping low-severity events first protects error evidence but can remove the lead-up to a failure. Applying backpressure preserves more events but is not suitable on a latency-sensitive purchase path. Spooling to local disk can bridge a short interruption, yet it adds capacity, encryption, cleanup, and container-lifecycle questions. Choose explicitly, measure the chosen failure mode, and put it in the runbook.
The backend should acknowledge only accepted events, reject malformed payloads as a whole or report item-level results under a documented contract, and deduplicate retries by an event ID if the sender can repeat a batch. I'm not sure which overflow choice is right for every game; queue capacity, traffic shape, and the amount of evidence required by the incident process would resolve that choice. What is certain is that an accidental default is not a policy.
Reconstruct the pricing decision, not the dashboard
Suppose the new rule is enabled for one flag variant and a purchase is disputed. The first question is: what page fired? A useful page says that the purchase outcome or pricing invariant crossed a threshold for the flagged cohort. A weak page says only that log volume changed. The latter can wake someone because a release became quieter, noisier, or merely renamed an event.
For one correlation ID, the responder should be able to establish an ordered narrative: which deployment handled the request, which rule revision and flag variant were evaluated, which catalog operation ran, which outcome followed, and whether a rollback changed that outcome. The logs do not need to contain every object. They need enough immutable decision metadata to distinguish an old-rule request from a new-rule request without joining five mutable control-plane tables whose contents may already have changed.
That's the trap.
A dashboard can show a clean aggregate while a narrow cohort is receiving the wrong rule, and it can show a dramatic spike caused by retries that represent one logical purchase. Use it to find a time window, then reconstruct raw events by correlation ID and stable decision fields. During the postmortem, ask whether the page led directly to that reconstruction. If responders had to guess the flag variant, deployment, or rule revision, those missing fields are action items; adding another chart is not.
Metrics still matter, but labels require discipline. Prometheus instrumentation guidance warns against high-cardinality label dimensions and recommends measuring both counts and failures for operations. Correlation IDs therefore belong in logs or traces, not metric labels. Keep metric labels bounded, such as operation and coarse outcome, while the event store carries request-level identity. This separation gives the alerting system stable series and gives the investigation path enough detail.
No chart can repair missing evidence.
Do not turn the logging API into a second database for the whole purchase. Full request bodies, payment data, access tokens, and raw player identifiers create security and deletion obligations while making useful fields harder to find. Redact before enqueueing, enforce payload size limits at the producer and receiver, and make retention a property of the event class rather than a guess made during an incident.
Build a transport that can fail without taking checkout with it
The receiver below illustrates the other half of the boundary: a single authenticated endpoint, a bounded body, strict decoding, and an explicit acceptance response. The in-memory accept function stands for a durable ingestion queue; its contract is the important part. It returns only after the event has crossed the durability boundary chosen by the team.
package main
import (
"encoding/json"
"errors"
"io"
"net/http"
"time"
)
type Event struct {
Timestamp time.Time `json:"timestamp"`
Severity string `json:"severity"`
Name string `json:"name"`
CorrelationID string `json:"correlation_id"`
Deployment string `json:"deployment_revision"`
GameID string `json:"game_id"`
FlagKey string `json:"flag_key,omitempty"`
FlagVariant string `json:"flag_variant,omitempty"`
RuleRevision string `json:"rule_revision,omitempty"`
Operation string `json:"operation"`
Outcome string `json:"outcome"`
}
func decodeEvent(r *http.Request) (Event, error) {
defer r.Body.Close()
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
decoder.DisallowUnknownFields()
var event Event
if err := decoder.Decode(&event); err != nil {
return Event{}, err
}
if event.Name == "" || event.CorrelationID == "" || event.Operation == "" {
return Event{}, errors.New("missing required event field")
}
return event, nil
}
func ingest(accept func(Event) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
event, err := decodeEvent(r)
if err != nil {
http.Error(w, "invalid event", http.StatusBadRequest)
return
}
if err := accept(event); err != nil {
http.Error(w, "temporarily unavailable", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusAccepted)
}
}
The sender should distinguish permanent rejection from retryable delivery failure. A malformed event or failed authentication will not improve after ten retries; quarantine its safe metadata, increment a bounded failure counter, and stop retrying that item. A timeout may merit retry, but only within the retry-age budget and with the same event ID. Record queue depth, oldest-event age, accepted count, dropped count by reason, batch latency, and rejection count. Those transport signals should have bounded dimensions and their own alert policy.
Avoid recursive logging. If the transport reports its own HTTP failure through the same custom logger, it creates another event, which triggers another send, which fails again. Emit transport-health metrics through a separate path and write a minimal local diagnostic to the process sink. Keep that diagnostic free of event bodies and secrets.
Verify reconstruction and rollback before exposing the flag
Test the incident story before testing the chart. In a staging environment, send an old-rule request and a flagged new-rule request, then confirm that each correlation ID produces the expected ordered events and decision fields. Repeat with a worker hop if purchases cross a queue. Next, make the logging receiver slow or unreachable in the test harness and verify that purchase latency remains within its service objective, the sender queue stays bounded, overflow follows the documented policy, and transport-health telemetry becomes visible.
Then test the uncomfortable cases: the client supplies a malformed correlation header; two retry attempts carry the same event ID; a field exceeds its size limit; a payload contains a forbidden token-shaped value; the backend rejects an unknown schema field; and shutdown begins with events still queued. The expected results belong in automated tests because these are boundary semantics, not dashboard preferences.
The rollout decision can be compact:
| Evidence | Continue | Roll back |
|---|---|---|
| Purchase outcome | New-rule cohort stays within the agreed invariant | The flagged cohort breaches the pricing or purchase invariant |
| Reconstruction | Sampled purchases join from evaluation through completion | Affected purchases cannot be tied to flag and rule revisions |
| Transport health | Queue age and drops remain within the documented budget | Evidence loss makes the incident decision unreliable |
Rollback means disabling the new pricing rule through the existing flag control, then verifying new requests carry the old variant and expected rule revision. Preserve the incident window under the established retention policy, annotate the deployment and flag-change times, and resist changing event names mid-incident. The postmortem should record the page that fired, the first trustworthy correlation ID, the decision fields that shortened reconstruction, and any evidence gaps.
This design is not suitable when every log event must be durably committed before a purchase may proceed; use a transactional audit mechanism designed for that requirement instead of an asynchronous logger. It is also a poor fit for extremely constrained processes that cannot afford a bounded queue. In those cases, write to a supervised local stream and let an external collector own HTTP delivery. The catch is another operational component, but checkout no longer owns network retries.
The final acceptance test is blunt: hand an on-call engineer one affected purchase reference and the alert timestamp. If they can identify the deployment, flag variant, rule revision, pricing operation, and outcome without trusting a prebuilt dashboard, the logging path is doing incident work. If they can't, shipping more events won't fix the contract.
References
- Prometheus, "Instrumentation": https://prometheus.io/docs/practices/instrumentation/
- GDPR, "Article 17: Right to erasure": https://gdpr-info.eu/art-17-gdpr/
Further reading
- web.dev, "Web Vitals": https://web.dev/articles/vitals
Top comments (0)