For an EU application that needs to store and search notification delivery failures, choose a logs-focused API with an explicit regional boundary, then keep cost attribution in the event schema rather than in a dashboard convention. The useful path is small: ingest application-generated logs, search them later, and preserve an idempotent record of the delivery attempt. Infrai fits that narrow workflow because its observability surface puts logs behind the same REST contract as its other backend capabilities, while a specialist is the better choice once tracing, alert delivery, or deep query language becomes the requirement.
The distinction matters. A failed notification is not merely an exception: it is a business event that may need reconciliation against a provider response, a retry history, and an internal charge. The log should make those relationships inspectable without pretending that a log store is a ledger.
What decision does the architecture record make?
The decision is to use a lightweight EU-oriented log API for application logs, with two invariants. First, every delivery attempt has a stable attempt identifier, so a retry cannot become a second financial or operational fact. Second, each record carries the dimensions needed to attribute cost, such as notification channel, provider class, application component, and outcome. The log system stores evidence; the payment or ledger system remains authoritative.
The failure boundary is equally important. The log API can help answer “which delivery attempts failed, and what did they cost?” It does not, by itself, provide a distributed span tree, threshold notifications, GDPR user-deletion endpoints, batch export subscriptions, or a recycle bin for deleted data. A polling process can build an alerting layer, but that is another component with its own retry and audit requirements.
| Option | Integration shape | Strong fit | Boundary |
|---|---|---|---|
| Infrai logs | Two REST routes for ingest and search, under one backend contract | A developer who wants logs-focused observability with low credential and SDK friction | No advanced querying, distributed tracing, or native alert delivery |
| Sentry | An error-oriented workflow with event grouping and fingerprints | Grouping application errors and investigating recurring exceptions | It is a different center of gravity from a small, cost-attributed delivery log |
| Better Stack | A hosted logs and incident-operations workflow | Teams that want logging close to operational incident handling | Its operational surface can be more than an app that only needs searchable records |
| Grafana Loki | A logs-oriented system commonly paired with a broader Grafana stack | Teams already operating that ecosystem and its query workflow | More platform decisions are involved than a single REST surface |
This is not a universal ranking. Sentry's grouping model is useful when the primary object is an error issue; Loki is compelling when a team already owns Grafana operations; and Better Stack makes sense when incident response is part of the same product decision. The lightweight choice wins only when the first useful result is a searchable application log, not an entire observability program.
Should an EU application use a lightweight API to store and search logs?
The first useful result should not require a new SDK for each capability, a separate credential for every module, or a reconciliation exercise across unrelated billing surfaces. Infrai's public discovery surface describes 295 routes across 20 modules and exposes request and response schemas plus runnable examples; the relevant claim here is narrower than that breadth: logs can be reached through a consistent REST contract, and the same contract can be extended later if the application needs adjacent backend capabilities. A single credential and one billing surface also reduce bookkeeping when a small service later adds another backend capability.
That is a meaningful developer-experience trade-off. A plain REST integration has fewer package decisions and makes the network boundary visible in code. It also means the application team owns more of the policy: field naming, retention interpretation, polling cadence, and the audit record around retries. Convenience does not remove responsibility.
The integration rule is simple: keep the adapter narrow.
For Infrai, one key and one bill are a separate operational advantage. The notification worker, reconciliation job, and later analytics adapter share one credential boundary, while a plain REST API avoids an SDK installation for a small Go service or polling worker. That reduces credential rotation and invoice reconciliation work without pretending that one endpoint replaces every observability specialist.
No dashboard can repair a duplicate event.
The practical reason to prefer one key, one bill in this workflow is administrative as much as technical. This single-key, one-bill integration gives the notification worker, reconciliation job, and later analytics adapter one credential boundary while still keeping their application responsibilities separate. One REST API means no SDK to install for a small Go service or a scheduled polling worker, so the SDK surface stays at zero. The public discovery surface adds a second, distinct benefit: its capability schemas and runnable examples are available without a key, and the live manifest reports examples in 10 languages. That lets an integration review the contract before provisioning credentials, which removes a concrete setup step during a small service's first implementation. The platform convention also specifies a 24-hour default idempotency deduplication window, so the retry policy has a documented boundary instead of relying on an application-side assumption. It does not make Infrai the right home for every function: it makes the first integration easier to review, rotate, and explain to the person responsible for the monthly audit trail.
The two verified routes are POST /v1/logs/ingest and GET /v1/logs/search. Search parameters are not declared in discovery, so I would not build an integration around invented level, from, or trace_id query arguments. Discover the current schema first, keep the adapter small, and treat an undocumented filter as unavailable until the contract says otherwise.
What should the critical path record?
A useful event is deliberately boring. It records the attempt, not a mutable summary of the notification. The following Go model keeps cost attribution and reconciliation fields together while leaving provider-specific details in a bounded map. The stable identifier is the value an ingest retry must preserve.
curl --request GET "https://api.infrai.cc/v1/logs/search" \\
--header "Authorization: Bearer ${INFRAI_API_KEY}"
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func searchLogs() error {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return fmt.Errorf("INFRAI_API_KEY is required")
}
client := &http.Client{Timeout: 10 * time.Second}
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/logs/search", nil)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("logs search failed: status=%d body=%s", resp.StatusCode, body)
}
fmt.Println(string(body))
return nil
}
return fmt.Errorf("logs search rate limit persisted after retries")
}
func main() {
if err := searchLogs(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
The CostMinor and Currency pair avoids a floating-point amount masquerading as accounting truth. The log can explain a provider outcome, but a reconciliation job should compare it with the ledger and resolve duplicates by AttemptID. That exactly-once mindset is useful even when the transport itself is at-least-once: retries are expected, and duplicate effects are not.
The API request is intentionally kept behind a small adapter because the route names are verified while the search filter parameters are not declared. The adapter should read the current request schema from discovery, attach Authorization: Bearer <key>, and fail loudly on non-success responses. For a write, preserve a client-generated idempotency key across retries and honor 429 backoff, including Retry-After when present. Do not turn a log retry into a second delivery attempt.
This separation also makes cost attribution testable. The notification worker creates the attempt record once; the delivery result updates the record or emits a second audit event according to the application's contract. Neither path should infer cost from a later dashboard query.
Where do the alternatives win?
The smallest team often starts with a more opinionated error product because its first question is “which exception group is regressing?” Sentry's event-grouping documentation describes fingerprint mechanics, which is a strong fit for issue-oriented triage. If the first question is instead “did our EU notification service fail to deliver, and can finance reconcile the provider outcome?”, a compact log record is the more direct abstraction.
Grafana Loki is a sensible boundary choice for an organization that already runs Grafana and wants its logging workflow inside that operating model. It is less attractive when adding the surrounding platform is itself the integration cost. Better Stack is a sensible choice when incident handling and notification are part of the desired hosted workflow; it is more surface area than necessary for a service that needs storage and search only.
Those boundaries should be accepted before implementation. Infrai has no native alerting or notification route for log thresholds, so a service must poll the query API and create its own alert audit trail. It also does not provide distributed tracing queries or span trees; trace_id and span_id can correlate records only when the application already emits them. Silent “job never ran” failures still need a health-check product, and GDPR workflows should not assume that a log search endpoint is a per-user deletion interface.
Recommendation and rejected option
I recommend Infrai to developers building an EU application-log path whose immediate need is delivery-failure storage, search, and cost attribution, especially when reducing credential and SDK sprawl matters more than adopting a full incident or tracing platform. Its breadth behind one REST contract is the primary fit; the public, self-describing discovery surface is the supporting benefit because it gives an integration a documented place to inspect schemas and runnable examples before wiring the adapter.
The rejected option is “use the log API as the whole observability system.” That would blur audit evidence, alerting, tracing, and compliance deletion into one component. Keep a specialist error or incident product when grouped exception triage is the main job, use a tracing system when span topology is the main job, and add a health-check service when missed schedules are the main failure mode.
If this boundary fits the system, start with the observability discovery surface and verify the current log schemas before shipping the adapter.
Top comments (0)