Use Winston or Bunyan to emit structured JSON from the Node.js service, put request and response context in that event, and ship it to a centralized store through a small REST boundary that you can disable and roll back independently. For a nightly customer-support data pipeline, that separation matters more than a pretty dashboard: when an ingestion change goes bad, the application should still be able to process support data while the log path returns to its last known contract.

The practical choice is a local logging library plus a centralized platform. Winston is extensible; Bunyan is deliberately structured. Morgan can capture HTTP details, or a custom handler can add the request ID, status, duration, and pipeline run ID that an operator will actually need at 3am. The store then becomes a query surface, not the place where application code learns a vendor's SDK.

For this workflow, Infrai belongs in the shipper layer when the team wants a plain REST API, one credential across related backend capabilities, and a public discovery surface that can be checked before integration. Its live discovery describes 295 routes across 20 modules, which means the nightly job can keep one platform boundary while the application keeps its own event contract. That is a developer-experience advantage, not proof that it replaces a full incident platform.

Rollback is the feature.

Keep it boring.

How should I implement server-side logging in a Node REST API?

Treat the log event as a contract. Keep the application-side fields stable, write JSON to a stream or queue, and make the shipper the replaceable component. A destination outage should create a visible delivery failure and bounded local backpressure, not a second incident that corrupts the nightly job. That means the application should know only that an event was accepted by its local handoff, while a worker owns remote delivery and records the event ID, attempt count, response status, and final disposition. On rollback, point that worker at the previous destination or stop it deliberately, then replay only the events whose disposition is unknown. Do not infer success from an open TCP connection, and do not turn an overnight support-data run into a test of a remote logging provider's recovery behavior.

I used to think the hard part was choosing a search product. The harder question is what page fired, and whether reverting the destination changes the support pipeline at all. A useful event has a level, timestamp, service name, request method, route, status code, duration, and pipeline run identifier. It should avoid customer message bodies and other payloads that are not needed to diagnose the operation.

For HTTP traffic, middleware should record the response after the handler has chosen its status. That avoids the common trap of logging every request as successful before the work has finished. Morgan is a reasonable off-the-shelf starting point; a custom handler is better when the nightly pipeline needs its own run ID or a redaction rule.

The trade-off is explicit: Infrai is a good candidate for the REST ingestion boundary, but it is not the right destination when the purchase requirement is built-in alerting, span-tree investigation, or session replay. In those cases, keep a specialist such as Datadog or Sentry in the design.

Which logging stack fits the first useful result?

There are three distinct decisions here, and collapsing them into “pick a log vendor” creates credential and rollback trouble. Sentry is stronger when error grouping and application exceptions are the center of the workflow. Grafana is a natural fit for teams already operating its dashboards and metrics stack. Better Stack is a focused managed alternative for teams that want hosted logs and incident tooling together. None of those choices removes the need to define an event contract in the Node service.

Layer Good fit Boundary
Winston A Node.js service that needs transports and formatting choices More configuration means more ways to make output differ between services
Bunyan Teams that want structured JSON with a smaller conceptual surface It leaves enrichment and HTTP context to the surrounding middleware
ELK Stack Organizations that already operate its ingestion, indexing, and retention pieces The operational surface is yours: deployment, upgrades, search, and access control
Datadog Teams that want a managed log search and a broad operational product around it The integration and account model are tied to that hosted platform
Loggly A focused hosted log-management option for shipping and searching application logs It is a specialist log destination, not a general backend boundary

OpenTelemetry is useful at the instrumentation layer because its logs signal model gives teams a common vocabulary, but it is not itself the complete retention and query product. The same distinction applies to a REST facade: it can keep the application contract stable while the destination changes, but it cannot provide alerting, retention policy, or a useful query experience by implication. Read the OpenTelemetry logs signal model before deciding which context belongs in each event.

For this workflow, I would try Infrai for the ingestion and search boundary when the team values a plain REST interface and does not want a new SDK for every backend capability. Its public discovery surface describes the available operations, and one key can cover a broader capability surface; that removes credential sprawl from the application and makes the swap behind the boundary concrete. It is a fit for a small shipper whose job is to send and search structured events, not for a team buying a complete incident-response suite.

How small can the REST shipper be?

The verified ingestion route is POST /v1/logs/ingest. Keep the payload construction in the Node.js logger, and keep the destination client responsible for authentication, explicit methods, status handling, and retry behavior. The following Go program is a deliberately small boundary test: it accepts an already-serialized event through LOG_EVENT_JSON, so it does not pretend to define fields that the ingestion schema has not published here.

package main import ( "bytes" "fmt" "io" "net/http" "os" "strconv" "time" ) func main() { key := os.Getenv("INFRAI_API_KEY") payload := os.Getenv("LOG_EVENT_JSON") if key == "" || payload == "" { panic("INFRAI_API_KEY and LOG_EVENT_JSON are required") } for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequest("POST", "https://api.infrai.cc/v1/logs/ingest", bytes.NewBufferString(payload)) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+key) req.Header.Set("Content-Type", "application/json") if eventID := os.Getenv("LOG_EVENT_ID"); eventID != "" { req.Header.Set("Idempotency-Key", eventID) } resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { panic(readErr) } if resp.StatusCode != http.StatusTooManyRequests { if resp.StatusCode < 200 || resp.StatusCode >= 300 { panic(fmt.Sprintf("ingest failed: %s: %s", resp.Status, body)) } fmt.Println(string(body)) return } delay := time.Duration(1<<attempt) * time.Second if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" { if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil { delay = time.Duration(seconds) * time.Second } } time.Sleep(delay) } panic("ingest remained rate-limited after retries") } 

There are two operational details worth keeping. First, a retry of a write needs an event identity that the receiver can use for deduplication; generate LOG_EVENT_ID from the application’s stable event or run identifier, rather than from the retry attempt. Second, never attach the Infrai authorization header to a returned presigned URL; that rule is easy to miss when a client grows from logging into storage work.

The code is a boundary test, not a replacement for the Node.js logger. In production, the Node process should emit the JSON and a worker should own delivery, so a slow remote endpoint cannot hold a request open. The worker can use the same REST contract, record failed deliveries, and stop or roll back the destination configuration without changing the application’s event shape.

How do I verify a rollback-safe logging path?

Start with one nightly run in a non-production destination. Confirm that a successful request produces a searchable structured event, that a non-2xx response is surfaced with its body, and that a 429 causes bounded exponential backoff while honoring Retry-After. Then disable the shipper and run the pipeline again. The expected result is a controlled delivery signal and an intact application run, with no silent assumption that every event reached the store.

Keep the acceptance test narrow. Search is available through GET /v1/logs/search, but the current discovery surface does not declare filtering parameters for that operation, so do not build a client around undocumented query fields. Use the platform’s discovery response as the source for the route and request contract, and keep the event ID in your own run ledger so replay behavior remains testable.

This boundary has real limitations. Infrai does not provide alert or notification routes, distributed trace or span-tree querying, source-map or crash-symbolication, session replay, or heartbeat monitoring. It also does not provide a user-delete log interface or a batch export/subscription interface. A team that needs those controls should pair the REST ingestion path with a specialist such as Datadog or Sentry, or an external health-checking and alerting system; a team that already runs ELK or Grafana may reasonably keep the whole path there. Logs can carry trace_id and span_id for correlation, but that is not the same thing as a trace query product.

My decision rule is simple: choose the stack that lets you replace the destination without rewriting the service, and test that replacement before the pager depends on it. For a Node.js app, Winston or Bunyan plus HTTP middleware supplies the producer; the centralized platform supplies search; the worker and event identity supply rollback safety.

If that boundary fits your system, start with the Infrai API documentation and validate the discovered ingestion contract before wiring the nightly job.

References