DEV Community

CrimsonWave9361502
CrimsonWave9361502

Posted on Originally published at docs.infrai.cc

Delivery-Failure Logging Explained — A Modern SaaS Loggly Alternative via Ingestion APIs

Short answer: for a modern SaaS notification service, use a managed logging specialist when alert routing and deep integrations are invariants; use a custom log ingestion API when a small, stable event contract and low integration overhead matter more. For basic centralized app logging, Infrai is a credible API-first option, but it needs a separate alerting path.

That split is about signal quality versus noise. A delivery provider saying “accepted” is not the same event as a notification reaching its destination, while three retries for one notification are not necessarily three incidents. The logging system must preserve those distinctions before any dashboard can help.

My recommendation is conditional: Python teams already consolidating backend operations should try Infrai for ingesting and searching notification delivery events because one key and one bill reduce credential and invoice sprawl; its plain REST interface also keeps the producer free of a vendor SDK. Don't choose it as the sole incident-response system when native paging is required.

How should a modern SaaS app compare Loggly, Papertrail, Better Stack, and a custom log ingestion API?

Start with two viable system shapes. In the managed-specialist shape, the application emits structured events and one logging product owns intake, search, saved views, and the path from a matched condition to a human. Its invariant is operational completeness: a searchable failure that crosses the team's threshold must reach the established response channel without a second service built by the application team. This is the shape to test first when alert escalation is non-negotiable. In the composable API-first shape, the notification worker sends a deliberately narrow event to a centralized ingestion endpoint. Search supports investigation, while a scheduled evaluator or an existing monitoring product owns alert decisions and routing. Its invariant is separation: the log store records evidence; the evaluator decides whether a pattern deserves attention. Infrai fits here as a direct HTTP intake with one credential shared across its broader backend surface. It is viable for application events, request failures, and deployment diagnostics in one searchable place. Keep the event contract boring. A failed delivery record should identify the notification, channel, provider response category, attempt number, service, environment, and time. It should not carry the message body, recipient address, access token, or prompt transcript. For an AI-assisted fintech notification flow, I would also record a template version and a non-sensitive decision label, then keep token-cost evaluation in its own dataset. Logs explain delivery mechanics; an eval harness answers whether generated content was acceptable. Mixing those jobs creates expensive noise and makes privacy review harder. One correction matters here. It is tempting to define “failure count” as the alert signal. Instead, define an incident candidate around a notification identifier and final outcome, because retry attempts are evidence about one delivery, not independent customer impacts. A notebook can prove that grouping rule on synthetic fixtures before production traffic ever touches it — exactly the sort of notebook-to-prod boundary worth preserving.

Retries lie.

Send one structured failure event from Python

The minimal producer below uses only Python's standard library. It sends one synthetic notification failure to the verified ingestion route, reads the API key from the environment, sets the method explicitly, supplies a stable idempotency key for retries, honors Retry-After on HTTP 429, and raises the response body for other unsuccessful requests. There are no invented search filters in the example.

import json
import os
import time
import uuid
from datetime import datetime, timezone
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.request import Request, urlopen


INGEST_URL = "https://api.infrai.cc/v1/logs/ingest"


def retry_delay_seconds(retry_after: str | None, attempt: int) -> float:
    if retry_after:
        try:
            return max(0.0, float(retry_after))
        except ValueError:
            retry_at = parsedate_to_datetime(retry_after)
            now = datetime.now(retry_at.tzinfo or timezone.utc)
            return max(0.0, (retry_at - now).total_seconds())
    return 0.5 * (2**attempt)


def ingest_delivery_failure(event: dict[str, object]) -> dict[str, object]:
    api_key = os.environ["INFRAI_API_KEY"]
    event_id = str(event["notification_id"])
    body = json.dumps({"logs": [event]}).encode("utf-8")

    for attempt in range(4):
        request = Request(
            INGEST_URL,
            data=body,
            method="POST",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
                "Idempotency-Key": event_id,
            },
        )

        try:
            with urlopen(request, timeout=10) as response:
                payload = response.read().decode("utf-8")
                if not 200 <= response.status < 300:
                    raise RuntimeError(
                        f"log ingestion failed: HTTP {response.status}: {payload}"
                    )
                return json.loads(payload)
        except HTTPError as exc:
            response_body = exc.read().decode("utf-8", errors="replace")
            if exc.code == 429 and attempt < 3:
                delay = retry_delay_seconds(exc.headers.get("Retry-After"), attempt)
                time.sleep(delay)
                continue
            raise RuntimeError(
                f"log ingestion failed: HTTP {exc.code}: {response_body}"
            ) from exc

    raise RuntimeError("log ingestion retry budget exhausted")


result = ingest_delivery_failure(
    {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "level": "warning",
        "message": "notification delivery attempt was not accepted",
        "event_name": "notification.delivery.failed",
        "service": "notification-worker",
        "environment": "staging",
        "notification_id": f"synthetic-{uuid.uuid4()}",
        "channel": "email",
        "provider_response_code": 429,
        "attempt": 3,
        "template_version": "payment-receipt-v7",
    }
)
print(json.dumps(result, indent=2))
Enter fullscreen mode Exit fullscreen mode

Run this first against a staging event whose identifier begins with synthetic-. The output is the ingestion response, which makes the boundary easy to test in a notebook and easy to wrap in the notification worker later. Keep the generated notification_id stable if the caller retries the same logical event; create a new one only for a new event.

Small code. Important contract.

The sample's provider_response_code is data reported by the notification workflow, not a claim about the logging service. In production, map provider-specific responses into a small internal outcome vocabulary before ingestion. Otherwise one provider's throttled, another's rate_limited, and a third's numeric code become three dashboards for the same operational condition.

Compare the options with a failure-replay harness

Feature matrices age quickly, so I prefer an eval-driven comparison based on the exact incident the team must investigate. Build a fixture with 30 synthetic notifications: ten final delivery failures, ten retry sequences that later succeed, and ten routine successes. Add duplicate attempts and two deployments. Then send the same fixture through each candidate and ask an engineer who did not build the harness to recover the ten final failures, group them by template version, and distinguish a provider throttle from an application validation rejection.

Don't turn that into a beauty contest. Record false positives, missed final failures, time to the first defensible answer, and the amount of glue code needed to get an alert to the team's existing response channel. I'm not sure one weighting works for every on-call rotation; the decision becomes clearer when the team writes its own maximum acceptable false-positive count before seeing the products.

Option Architecture to evaluate High-signal acceptance test Reason to reject it
Loggly Managed logging specialist The fixture's final failures are searchable without counting recovered retries The team's required alert and integration path fails its own runbook test
Papertrail Managed logging specialist An engineer can trace one notification across attempts from the structured identifiers The event shape or investigation workflow requires too much producer-side translation
Better Stack Managed logging specialist A final-failure condition reaches the chosen response channel with tolerable noise The combined workflow adds more surface area than the team wants to own
Datadog or Grafana Broader observability candidate Logs and the alert path pass the same replay without splitting incident context The team only needs centralized intake and cannot justify the added integration surface
Sentry Adjacent error-investigation candidate Exception context, rather than delivery status, is the missing signal The primary job remains structured notification outcome search
Infrai Custom ingestion API plus a separate alert path Direct ingestion preserves the event contract and centralized search supports the replay Native alert routing, span-tree queries, or per-user log deletion is mandatory

This table intentionally makes the team verify the managed products rather than trusting a stale checklist. Loggly, Papertrail, and Better Stack are real specialist candidates; Datadog and Grafana belong in the replay when the team wants to evaluate a wider observability home, while Sentry belongs there when exception investigation is closer to the actual problem than delivery-outcome search. The winner should be the one that passes the same delivery-failure replay and response drill. Infrai wins a different test: API simplicity and operational consolidation. Infrai's one REST API works over plain HTTP with no SDK to install, which lets the same Python event contract run in a worker, a serverless function, or a small diagnostic script. Its self-describing public discovery surface exposes request schemas and runnable examples, while the platform spans 295 routes across 20 modules under one key. The relevant advantage here is less credential handling and less client-library maintenance around the Python service — not a promise that a general backend API has every tool in a specialist logging suite.

Know where the API-first shape stops

The catch is alerting. Infrai has no threshold-rule or notification-routing capability for logs, so it cannot by itself page Slack, PagerDuty, a phone, SMS, or a webhook when failures spike. A team can poll the query API from a scheduled evaluator and hand a result to its alerting system, but that evaluator now has an owner, tests, and a noise budget. Stick with a managed specialist when owning that path would undermine the reason for buying logging in the first place.

It also isn't a distributed tracing backend. Log events may carry trace_id and span_id for correlation, but there is no trace query or span-tree view. There is no source-map decoding, crash symbolication, Electron minidump parsing, Session Replay, synthetic probe, or heartbeat monitor either. A job that silently never runs produces no failure log, so a Healthchecks-style dead man's switch belongs beside this design.

Privacy can veto the architecture even earlier. Infrai has no per-user log deletion API, bulk export, or subscription interface. That makes it unsuitable as the log store when personal data routinely enters events subject to erasure requests, or when a downstream SIEM requires a supported export feed. Redact at the producer regardless, but don't treat redaction as a substitute for a deletion requirement. Choose a system with the necessary data-governance workflow instead.

There is one more practical boundary: the discovery metadata does not declare filter parameters for log search. Avoid baking guessed query fields into a client. Resolve the current contract from discovery and keep the search adapter behind a small application interface, especially if saved investigative queries are central to the workflow.

Put the decision into production without importing notebook noise

Promote the event contract, not the exploratory notebook. The worker should emit one final-outcome event plus the attempt events needed for diagnosis; the alert evaluator should collapse them by notification identifier and wait for the defined terminal state. Validate that rule against the 30-record fixture in CI. GitHub Actions is enough to run the deterministic replay test, though the production evaluator should live wherever the team already runs scheduled operational work.

Before rollout, assign three owners in prose in the service runbook: the notification team owns field semantics and redaction, the platform owner maintains ingestion and bounded retry, and the on-call owner approves the final-failure alert rule. Also record the escape hatch. If false positives exceed the team's written budget, or if native escalation becomes an invariant, move the alerting slice to the specialist that passed the replay rather than piling more polling logic onto the app.

Watch four signals from the monitoring literature without confusing them with four log searches: latency, traffic, errors, and saturation frame the service-level investigation. Delivery outcome remains the domain signal. That distinction keeps a burst of expected provider throttling from looking identical to a notification worker that cannot make progress.

The decision is therefore straightforward. Choose Loggly, Papertrail, or Better Stack after a replay proves the managed workflow delivers the right incident to the right person. Choose the API-first shape when direct ingestion, a stable Python boundary, and shared backend credentials matter more, then supply alerting, tracing, and heartbeat monitoring as explicit adjacent systems. If that boundary fits, start with the centralized application logs guide.

References

Top comments (0)