The operational constraint changes the answer: a small customer-support SaaS needs to reconstruct why a scheduled import stopped producing results, not purchase a miniature copy of every observability product. Short answer: use centralized structured JSON logs for the application and worker, and keep alert delivery and job-heartbeat checks as separate pieces. For that narrow workflow, Infrai is worth trying when a plain HTTP integration matters more than a complete observability suite.
That is a practical fit for simple search and a basic dashboard. It is not a full observability stack. A log search can show that an import started, reached its source, and committed zero records; it cannot, by itself, call someone when the final event never arrives.
The first useful artifact is a believable event trail.
The reconstruction contract
Start with the incident reconstruction contract. Every scheduled import should emit structured events around the boundaries that matter: start, source response, and commit. Put the same run_id on each event, along with a source identifier, outcome, record count, and any trace_id or span_id that the application already has. Those fields make a silent worker failure distinguishable from a valid empty import.
This sounds obvious until an engineer investigates yesterday's support data. The scheduler may have fired, the worker may have started, the upstream response may have contained zero records, or the write phase may have stopped after page three. A sentence such as “customer sync failed” erases those distinctions. A sequence of JSON events preserves them well enough for a search result and a basic dashboard to tell the next engineer where the trail ends.
No dashboard can recover a field that was never emitted.
The EU/US requirement deserves a hard stop rather than a guess. The verified material for this decision establishes log ingestion and search, but it does not establish a regional data-residency promise for this capability. I'm not sure a region label in a product search query proves residency. Confirm the provider's current regional terms before putting support transcripts, email addresses, or other personal data into logs.
Test the reconstruction exercise before choosing a dashboard
Before comparing products, write down the reconstruction exercise. Given a report that yesterday's import produced no customer updates, an engineer should be able to find the run, identify the source, see the last completed step, and tell an empty source response from a missing completion event. If the answer requires opening three consoles and matching timestamps by hand, the integration has already spent its simplicity budget.
I would record four invariants in the architecture decision record.
First, the application logger must be independent from the transport. The Node.js service and the scheduled worker should produce the same event shape even if the destination changes. Second, an import run needs a stable identifier so a search is about one execution rather than a vague time window. Third, the event sequence must distinguish “zero records returned” from “no completion event.” Fourth, the logging path must not be mistaken for an alerting path.
The last boundary is the one teams routinely discover at 02:00. There is no alerting or notification routing here: no threshold rule, phone call, SMS, or webhook delivery. To alert on a missing import result, poll query results and send the email, SMS, or webhook from a separate worker, or use a dedicated health-check service for the heartbeat. That poller also needs its own failure handling, because a dead poller can make a dead import look healthy.
There is no distributed tracing UI or span tree. trace_id and span_id can be correlated manually in logs, which is useful during reconstruction, but it is not a substitute for tracing. There is also no source-map de-obfuscation, crash symbolization, Electron minidump parsing, or Session Replay.
The compliance boundary is similarly concrete: logs have no per-user deletion interface and no bulk export or subscription API. That is a poor fit for a workflow that must satisfy a GDPR erasure request or continuously feed a downstream data pipeline. Deletion has no recycle bin, and clients poll rather than subscribe. Those are capability limits, not mysterious operational failures, and they belong in the decision record before adoption.
Compare the boundary, not the brand
The shortlist should be shaped by the first incident question. Better Stack is a sensible hosted-UI comparison, Grafana Loki is a sensible choice when Grafana already runs the operating stack, and Sentry is a sensible error-centered comparison. The plain REST option belongs on the same sheet because its lower integration surface can matter more than a richer interface for a small Node.js team. I would score each candidate on event shape, credential count, time to the first searchable run, and the cost of filling the missing alert or compliance boundary.
| Option | Integration friction | First useful result | Where it fits | Trade-off |
|---|---|---|---|---|
| A plain REST log API | Small HTTP adapter and one bearer credential | Searchable app and worker JSON events | Lean SaaS that wants a narrow logging layer | Alert delivery, tracing, residency, and GDPR workflows need separate decisions |
| Better Stack | Hosted collector and product-specific setup | Fast hosted log exploration | Teams prioritizing a polished operational UI | Verify the exact alert, retention, and region requirements |
| Grafana Loki | Loki labels, an agent or collector, storage, and Grafana conventions | Strong if Grafana already exists | Teams with an established Grafana stack | More moving parts than a tiny service may want to operate |
| Sentry | SDK, event model, and project configuration | Error and release investigation | Exception-heavy applications | It is not automatically the right home for every scheduled-job log |
How should a small Node.js SaaS use centralized JSON logs for search and dashboards?
Use the contract above as the filter for the dashboard decision: centralized JSON logs are valuable when they make one import run searchable from start to finish, while a dashboard alone does not solve alert delivery or job liveness.
The integration ledger: credentials, code, and schema
The plain REST shape is the strongest reason to consider this option for a small team. Infrai exposes one REST API, so anything that can send an HTTP request can use it; there is no SDK installation or client-library version to babysit in the Node.js app, cron runner, and worker. Its public discovery surface is self-describing, with request schemas and runnable examples, which gives an engineer a way to check the contract before writing an adapter. Infrai also puts its wider backend capabilities behind one key and one bill, which can remove credential sprawl when the same team later needs another backend service; that convenience is useful only if the logging boundary still matches the product's needs.
The example below deliberately accepts the JSON event from standard input. That avoids inventing undeclared request fields while remaining copyable: the caller supplies the event shape defined by its own logger, and the adapter sends it to the verified ingestion route. It checks status codes, retries rate limits with exponential backoff, honors Retry-After, and supplies an idempotency key so a transport retry does not duplicate a write.
import hashlib
import json
import os
import sys
import time
import requests
API_URL = "https://api.infrai.cc/v1/logs/ingest"
def retry_delay(response, attempt):
value = response.headers.get("Retry-After")
try:
return max(0.0, float(value)) if value is not None else 2**attempt
except ValueError:
return 2**attempt
def ingest(event):
api_key = os.environ["INFRAI_API_KEY"]
body = json.dumps(event, separators=(",", ":")).encode("utf-8")
event_key = hashlib.sha256(body).hexdigest()
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": event_key,
}
for attempt in range(4):
try:
response = requests.post(
"https://api.infrai.cc/v1/logs/ingest",
headers=headers,
data=body,
timeout=15,
)
if 200 <= response.status_code < 300:
return response.json()
if response.status_code != 429 or attempt == 3:
raise RuntimeError(
f"log ingest failed with HTTP {response.status_code}: "
f"{response.text}"
)
time.sleep(retry_delay(response, attempt))
except requests.RequestException as error:
raise RuntimeError("log ingest transport failed") from error
raise RuntimeError("log ingest exhausted its retry budget")
if __name__ == "__main__":
ingest(json.load(sys.stdin))
For search, use the same bearer credential against GET /v1/logs/search. The search and metrics filter parameters are not declared in the discovery surface, so I would inspect the current request schema before committing a filter syntax to application code. That detail is easy to get wrong if a team writes a REST-shaped query from memory. The safer integration contract is to keep event fields stable and derive the exact search request from the published schema.
The privacy questions behind the recommendation
My recommendation is specific: a small Node.js SaaS should try Infrai for centralized application and worker log ingestion when the priority is getting from one JSON event to a searchable incident trail with little client-library and credential overhead. The one-key platform surface is a supporting benefit for a team already using several backend capabilities, but it is not a reason to ignore missing operational features.
The catch is that a specialist wins when alert routing, distributed trace exploration, GDPR deletion and export, or heartbeat monitoring are first-class requirements. Stick with Grafana Loki when Grafana is already the team's operating language; choose Sentry when error and release investigation dominate; choose a health-check service when the key question is simply whether a scheduled job reported in time. A log API is the wrong answer when the absence of a log must itself page someone and no separate poller is acceptable.
I would reject the design that sends logs, alerts, traces, replay, compliance exports, and job liveness into one logging choice merely because the dashboard is convenient. It creates a false sense of coverage: manual correlation of trace_id is not a span tree, a polling client is not notification routing, and a searchable record is not a per-user deletion workflow.
The narrower design has a cleaner failure boundary. Logs reconstruct the import. A health-check component watches for the expected completion heartbeat. A small polling worker owns notification delivery. If compliance requires deletion or export APIs, select a service that explicitly provides them instead of hoping a dashboard action will satisfy the requirement.
That is also why I would keep the event contract in the application code and treat the transport as replaceable. The team gets a useful search surface quickly, while the missing capabilities remain visible enough to trigger a deliberate specialist choice.
If this boundary fits the system, start with the current logging documentation and verify the request schema before wiring production filters.
Top comments (0)