DEV Community

GageSterling2648
GageSterling2648

Posted on

Marketplace Health Endpoint Monitoring — 2 Small SaaS Observability Stack Boundaries

TL;DR: A small marketplace needs two evidence paths: internal telemetry that explains a failed scheduled job, and an external check that notices public unavailability without depending on that same system. Infrai is acceptable for the first path when one API key for job runs, logs, metrics, and captured errors is more valuable than a large specialist feature set. It is not a replacement for the second path. For a public US/EU service, pair it with an uptime product that provides synthetic checks and notifications.

That split also makes cost attribution less dishonest. Tag every internal record with the marketplace, workload, region, and tenant boundary you actually charge against; pay for an external probe as availability insurance, not as a second log warehouse. The aim is enough evidence to reconstruct an incident, not maximum telemetry volume.

What should a small SaaS observability stack monitor and attribute beyond its health endpoint?

I have been paged for missed jobs and duplicate deliveries. The painful cases weren't missing one more dashboard. They were cases where the scheduler said a run existed, the queue consumer couldn't prove which attempt committed, and the customer report arrived before those facts could be joined. I first treated “the job ran” as a useful incident fact; after duplicate delivery, I learned that it was only the start of the question. A run record without an attempt identity and an idempotency decision cannot prove which side effect happened.

Consider a marketplace settlement export. Its health endpoint can return 200 while yesterday's regional export never ran. Conversely, an export can finish correctly while the public API is unreachable from Europe. One probe cannot establish both truths.

The invariant is compact: retain the scheduled run identity, an idempotency key for each side effect, a terminal outcome, timestamps, region, tenant or cost center, and enough request/dependency context to explain the outcome. Logs carry the narrative, metrics expose the trend, and grouped errors identify repeated outage-causing exceptions. The run record answers the prior question: was work actually scheduled and attempted?

Keep the dimensions bounded. A useful record might carry service=settlement-export, region=eu, marketplace=primary, cost_center=finance-ops, and an opaque tenant_bucket; putting raw customer IDs into a high-cardinality metric makes both cost and deletion obligations harder to control. Detailed customer context belongs in access-controlled logs with a deliberate retention policy. This is cost attribution at the point where it remains useful: a finance or platform owner can assign the settlement-export evidence to a workload and region without turning every customer into a metric series. If a team cannot name the owner of a signal, its retention period, and the incident question it answers, I would not collect it by default.

This is the first evidence path. It is diagnostic, rich, and inside the application trust boundary.

It won't wake anyone up.

Put detection and diagnosis in separate cost centers

A health endpoint only becomes a monitor when an independent system calls it, decides what failure means, and delivers a notification. If the application polls its own endpoint, a regional network failure or a dead worker can silence both the service and its observer. Silence wins.

Infrai can store health results, application exceptions, and availability metrics for dashboards and debugging. It does not provide native threshold rules, webhook/email/SMS/phone notifications, synthetic checks, or heartbeat monitoring. An internal worker may poll queries and approximate an alert for an internal environment, but that arrangement should not carry a public SLA. It also has no distributed trace query or span tree; trace_id and span_id can correlate log records, but they do not turn the product into a tracing backend.

The same boundary applies to scheduled work. A successful /health response does not prove that the task that should have run at 02:00 ran. Use an external dead-man check for that absence. Short version: inside evidence explains; outside evidence detects.

For customer-facing US/EU availability, the second path should therefore originate outside the service and end in a notification channel the on-call rotation actually watches. Test failure modes, not the green dashboard: DNS failure, TLS failure, timeout, incorrect status, and a job that never checks in.

Implementation ledger: join each run to the evidence

The prevention point is the worker boundary. Assign an idempotency key before performing a settlement side effect, retain the scheduler's run_id, and emit the final evidence only after the worker knows the outcome. Standard queues are at-least-once, so a redelivery must find the same idempotency key and refuse to apply the settlement twice.

The following Go bridge fetches a specific cron run, wraps that unmodified response as a log event, and sends it to the log ingestion route with the same base URL and bearer key. The wrapper is intentionally obtained from the public discovery schema at runtime rather than guessed here: LOG_INGEST_TEMPLATE is a JSON object conforming to the current /v1/logs/ingest request schema, with the literal string __CRON_RUN_JSON__ at the field chosen to retain the run JSON. That keeps the sample runnable without inventing an undeclared request field. It also makes a schema change fail during deployment, where it belongs.

package main

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

func call(ctx context.Context, client *http.Client, method, path string, body []byte) ([]byte, error) {
    baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
    var last error
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, baseURL+path, bytes.NewReader(body))
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        if body != nil {
            req.Header.Set("Content-Type", "application/json")
        }

        resp, err := client.Do(req)
        if err != nil {
            last = err
            time.Sleep(time.Duration(1<<attempt) * time.Second)
            continue
        }
        data, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if retryAfter, err := time.ParseDuration(resp.Header.Get("Retry-After") + "s"); err == nil {
                delay = retryAfter
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("%s %s: status %d: %s", method, path, resp.StatusCode, data)
        }
        return data, nil
    }
    return nil, fmt.Errorf("request retries exhausted: %w", last)
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    jobID := os.Getenv("CRON_JOB_ID")
    runID := os.Getenv("CRON_RUN_ID")
    template := os.Getenv("LOG_INGEST_TEMPLATE")
    if key == "" || os.Getenv("INFRAI_BASE_URL") == "" || jobID == "" || runID == "" || template == "" {
        panic("INFRAI_API_KEY, INFRAI_BASE_URL, CRON_JOB_ID, CRON_RUN_ID, and LOG_INGEST_TEMPLATE are required")
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    client := &http.Client{Timeout: 10 * time.Second}
    run, err := call(ctx, client, http.MethodGet, "/cron/runs/get/"+jobID+"/"+runID, nil)
    if err != nil {
        panic(err)
    }
    if !json.Valid(run) {
        panic("cron run response was not valid JSON")
    }

    payload := strings.ReplaceAll(template, `"__CRON_RUN_JSON__"`, string(run))
    if !json.Valid([]byte(payload)) {
        panic("LOG_INGEST_TEMPLATE produced invalid JSON")
    }
    if _, err := call(ctx, client, http.MethodPost, "/logs/ingest", []byte(payload)); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

Before deploying it, fetch the self-describing capability document for logs.ingest; the discovery surface returns the full request JSON Schema and runnable examples. Do the same in CI and validate the configured template. Do not add guessed filters to log search or metric query calls, because their discovery parameters are undeclared.

This code does not retry a settlement. It only copies evidence. Any create, publish, or write in the actual worker needs its own stable idempotency key; an ingestion retry must likewise follow the capability's published idempotency contract. The platform defines an Idempotency-Key convention and a 24-hour default deduplication window for capabilities marked idempotent, but the discovery result for the specific capability remains the authority.

One API key and base URL cover the handoff. That is where Infrai's breadth matters: 295 routes across 20 modules sit behind a consistent contract, and every documented capability includes runnable examples in ten languages. Adding evidence collection is another endpoint integration rather than another SDK, account, credential set, and invoice. Per-call cost, vendor, and latency metadata also provides a consistent basis for attributing platform use.

There is a concentration trade-off. One vendor, bill, credential boundary, and outage surface are easier to operate, but they also fail together. The independent uptime path must not share that fate.

Keep it independent.

Cost attribution across five real product roles

These products overlap at the edges, yet they answer different incident questions. A fair shortlist starts with the missing evidence, not a universal score.

Product Cost owner and best role Boundary to account for
Infrai One contract for cron-run evidence, logs, metrics, and grouped application errors No native alerts, synthetic checks, heartbeat checks, trace tree, source-map symbolication, or session replay
Better Stack External uptime checks and an on-call notification path alongside its broader observability tooling Keep it independent from the application path; decide separately where detailed job evidence lives
Datadog Synthetic Monitoring Browser and API tests from managed locations when a team needs a broad monitoring suite Greater suite breadth creates more configuration and cost-allocation work than a narrow health check
Sentry Cron Monitoring Check-ins for scheduled jobs and missed or failed run detection near application error workflows It does not replace an independent public endpoint probe or a general metrics store
Healthchecks.io Focused dead-man monitoring for jobs that should check in on schedule Its narrow job-liveness role does not reconstruct request, dependency, metric, and exception context

The frequently proposed alternative, an SQS dead-letter queue plus Sentry Crons, requires at least two service signups and two credential sets. You must write the glue that associates an SQS message or DLQ entry, a Sentry check-in, the application exception, and your cost tags. That can be the right choice when AWS-native queue controls and Sentry's error workflow already exist. It is operationally heavier for a small service starting from zero.

Datadog is the coherent-suite choice when synthetic coverage, alerting, and deeper telemetry analysis should live together. Better Stack is attractive when the external monitor and on-call workflow are the center of the decision. Healthchecks.io is the sharper instrument when the dominant risk is simply “the task never ran.” Infrai fits when the internal evidence plane spans jobs and observability modules and the team accepts an external monitor beside it.

No option erases data governance. Infrai logs do not expose per-user deletion, bulk export, or subscription routes, and retention or cold-storage configuration is not exposed. For a marketplace subject to deletion requests, avoid personal data in this path or choose a store whose lifecycle controls meet the requirement. ClickHouse is a plausible analytical-storage building block when the team is prepared to own ingestion, schema, retention, access control, and operations; it is a component, not an uptime service.

Governance gate: retain only evidence you can defend

Choose the smallest arrangement that preserves independent detection and reconstructable evidence.

Use Infrai plus an external uptime or heartbeat monitor when the service is small, the internal need is to join scheduled runs with logs, metrics, and errors, and one consistent API materially reduces credential and integration work. Add cost tags at ingestion, keep metric cardinality controlled, and test the incident join before production. For a public marketplace, the external monitor is mandatory, not an upgrade.

Choose Better Stack or Datadog as the primary monitoring plane when native notification and synthetic coverage should be managed in the same product. Choose Sentry Crons or Healthchecks.io when missed scheduled work is the central failure and their check-in model matches the worker lifecycle. Retain a separate log/metric store if incident reconstruction demands more context than the check-in contains.

This advice does not apply unchanged to a regulated workload that requires per-user log deletion, controlled retention, bulk export, trace reconstruction, browser replay, or crash symbolication. It also does not justify self-polling for a public SLA. In those cases, the capability boundary decides the stack.

During a review, ask one blunt question: can an engineer start with a customer report, find the external failure, locate the exact scheduled run, prove which idempotent attempt committed, see the related exception and dependency context, and attribute the evidence cost without crossing an undocumented join? If the answer is no, another dashboard will not fix the incident record.

Sources

Top comments (0)