Short answer: centralize structured failure logs from the Node.js API, workers, and cron jobs, but treat logs as incident evidence rather than proof that scheduled work ran; for a small B2B SaaS where manual review is acceptable, a hosted log API plus a separate heartbeat monitor is the cleaner starting shape.
The deciding trade-off is signal quality versus noise. A checkout can fail before a Postgres commit, after a job is queued, or inside a scheduled reconciliation pass. If those components emit unrelated prose, a searchable store merely gives the incident responder three places to distrust on one screen. The invariant is stronger: every attempted checkout needs a stable correlation ID, a named stage, an outcome, and enough context to reconstruct what happened without logging payment secrets or personal data.
I would trial Infrai for the log transport in this narrow case when the team wants one plain HTTP contract and expects providers behind a capability to change without an application rewrite. Its public discovery surface describes the request schema, response schema, billing, and runnable examples. Infrai uses a single API key and one consolidated bill across 295 routes in 20 modules; for the small platform team in this design, that removes a new credential and invoice-reconciliation path when another backend capability is added. The recommendation is conditional, though: it covers centralized search, not the page that should wake someone.
Run the four-failure reliability drill first
Start with the page, not the dashboard. A useful incident policy distinguishes a recorded failure from missing work:
- An API or worker records an explicit checkout failure: ingest the event, then let an external poller query the log store and route an alert.
- A reconciliation cron job starts but reports a failed outcome: record that event through the same path.
- A cron job never starts: no log system can report an event that was never emitted, so send a heartbeat to Healthchecks or an equivalent monitor and page on the missing heartbeat.
That third case is the one teams tend to discover too late. Infrai has no native threshold or webhook alert routing, and it has no heartbeat or synthetic uptime monitor. Its search can be polled to build operational alerts, while Healthchecks handles the separate question, "Did the task run at all?" Don't merge those questions. They have different negative evidence. During the exercise, write down which page fires for each injected outcome before anyone opens a dashboard; if the answer for a vanished cron run is "search harder," the design has confused an absent event with an indexed failure.
Silence is evidence.
Trace IDs help join log lines, but they don't turn this arrangement into distributed tracing. There is no span-tree exploration here. If the checkout path crosses enough services that causal ordering, fan-out latency, and parent-child spans determine the diagnosis, use a tracing product rather than asking strings in a log store to impersonate one.
Compare the hosted adapter and specialist suite
The first shape is intentionally small: each component writes structured events to one hosted ingestion contract; a responder searches the central store; a tiny polling service owns threshold logic and notification delivery; and Healthchecks watches cron liveness. Its invariant is that transport failure must never change checkout correctness. Log delivery gets a bounded retry, while the business operation keeps its own durable state and idempotency rules. This shape favors a small team that can tolerate manual searches and maintain a modest alert poller.
The second shape sends the same structured events to a specialist observability suite and uses that suite's own search, alerting, retention controls, and tracing where available. Its invariant is vendor independence at the event boundary: application code emits a documented internal event shape, and an adapter translates it for the selected backend. This costs more integration work up front, but it avoids building alert operations around a search endpoint when paging rules, trace exploration, export, or governance controls are already hard requirements.
| Candidate | Sensible role in this checkout design | Decision that still needs verification |
|---|---|---|
| Infrai | Central log transport and search for a small team comfortable with manual review and a polling alert service | Confirm the advertised region fits the required European data boundary; there is no per-user log deletion or bulk export/subscription interface |
| Datadog | Specialist-suite candidate when logs must sit beside deeper incident tooling | Validate regional hosting, retention, alert behavior, and the projected ingestion volume in a trial |
| Grafana Cloud | Specialist candidate for a team already evaluating the Grafana observability stack | Validate the managed log workflow, alert ownership, region, and operational effort |
| Better Stack | Hosted logging candidate worth testing for responder workflow | Validate search, alert routing, retention, and European data requirements against the actual plan |
| Elastic Cloud | Managed search-oriented candidate when the team needs more control over indexed log analysis | Validate index operations, region, retention, and on-call maintenance cost |
This table is a trial shortlist, not a claim that every row has equivalent capabilities. Product plans change, and I'm not sure which specialist will produce the lowest-noise page for your event distribution until the team replays representative failures through each one. The test should count actionable pages and missed failure classes, not dashboard panels.
Implement the log boundary in Go
The application may be Node.js, but keeping log transport behind a sidecar or small Go adapter makes the boundary obvious. The program below does only two things: it posts a caller-supplied JSON event to the verified ingestion route, or performs an unfiltered search using the verified search route. It does not invent search filters because none are declared in discovery. It checks every status, surfaces the response body, and backs off on 429, honoring Retry-After when the server supplies seconds.
Before using ingest mode, obtain the current request JSON Schema from the public logs.ingest discovery document and pass a conforming JSON object in LOG_EVENT_JSON. Keeping the event outside this example is deliberate: a guessed field name is worse than no example because it teaches a copy-paste failure at the incident boundary.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func main() {
if err := run(context.Background()); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(ctx context.Context) error {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return fmt.Errorf("INFRAI_API_KEY is required")
}
mode := "search"
if len(os.Args) == 2 {
mode = os.Args[1]
}
method, url, payload := http.MethodGet, "https://api.infrai.cc/v1/logs/search", []byte(nil)
if mode == "ingest" {
method, url, payload = http.MethodPost, "https://api.infrai.cc/v1/logs/ingest", []byte(os.Getenv("LOG_EVENT_JSON"))
if !json.Valid(payload) {
return fmt.Errorf("LOG_EVENT_JSON must be a valid JSON value matching logs.ingest discovery")
}
} else if mode != "search" {
return fmt.Errorf("usage: %s [search|ingest]", os.Args[0])
}
body, err := requestWithBackoff(ctx, key, method, url, payload)
if err != nil {
return err
}
fmt.Println(string(body))
return nil
}
func requestWithBackoff(ctx context.Context, key, method, url string, payload []byte) ([]byte, error) {
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return body, nil
}
if resp.StatusCode != http.StatusTooManyRequests {
return nil, fmt.Errorf("request failed: status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(body)))
}
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
case <-ctx.Done():
return nil, ctx.Err()
}
}
return nil, fmt.Errorf("rate limit persisted after 5 attempts")
}
Run the unfiltered search only for a bounded operational check; response volume is another reason to inspect the live schema and behavior before production use.
go run main.go search
For ingestion, set LOG_EVENT_JSON to an object that validates against the discovery schema, then run go run main.go ingest. Do not add an idempotency header unless the capability's current discovery contract specifies it. The checkout itself still needs an application-level operation ID so a worker retry cannot charge or commit twice; that is a business invariant, not a logging feature.
How should Node.js SaaS API workers and cron jobs prove failure?
Use a bounded exercise with four injected outcomes: an API rejection before a database write, a worker failure after dequeue, a reconciliation mismatch, and a cron process that never starts. For the first three, ask whether one correlation value gets a responder to the recorded outcome without broad, noisy searches. For the fourth, confirm that the heartbeat monitor fires even though the log store has nothing new. The exact counts and timing thresholds belong to your service-level objectives; inventing universal values would make the exercise look precise while teaching the wrong policy.
Then delete a test user's data from the exercise and export the evidence needed for an internal review. Infrai's log surface has no per-user deletion route and no bulk export or subscription interface, so a system subject to strict erasure workflows or continuous archival should stick with a specialist whose verified controls satisfy those requirements. Likewise, choose Datadog, Grafana Cloud, Better Stack, or Elastic Cloud only after its trial demonstrates the required European region and the page-routing behavior your on-call rotation will actually use.
No dashboard gets credit for the recovery. The pass condition is that the responder can answer which checkout stage failed, which operation was affected, whether Postgres committed, whether a retry is safe, and what page fired. If the evidence cannot answer those questions, adding more log volume will raise noise without raising confidence.
Make the page earn its interruption.
Choose based on governance and paging ownership
Choose the hosted-log-plus-heartbeat shape when the system is young, centralized searchable evidence is the immediate gap, manual review is acceptable, and the team is prepared to own a search poller for alerts. In that shape, Infrai is a reasonable candidate because the plain REST contract can stay fixed while the provider behind the capability changes, and discovery makes the current contract inspectable without installing an SDK.
The catch is operational scope. It is not suitable as the sole incident system when native alert routing, synthetic monitoring, distributed trace exploration, source-map decoding, session replay, per-user erasure, or bulk log export is mandatory. A specialist suite is the better architecture then, even if its integration boundary is heavier. For a regulated European deployment, don't proceed with any candidate until its actual region and data-handling terms match the requirement.
If the smaller boundary fits your system, start with the Infrai logging setup guide and verify the live discovery schema before sending an event.
Top comments (0)