Short answer: choose an app logging service only after it proves that structured JSON ingestion and search can produce one actionable page for sustained notification delivery failures, with explicit US and EU data handling and bounded impact on the application.
I've been woken by alerts that meant nothing and missed the one that mattered. That history makes the selection rule blunt: don't buy the dashboard that looks best in a demo; buy the evidence path that can tell the responder whether marketplace notifications are failing now, which fault domain is involved, and whether a rollback can change the outcome.
Seven checks expose that path: page semantics, lifecycle modeling, schema fidelity, investigative search, ingestion isolation, regional governance, and recovery verification. Cost matters, but compare it only after replaying representative event volume, retention, and indexed-field choices. A low advertised ingest rate is useless if noisy retries consume the budget or sampling removes the terminal failures needed during an incident.
The postmortem begins with the 03:07 page
Write the page contract before evaluating a service. For this marketplace notification system, a page means that a sustained share of eligible notifications reached a terminal failed state and a person can take an immediate action. An individual provider rejection, the first failed retry, and a burst of malformed recipient data may deserve a warning or a ticket, but none automatically proves broad customer harm. Ask what page fired. If the answer is "error logs increased," the contract is unfinished.
The numerator is terminal failures; the denominator is eligible notification attempts. Keep a minimum-volume guard so a tiny denominator doesn't flap, and require more than one evaluation window so a transient retry doesn't wake someone. The exact threshold cannot be copied from another system. I'm not sure what threshold fits your traffic until production distributions, retry policy, and error-budget decision are available, so treat any starting value as a hypothesis and tune it with observed data.
No denominator, no page.
The alert payload should name the affected customer operation, time window, region, channel, current ratio, and a bounded search that opens near the detection window. It should also identify an owner and an available action: pause a deployment, shift provider traffic, or inspect retry exhaustion. This is a stricter criterion than "supports alerts," and that's intentional. Signal quality beats a large alert catalog at 3 a.m.
A notification attempt is not a notification. Model the lifecycle as accepted, handed to a provider, acknowledged or rejected, retried, and finally delivered or exhausted. A retryable failure is evidence about work in progress; a terminal failure is evidence about the customer outcome. Mixing those states under a single failed label makes both search and paging noisy.
Use one JSON object per event with stable names and stable types. OpenTelemetry's logs data model describes a log record with timestamps, severity, body, attributes, and optional trace or span context. That common shape gives a small team an exit path: application code can emit a consistent record while an exporter or collector handles transport to the selected backend.
For a terminal outcome, useful fields include event_name, outcome, reason_code, channel, region, attempt, deployment_id, a pseudonymous tenant identifier, and trace context. Keep reason_code to a controlled vocabulary. Free-form exception text belongs in the body, not in a grouping key, because variable text can split one underlying condition into many apparent problems; event-grouping systems normalize variable data for the same reason.
Do not log message bodies, email addresses, phone numbers, authorization headers, or provider credentials just because full-text search makes them convenient. Decide which attributes are allowed before export, assign an owner to high-cardinality fields, and define deletion and retention behavior. For US and EU workloads, document where ingestion, indexing, archives, and support access occur. A region label alone cannot settle contractual obligations; legal and security owners have to evaluate the actual data and agreements.
Can a small SaaS app logging service reconstruct notification delivery failure?
Run a tabletop exercise with synthetic data and no customer payloads. At 03:07, a marketplace order has produced a notification request but no message has reached its destination. The responder starts from a pseudonymous correlation key, finds three attempts, identifies a terminal provider_rejected outcome in the EU email channel, groups that reason over the alert window, and checks whether the ratio changed with deployment_id=canary-42. This is a test scenario, not a claimed production incident, and it is far more revealing than a guided dashboard tour.
Make the backend answer the questions in the order a responder asks them: Is delivery harm still occurring? Is it isolated by region or channel? Are retries recovering? Did the distribution change near a deployment? Can one query move from the aggregate page to the affected attempts without copying identifiers across several screens? Search that cannot preserve typed fields or correlate those stages fails the trial even if its charts are attractive.
Use the following acceptance table rather than a feature matrix:
| Check | Evidence to capture | Reject the candidate when |
|---|---|---|
| Page semantics | One sustained customer-outcome alert with an owner | Individual retries create pages |
| Lifecycle model | Retryable and terminal states remain distinct | One failed value mixes both states |
| Schema fidelity | Strings, integers, Booleans, timestamps, and nested attributes remain searchable | Types change or required attributes disappear |
| Incident search | Region, channel, reason, deployment, and correlation searches work over a bounded window | Investigation requires manual joins across views |
| Ingestion isolation | Queue depth, retries, and local drops are observable | Remote logging latency enters the request path |
| Regional governance | Ingestion, storage, access, deletion, and export locations are documented | "EU available" is the entire answer |
| Recovery proof | The page resolves after healthy windows and the runbook records why | Silence is treated as recovery without checking traffic |
This trial also makes "cheap" measurable without turning price into the recommendation. Replay a representative day, vary retention and the set of indexed fields, and compare each candidate's resulting billing model. Your mileage may vary sharply with message size, retry volume, and high-cardinality attributes. More important, verify that any sampling policy retains terminal outcomes and the dimensions required by the page; a smaller bill paired with missing incident evidence is a bad trade.
One page. One owner.
Bound JSON ingestion, then practice recovery and rollback
The application should emit a typed event into a bounded in-process or local queue, while an exporter batches records to a documented HTTPS ingestion endpoint. Don't let an Express request wait indefinitely for remote logging. If audit durability is a business requirement, build that as a separate data path with its own availability contract; diagnostic logging should not become a hidden dependency of notification delivery.
The probe below is intentionally written in Go even though the application runs on Node.js and Express. It tests the logging API independently of the application runtime, sends one synthetic event, applies a three-second timeout, and treats every non-2xx response as an ingestion failure. The endpoint comes from deployment configuration, so the example invents no vendor route.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)
type LogRecord struct {
Timestamp time.Time `json:"timestamp"`
Severity string `json:"severity"`
EventName string `json:"event_name"`
Outcome string `json:"outcome"`
ReasonCode string `json:"reason_code"`
Channel string `json:"channel"`
Region string `json:"region"`
DeploymentID string `json:"deployment_id"`
Attributes map[string]string `json:"attributes"`
}
func send(ctx context.Context, client *http.Client, endpoint, token string, record LogRecord) error {
body, err := json.Marshal(record)
if err != nil {
return fmt.Errorf("encode record: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("build request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("send record: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("ingestion status: %d", resp.StatusCode)
}
return nil
}
func main() {
client := &http.Client{Timeout: 3 * time.Second}
record := LogRecord{
Timestamp: time.Now().UTC(),
Severity: "ERROR",
EventName: "notification.delivery",
Outcome: "failed_terminal",
ReasonCode: "provider_rejected",
Channel: "email",
Region: "eu",
DeploymentID: "canary-42",
Attributes: map[string]string{"synthetic": "true", "attempt": "3"},
}
if err := send(context.Background(), client, os.Getenv("LOG_ENDPOINT"), os.Getenv("LOG_TOKEN"), record); err != nil {
panic(err)
}
}
Production transport needs bounded queues, exponential backoff with jitter, a maximum retry age, payload-size limits, and counters for accepted, retried, and dropped records. Decide whether a full queue drops the oldest or newest records, then test that policy under load; there is no universally correct answer. Keeping older events preserves the beginning of an incident, while keeping newer events improves visibility into current conditions.
The catch is that direct API ingestion is not suitable when many workloads need shared redaction and routing, network policy forbids workload egress, or application processes cannot afford exporter work. Use a local or centralized collector in those cases. A bounded direct exporter fits a small estate with simple routing, but it repeats policy and buffer configuration in each workload. A collector centralizes those concerns — and introduces another component whose capacity and deployment behavior the team must own.
Deploy the logging path as an operational change. Emit one unique synthetic record in each region, retrieve it through the same search used by the runbook, and confirm exact field types. Then send a controlled stream above the chosen minimum volume with a known number of terminal failures. The expected page should fire once, contain the correct region and channel, open the intended search window, and resolve only after healthy windows return.
Then break it.
Disconnect the exporter destination in staging. Application requests should remain within their normal latency objective, queue memory should remain bounded, and a local metric should expose delayed or dropped records. Restore connectivity and verify that the retry burst does not overwhelm the application or ingestion endpoint. Also submit a malformed synthetic event: validation should reject a missing event_name or a nonnumeric attempt before transmission, without copying a customer payload into diagnostic output.
Roll back the logging change if request latency, memory use, local drop rate, or alert ambiguity regresses. Roll back the application release when delivery health regresses and deployment_id provides a credible correlation, while remembering that timing alone does not prove causation. Schema changes should be additive during their transition: update searches and alerts, verify both paths over the longest relevant query window, and then remove the old field.
After each test page, write a miniature postmortem: what signal fired, what query narrowed the fault domain, what evidence was absent, what action followed, and how recovery was proven. A service that passes this rehearsal may look less exciting than one with dozens of dashboards. Good. At 3 a.m., the winning feature is the page that means what it says.
References
- OpenTelemetry, "Logs signal concepts": https://opentelemetry.io/docs/concepts/signals/logs/
- Sentry, "Event grouping and fingerprint mechanics": https://docs.sentry.io/concepts/data-management/event-grouping/
Top comments (0)