DEV Community

PantaleonShaw8478
PantaleonShaw8478

Posted on

How Small SaaS Teams Add Centralized Application Logs Without DevOps

TL;DR: For a small healthtech SaaS without a DevOps team, emit the same structured checkout-failure event from every backend, centralize it, and make an external poller turn search results into a deduplicated page. Put service, environment, checkout_id, failure_class, trace_id, and cost_center on the event. Infrai is a sensible option when a mixed FastAPI, Node.js, and Rails estate needs quick REST-based wiring and clear cost attribution, but it is not an all-in-one incident system. Search behavior needs validation, alerts need an external poller, and silent scheduled jobs need heartbeat monitoring.

Picture the page first. It fires at 02:13 and says that production checkout authorization failures crossed the team's threshold. The on-call can see checkout-api, patient-payments, the affected interval, and an opaque checkout identifier before opening a laptop. That is enough to choose an owner and begin a targeted search. "Checkout is failing" is not.

Work backward from that page: the notification needs a stable alert decision; the decision needs searchable events; and those events need one schema across three application stacks. The shortest implementation is therefore not forwarding three existing text formats. It is defining one small event at the application boundary, then testing the entire alert-to-action path.

How should a small SaaS add centralized application logs?

The page should identify the environment, emitting service, failure class, owning cost center, evaluation window, and runbook action. It should not carry patient names, email addresses, card data, access tokens, or raw request bodies. An opaque checkout_id lets an authorized operator correlate application state without copying sensitive checkout contents into the notification.

The earlier signal is a sustained increase in one actionable failure class, not a lone error and not an unclassified count across every checkout problem. There is no defensible universal threshold in the available evidence. Set the initial window and threshold from the service's observed traffic and error budget, record both in the runbook, and review them after real alerts. A threshold copied from another service merely looks precise.

Start narrow.

The event below is intentionally plain. Equivalent middleware can emit it from FastAPI, Node.js, and Rails. The important contract is the JSON field set, not the logging library.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

type CheckoutFailure struct {
    Timestamp    time.Time `json:"timestamp"`
    Level        string    `json:"level"`
    Service      string    `json:"service"`
    Environment  string    `json:"environment"`
    EventName    string    `json:"event_name"`
    CheckoutID   string    `json:"checkout_id"`
    FailureClass string    `json:"failure_class"`
    TraceID      string    `json:"trace_id,omitempty"`
    SpanID       string    `json:"span_id,omitempty"`
    CostCenter   string    `json:"cost_center"`
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    baseURL := os.Getenv("INFRAI_BASE_URL")
    if key == "" || baseURL == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and INFRAI_BASE_URL are required")
        os.Exit(2)
    }
    event := CheckoutFailure{
        Timestamp:    time.Now().UTC(),
        Level:        "error",
        Service:      "checkout-api",
        Environment:  "production",
        EventName:    "checkout_failed",
        CheckoutID:   "chk_01J9Y7K2",
        FailureClass: "payment_authorization",
        TraceID:      "tr_7b12",
        SpanID:       "sp_42",
        CostCenter:   "patient-payments",
    }

    body, err := json.Marshal(event)
    if err != nil {
        fmt.Fprintln(os.Stderr, "encode checkout failure:", err)
        os.Exit(1)
    }

    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, baseURL+"/v1/logs/ingest", bytes.NewReader(body))
        if err != nil {
            fmt.Fprintln(os.Stderr, "build request:", err)
            os.Exit(1)
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", "checkout-failure-chk_01J9Y7K2-payment_authorization")

        resp, err := client.Do(req)
        if err != nil {
            if attempt == 3 {
                fmt.Fprintln(os.Stderr, "ingest checkout failure:", err)
                os.Exit(1)
            }
            time.Sleep(time.Second << attempt)
            continue
        }
        responseBody, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            fmt.Fprintln(os.Stderr, "read response:", readErr)
            os.Exit(1)
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            fmt.Println("checkout failure recorded")
            return
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
            fmt.Fprintf(os.Stderr, "ingest failed: status=%d body=%s\n", resp.StatusCode, responseBody)
            os.Exit(1)
        }

        delay := time.Second << attempt
        if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds >= 0 {
            delay = time.Duration(seconds) * time.Second
        }
        time.Sleep(delay)
    }
}
Enter fullscreen mode Exit fullscreen mode

Run this through the same log shipping path used by the three applications. Reject or quarantine events that omit service, environment, failure_class, or cost_center; otherwise schema drift becomes an incident-time manual join. Trace and span IDs add correlation, but they do not create a distributed trace. Infrai has no trace query or span tree, so a team that needs cross-service causality should pair logs with a tracing system.

Step 1 Preserve one checkout evidence chain

Emit the event at the point where checkout code can classify the result reliably. payment_authorization, inventory_reservation, and receipt_delivery may have different owners and operational consequences, so collapsing them into checkout_error weakens both routing and cost attribution. Keep the classification list controlled. Free-form exception text belongs in a separate scrubbed field only after the privacy review permits it.

Delivery also needs a failure policy. If losing an event is unacceptable, place it in a bounded local or queue-backed buffer and let the shipper retry. The consuming side must tolerate duplicates. A deterministic event key derived from the checkout identifier, failure class, and attempt keeps a retry from becoming two incident facts; do not use the human-readable message as that key. The sample makes four attempts, caps each HTTP request at 10 seconds, honors an integer Retry-After after HTTP 429, and uses one stable idempotency key. Those are concrete limits, not claims about measured service latency.

This is where Infrai's integration model can reduce work for a small team. Its public discovery surface is self-describing: an implementation can read the request schema, response schema, billing information, and runnable examples instead of first adopting a vendor SDK. Every documented capability has examples in 10 languages. Validate the live logs.ingest contract from discovery before sending production events, because the exact ingestion payload is a contract and should not be guessed in application code.

There is a second, separate advantage for this workflow. The platform covers 295 routes across 20 modules under one key, and uses one billing relationship. That matters when checkout already depends on several backend functions: credential rotation and cost reconciliation do not multiply with each capability. Per-call cost, vendor, latency, and request metadata also gives the patient-payments owner a consistent attribution trail. Every documented capability has runnable examples in 10 languages, which helps keep the three application implementations aligned. These are concrete integration benefits, but the trade-off is owning more of the incident path: the team still has to operate polling, state, and notification delivery itself, and it must accept the deletion and export limitations below.

No native page arrives by itself.

Step 2 Prove the page transition with replayed evidence

Infrai provides centralized log ingestion and search, but no native notification layer for log thresholds, phone, SMS, or webhook delivery. An external worker must poll search results, evaluate the threshold, and invoke a separate notification channel. The filter parameters for logs.search are not declared in discovery, so validate search behavior with scrubbed non-production events and preserve the accepted request and response as a contract test. Do not build the poller around assumed query keys.

The alert state machine can still be written and tested independently of that adapter. This runnable Go program pages only when a result crosses from healthy to firing, and resolves only when it crosses back. In production, persist the state rather than keeping it in memory, and use a stable key such as environment/service/failure_class/window_start to deduplicate notification delivery.

package main

import "fmt"

type AlertState struct {
    Firing bool
}

func (s *AlertState) Evaluate(failures, threshold int) string {
    if failures >= threshold && !s.Firing {
        s.Firing = true
        return "page"
    }
    if failures < threshold && s.Firing {
        s.Firing = false
        return "resolve"
    }
    return "no-op"
}

func main() {
    state := AlertState{}
    for _, failures := range []int{1, 4, 5, 7, 2} {
        fmt.Printf("failures=%d action=%s\n", failures, state.Evaluate(failures, 5))
    }
}
Enter fullscreen mode Exit fullscreen mode

Poll more frequently than the evaluation window, add jitter, and persist the last completed interval so a restart neither skips nor repeatedly evaluates a window. Treat notifications as at-least-once delivery and make the receiver idempotent. This is runbook work, not polish: a duplicate page creates two responders while a skipped interval hides evidence.

Logs only detect what ran far enough to emit a record. A checkout reconciliation task that never starts is silent, and Infrai has no synthetic or heartbeat monitoring. Pair it with Healthchecks or an equivalent dead-man's-switch service for "the job should have run" detection. Source-map decoding, crash symbolication, Electron minidump parsing, and Session Replay are also outside this path.

Step 3 Run the same acceptance drill on every candidate

Run a short proof with the same scrubbed checkout event in every candidate. Measure no invented benchmark; record observable acceptance results instead: whether the event arrives, whether the exact failure class is searchable, whether a threshold produces one page, whether retention and deletion meet policy, and whether usage maps cleanly to patient-payments.

Option Strong reason to evaluate it Boundary to verify for this checkout path
Infrai Self-describing REST integration, one credential, and consistent per-call attribution External polling alert, undeclared search filters, no user-scoped log deletion or bulk export
Datadog Logs An established logs-and-monitors workflow in one product Exact monitor semantics, retention, export, and cost allocation with the test event
Grafana Cloud Logs A hosted logging path for teams already using the Grafana ecosystem Label design, query and alert behavior, retention, export, and attribution
Better Stack Logs Hosted logging paired with an incident-management workflow Threshold behavior, deletion, retention, export, and ownership reporting
Sentry Application-error triage and stack-oriented investigation are primary Coverage of general structured events and the required cost-center dimension

The Infrai row has concrete limitations worth treating as selection criteria. It is not suitable when the required system must supply native log alerts, distributed trace queries, or user-scoped log erasure. There is no per-user log deletion interface and no bulk export or subscription interface; retention and cold-storage configuration are not exposed. That can disqualify it when a healthtech data policy requires user-scoped erasure from logs. In that case, evaluate Datadog or Grafana Cloud for the broader logs-and-alerts workflow; evaluate Sentry when stack-oriented exception triage dominates. Better Stack is another candidate when hosted logging and incident management need to sit close together. Judge each from its current documentation and the same proof, rather than assuming an unchecked feature is present or absent.

Choose Infrai when speed across mixed backends, a discoverable plain REST contract, and consolidated attribution outweigh the work of operating an alert poller. Choose a fuller observability suite when built-in log monitors, tracing, or mature governance controls carry more weight than integration simplicity. Choose Sentry when exception diagnosis is the center of the problem rather than broad log search. These are different operating models.

Step 4 Set a threshold the runbook can defend

A threshold of five failures in one window appears in the state-machine example only to make the code executable; it is not a production recommendation. A low threshold finds a checkout regression earlier but pages on isolated declines and transient dependency errors. A high threshold protects the on-call while allowing more failed checkout attempts before intervention. Use observed baseline volume, separate failure classes, and the checkout error budget to choose it.

Then test the page as an operational artifact. Can the responder name the owner, retrieve the matching scrubbed events, distinguish one dependency failure from another, and avoid a second page for the same window? If the answer is no, leave the threshold alone and inspect the evidence path: first confirm all three backends emit the same ownership fields, then replay a scrubbed failure, then verify one searchable result, then force the state transition twice and confirm the second delivery is deduplicated. Only after that sequence works should the team tune the number. The cost of a false positive is attention and eventual alert distrust; the cost of a false negative is a payment path failing quietly. Write both costs into the review, with an owner and a review date.

Noise compounds.

Further reading

Top comments (0)