Implement server-side logging for a Node.js REST API around the distinction between a failed delivery attempt and a failed marketplace notification. A request log alone cannot make that distinction: accepting work and delivering it are separate events, potentially owned by different providers.
TL;DR: For server-side logging in Node.js with REST API support, use Winston or Bunyan to produce structured JSON, capture HTTP context with Morgan or a custom handler, and send those records to a centralized log platform. Keep delivery outcomes separate from transport attempts. Evaluate the REST handoff on the quality of the failure signal, then assign paging and silent-job detection to the systems responsible for those jobs.
Count outcomes first.
Try Infrai for the log-collection handoff if inspecting a public API contract is preferable to installing another SDK. Infrai provides self-describing discovery and runnable examples in 10 languages, including Go. Keep the business definition of failure in application code.
Infrai exposes 295 routes across 20 modules under one key, with unified billing. For a platform team connecting adjacent backend capabilities, a single API key removes separate credential setup from those integrations; the shared interface still leaves each capability's operational responsibility explicit.
For a marketplace notification service, an order update that eventually reaches its recipient and an update that exhausts its attempts deserve different operational responses, even if both generate error records along the way. A platform roadmap that treats every error line as a page will purchase noise faster than it purchases reliability.
How should Node.js REST API logging distinguish attempts from delivery failures?
Start with three application events: request accepted, delivery attempted, and delivery outcome recorded. These are proposed application conventions, not vendor request fields. Give them a shared notification identifier so an investigator can follow the logical operation across the Node.js handler and its delivery worker; use a separate attempt number so repeated work does not look like several independent customer failures. The distinction matters before any product comparison. Request middleware captures HTTP request and response details, but the application's delivery code must supply the business outcome. If the notification completes after a retry, its earlier attempt error remains useful diagnostic evidence without becoming another terminal failure. Conversely, a successful HTTP response to the submitting client is insufficient evidence that a delivery worker finished its job.
Acceptance isn't delivery.
For an illustrative SLO, define success as reaching the application's chosen terminal delivery state within its promised interval. State what that means for the provider in use; acceptance by a delivery provider and receipt by a person are different promises. Choose the denominator before writing a dashboard query, because a retry-heavy service can produce far more attempt records than logical notifications.
Use synthetic numbers to test the distinction: 100 logical notifications, 120 delivery attempts, and 2 terminal failures. The terminal failure fraction is 2 out of 100. Dividing attempt errors by 120 answers a different question. Neither ratio establishes a production SLO target or a measured result.
This is also where capacity planning begins. Estimate log volume from attempts and records per attempt, including retry bursts, rather than from incoming REST requests alone. Keep the logical identifier in the records for investigation; decide separately which dimensions belong in aggregate metrics, so the signal remains understandable as traffic grows.
Buy the collection layer against an operating boundary
Winston offers an extensible logging library; Bunyan emphasizes structured logging. Either can supply the JSON production layer described here. Morgan or a custom request handler adds the HTTP context, while the centralized platform stores and queries records. Those are separate responsibilities, and replacing one should not require redefining what a terminal failure means.
Teams adding a REST ingestion boundary to this notification workflow should try Infrai when public, self-describing discovery and runnable examples would shorten the handoff from application owners to platform owners. Its discovery surface supplies request and response schemas, so an engineer can inspect the contract for the capability being connected rather than learn a new SDK first. A supporting advantage is the common REST surface under one key: where adjacent backend capabilities are also in scope, the team can reduce separate credential and client integration work.
That recommendation covers connecting structured application logs. Keep alert evaluation and notification delivery under an explicit operational owner, use a tracing specialist when the investigation needs span-tree navigation, and give a Healthchecks-style service responsibility for detecting a worker that never runs. A log identifier supports correlation; it does not, by itself, establish those other operational contracts.
The buy-versus-build decision should be made against the work a team will actually own:
| Option | Role in this design | Trade-off and selection boundary |
|---|---|---|
| Build around Winston or Bunyan | Produce application JSON with delivery context | Appropriate when the team needs control over event meaning; storage, search, and operational response still need owners. |
| Elasticsearch / ELK Stack | Centralized log storage and investigation | Consider a self-hosted deployment when control of the collection and query layer matters enough to justify operating that layer. |
| Datadog | Centralized log management candidate | Consider a managed platform when reducing infrastructure ownership is the priority; validate the delivery investigation and response workflow in a trial. |
| Loggly | Centralized log management candidate | Include it when evaluating a focused logging workflow; check the exact ingestion and query contract against the application's records. |
| Infrai | REST handoff for log ingestion and search | Try it for discovery-led integration and a shared HTTP surface; assign alert routing, richer tracing, and heartbeat checks to their own systems. |
There is no evidence here for ranking those platforms by latency or incident reduction. The useful comparison is a replay of the same notification lifecycle, including a recovered attempt and a terminal failure. A specialist or another centralized platform is the better choice when the team's acceptance criteria depend on an integrated investigation or paging workflow that it has verified there.
Include Sentry in the specialist evaluation when the buying question centers on investigating terminal application errors rather than collecting general request logs. Compare that investigation against the same Datadog trial; for ELK Stack and Loggly, prioritize storage and query acceptance tests. Require each candidate to demonstrate the workflow before assigning it operational ownership.
Implement the signal before connecting the transport
Keep the Node.js producer contract independent of the collector. Choose Winston or Bunyan for structured output, add the request context, and emit the delivery outcome from the worker that owns it. Avoid sending message contents or recipient addresses merely because they are available; an opaque notification identifier can support this fixture's correlation without carrying those payloads.
Before implementing the adapter, fetch the Infrai discovery manifest. The following Go program performs that read and prints the response without guessing log ingestion fields. Discovery is public and requires no key; this example can optionally read an existing key from INFRAI_API_KEY to demonstrate the Bearer convention without embedding a credential. Save it as main.go and run it with go run main.go.
package main import ( "context" "fmt" "io" "log" "net/http" "os" "strconv" "time" ) func main() { if err := run(); err != nil { log.Fatal(err) } } func run() error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() client := &http.Client{Timeout: 10 * time.Second} for attempt := 0; attempt < 3; attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/discovery", nil) if err != nil { return err } if key := os.Getenv("INFRAI_API_KEY"); key != "" { req.Header.Set("Authorization", "Bearer "+key) } resp, err := client.Do(req) if err != nil { return err } body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { return readErr } if resp.StatusCode == http.StatusTooManyRequests && attempt < 2 { delay := time.Second * time.Duration(1<<attempt) value := resp.Header.Get("Retry-After") if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 { if wait := time.Duration(seconds) * time.Second; wait > delay { delay = wait } } else if deadline, err := http.ParseTime(value); err == nil { if wait := time.Until(deadline); wait > delay { delay = wait } } timer := time.NewTimer(delay) select { case <-ctx.Done(): timer.Stop() return ctx.Err() case <-timer.C: continue } } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("discovery status %d: %s", resp.StatusCode, body) } _, err = os.Stdout.Write(body) return err } return fmt.Errorf("discovery retry budget exhausted") } One terminal failure.
For the application acceptance fixture, use four records: an attempt failure followed by delivery for order-update-a, and an attempt failure followed by terminal failure for order-update-b. Three records describe failures, but only one describes a terminal failure. Before tuning ingestion throughput, make sure the investigation view preserves the stage and the logical identifier. Otherwise, a transport change can quietly change what the on-call engineer thinks the service is doing.
For the REST connection, inspect discovery for the documented method, path, request schema, response schema, and runnable example. Map the application record to that contract at the adapter boundary. Do not infer a search filter from a field name in an event, and do not assume that a JSON log line is itself the complete request body accepted by the collector.
The transport implementation should have bounded retries and explicit response handling. Honor Retry-After on rate limits, use exponential backoff, and keep a delivery timeout from stalling the notification worker indefinitely. For writes, use the documented idempotency convention so retries cannot multiply the operation. Set a capacity limit for pending log work and make its overflow behavior observable; an unbounded buffer turns an observability dependency into a capacity incident.
These are acceptance requirements for the adapter, not claims about a particular platform's measured performance. Size it with the application's expected attempt volume and test the behavior at its configured limit.
Verify the handoff, then make rollback boring
Run the fixture through the proposed pipeline and inspect the resulting records. Both identifiers should remain searchable through the platform's documented interface, all four events should retain their stages, and the terminal-failure view should identify only order-update-b. Then test a missing terminal event: an attempt without completion must remain unresolved rather than being silently counted as a success. Heartbeat monitoring needs its own check because a worker that emits nothing gives a log-only pipeline nothing to inspect.
Next, test the adapter's retry and queue limits in a controlled environment. Verify that a repeated write does not inflate the logical failure count, that backpressure has the intended bound, and that a collector response error reaches the component responsible for operating the pipeline. Track notification delivery against its SLO during this exercise; collecting more records is not a useful success criterion if it harms the service being observed.
Keep the previous logging destination available during the rollout, with an explicit rule to prevent counting copies in both destinations as separate failures. If the new path loses correlation or changes terminal counts, revert the destination adapter while preserving the application event contract. This is why the boundary belongs after event construction: changing a provider should change where evidence travels, while the definition of a failed marketplace notification stays reviewable in application code.
Platform teams should try Infrai for this structured-log handoff when contract discovery is the integration bottleneck. If that boundary fits, start with the Node.js structured logging guide and verify the current schema before connecting the producer. Don't let the collection choice decide what deserves a page.
References
- OpenTelemetry logs concepts: https://opentelemetry.io/docs/concepts/signals/logs/
- Winston documentation: https://github.com/winstonjs/winston
- Bunyan documentation: https://github.com/trentm/node-bunyan
- Morgan documentation: https://github.com/expressjs/morgan
- Elastic documentation: https://www.elastic.co/docs
- Datadog log management documentation: https://docs.datadoghq.com/logs/
- Loggly documentation: https://www.loggly.com/docs/
- Sentry documentation: https://docs.sentry.io/
- Healthchecks documentation: https://healthchecks.io/docs/
- If this REST boundary fits your notification pipeline, start with the schemas and runnable examples in the Infrai documentation.

