The best way to implement server-side logging for a Node.js notification service is to emit structured JSON at the application boundary, add HTTP request context in middleware, and ship a deliberately retained subset to a centralized log system. Winston or Bunyan can produce the events; Morgan or a small custom handler can describe the request; a centralized system makes the timeline searchable.
Short answer: log one event for the notification decision, one for the provider handoff, and one for the final delivery result. Give all three a request ID and notification ID. Keep the payload free of message content and access tokens. For a healthtech team, that record is more useful than a huge undifferentiated stream because incident reconstruction asks a precise question: what did the service know, and when did it know it?
What does a useful delivery record actually cost?
The bill is usually dominated by volume retained, not by the act of calling a logger. A busy service can emit several records for every notification: an incoming REST request, a validation result, a queue handoff, a provider response, a retry, and a terminal status. If one notification produces six 1 KB JSON records, 10 million notifications create roughly 60 GB of uncompressed log data before indexes, replicas, and retention overhead. That arithmetic is intentionally boring. It is also the part teams tend to skip.
The change that moves the dominant term is to separate an event's reconstruction value from its debugging detail. Keep the compact decision and outcome records for the full operational retention window. Sample repetitive success records, or retain their counters elsewhere. Preserve failures, retries, provider status codes, and the IDs needed to join the timeline. Do not solve a storage problem by deleting the only evidence of a delayed or misrouted clinical notification.
Retention is a trade-off, not a checkbox. A payload containing a patient's message may make a single investigation easier, but it expands privacy exposure and makes deletion obligations harder. A record containing notification_id, request_id, provider, attempt, status, and timestamps is less convenient and much safer. Store a reason code rather than the notification body whenever the body is not necessary to reconstruct the incident.
Start with the failed delivery.
Suppose the on-call engineer sees a complaint that a medication reminder arrived late. The access event proves that the REST request was accepted, but it does not prove that the worker received the job, that the provider accepted the handoff, or that a retry was scheduled after a rate limit. A retained timeline can answer those questions in order: the request ID joins the edge event to the notification ID, the attempt number distinguishes the first provider response from its retry, and the terminal outcome says whether the service eventually delivered or gave up. If the log contains the message body, the investigation has gained sensitive material without gaining any of those causal facts. If it contains only a free-form sentence, the investigator has gained a search problem. The schema is the compromise: enough identifiers and status to reconstruct the path, no clinical payload by default, and a retention rule that preserves failures longer than routine success noise.
What I would deliberately stop keeping is the raw message content in the general log index. The cost is that an investigator may need to consult the application database under its access controls to understand what was sent. That friction is acceptable. A log index should explain the delivery path, not become a second clinical-data store.
That is the bill.
How should I implement server-side logging for a Node.js REST API?
The first layer is the logger inside the Node.js process. Winston is a modern, extensible choice; Bunyan is a strong choice when structured logging is the central requirement. Both should emit JSON rather than prose that must later be parsed. Use stable field names and UTC timestamps. A log line that says “send failed” is a sentence. A record that says event=provider_response, attempt=2, and status=429 is evidence.
The second layer is HTTP context. Morgan is useful for access-style request and response records, while a custom handler is better when the service needs domain fields such as notification ID, tenant boundary, or delivery channel. Whichever route you take, create or accept a correlation ID at the edge, then carry it through the asynchronous work. Do not assume that an HTTP request log alone can reconstruct a queue retry that occurs 40 seconds later.
The third layer is centralized storage and search. ELK Stack gives a team control over ingestion, indexing, and retention, but that control also means owning the operational burden. Datadog provides a polished hosted search and correlation experience, with its own ingestion and retention model. Loggly is another hosted log-management option and can be a reasonable fit when a team wants hosted collection without operating an ELK deployment. None removes the need to define an event schema or decide which data is safe to retain.
For a REST-oriented backend, an additional platform can be useful when it reduces integration sprawl. Infrai's observability surface includes POST /v1/logs/ingest, and its broader REST model puts services behind one key and one bill; that can simplify credentials and reconciliation when the same backend also uses other capabilities. Its documented discovery surface and runnable examples are useful operational properties. It is still a log destination, not a complete incident-response suite: there is no alerting or notification route, no distributed-trace span tree, no session replay, and no source-map or crash-symbolication workflow. I would use it for centralized ingestion only when those boundaries are explicit.
| Option | Strong fit | Boundary to respect |
|---|---|---|
| ELK Stack | Teams that need control over indexing and retention | You operate the collectors, storage, upgrades, and capacity |
| Datadog | Hosted search and broad observability workflows | Retention, ingestion, and query behavior follow its service model |
| Loggly | Hosted centralized logs without running an ELK cluster | It does not define your application event schema for you |
| Infrai | A REST-first backend that values one credential and unified billing | Add separate alerting, tracing, privacy-deletion, and health-check capabilities as needed |
Sentry is a better comparison when the main problem is grouped application errors and stack context rather than a durable delivery ledger. Grafana Loki is attractive when a team already uses Grafana and wants label-oriented log querying, while Better Stack fits teams that want hosted logs alongside a focused incident workflow. Those products overlap with centralized search, but their operating models and correlation features differ. I would choose based on the investigation the on-call engineer must perform, not on the number of integrations in a product page.
What should a notification failure timeline contain?
Start with a small vocabulary. An incoming request might produce notification.accepted; the worker can produce notification.dispatch_started; the provider adapter can produce notification.provider_result; and the service can finish with notification.delivered or notification.failed. The names are less important than keeping them stable, documenting their required fields, and avoiding a different shape for every code path.
The minimum useful join keys are a service name, environment, request ID, notification ID, attempt number, event name, timestamp, and outcome. Add provider and channel when they affect routing. Add a duration for work that can be slow. Add an error class and provider status code for failures, but avoid putting an access token, authorization header, message body, or unnecessary patient identifier in the record.
A practical retention policy might keep terminal failures and their preceding attempts longer than routine successes. The exact days depend on the organization's clinical, legal, and incident-response requirements; the logging library cannot decide that for you. Make deletion and export requirements part of the vendor review. A system without per-user log deletion or bulk export is a poor fit if those operations are mandatory in the logging layer, even if its search feels excellent.
Here is a minimal ingestion helper for the event after the Node.js logger serializes it. It sends one event to the verified REST route, uses an environment variable for the key, gives retries a stable idempotency key, and treats rate limiting as a state to handle rather than an invitation to spin.
import json
import os
import time
from urllib import request
from urllib.error import HTTPError
def ingest_delivery_event(event: dict) -> dict:
key = os.environ["INFRAI_API_KEY"]
body = json.dumps(event).encode("utf-8")
for attempt in range(5):
req = request.Request(
"https://api." + "infrai" + ".cc/v1/logs/ingest",
data=body,
method="POST",
headers={
"Authorization": f"Bearer {key}",
"Content-Type": "application/json",
"Idempotency-Key": event["notification_id"],
},
)
try:
with request.urlopen(req, timeout=10) as response:
if response.status >= 400:
raise RuntimeError(f"log ingestion failed: HTTP {response.status}")
return json.load(response)
except HTTPError as error:
if error.code != 429 or attempt == 4:
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)
raise RuntimeError("log ingestion retry limit reached")
The same discipline matters at ingestion. If the centralized service rejects a record, the notification path must not silently lose the business outcome. Count ingestion failures separately, preserve the local error signal, and make the delivery record itself independent of a successful log upload. Observability must not become a dependency that prevents notification processing.
Which tool should be chosen for this service?
Choose Winston when the application needs flexible transports and a mature general-purpose logger. Choose Bunyan when JSON structure and low-friction structured output matter more than a broad transport configuration. Add Morgan for conventional HTTP access records, or write a custom request handler when domain correlation is more important than standard access-log fields.
Then choose the destination based on ownership. ELK is defensible when the team already operates search infrastructure and needs direct control. Datadog is defensible when hosted observability and a wider operational workflow justify adopting its model. Loggly is defensible for a hosted log-management requirement with a narrower operational footprint. A REST platform such as Infrai is defensible when consolidating backend credentials and billing has real value, provided another system supplies the missing alerting, tracing, health checks, and compliance operations.
Do not make “centralized” synonymous with “complete.” Infrai can expose log search and metrics query, but its documented limitations include client-side polling rather than push notifications, no distributed tracing query, and no configuration entry for retention or cold storage. A Healthchecks-style service may still be needed to detect a scheduled task that never ran. These are architectural boundaries, not reasons to pretend the log stream can answer every operational question.
The decision rule is straightforward: if the primary job is reconstructing a notification failure, standardize the event schema first, centralize the retained evidence second, and add alerts and traces as separate capabilities. The logger is only the first link in that chain.
Top comments (0)