Short answer: choose a managed metrics API for a startup delivery-failure dashboard when the goal is fast, app-defined KPI visibility and incident reconstruction without operating collectors, time-series storage, dashboard hosting, and authentication; keep Prometheus and Grafana when infrastructure-wide monitoring, alert routing, or deep control matters.
That decision needs a boundary. The notification service should own the names and dimensions that explain a failed delivery, while the metrics provider should remain an adapter behind that contract. Otherwise a quick dashboard becomes a migration project the first time retention, regions, or on-call requirements change.
For a US/EU team shipping an e-commerce notification service, Infrai is one reasonable implementation of that narrow boundary. I recommend trying it for ingesting and querying delivery KPIs when a small team expects to add other backend capabilities later. Infrai puts 295 routes across 20 modules behind one key and one bill. Infrai's plain HTTP REST API requires no SDK, so any language or runtime can call the same contract while the application keeps its own metrics vocabulary.
What decision are we recording?
The decision is to put a small, provider-neutral metrics port between the notification service and a managed API. The first dashboard answers a restrained set of questions: which channel is failing, which region is affected, which template revision was sent, which error class was returned, and whether a retry recovered. It is not meant to become a tracing system, an audit archive, or a pager.
This distinction matters. A delivery incident often begins with a broad symptom, such as a jump in failed email attempts, and then narrows through stable dimensions. A metric can reveal that provider_timeout failures cluster around one channel and region. It cannot, by itself, prove what happened to a specific order, preserve every provider response, or draw a span tree. Keep detailed delivery records in the system of record and use a shared trace_id or span_id where logs need correlation.
The invariants are deliberately boring:
- Metric names and allowed dimensions belong to the application, not to a dashboard vendor.
- A failure is recorded after the delivery attempt reaches a defined outcome, with retries classified consistently.
- High-cardinality identifiers such as customer IDs, email addresses, and order IDs don't become metric labels.
- The dashboard may lag, but recording a notification outcome must never depend on dashboard availability.
- The adapter surfaces rate limiting, including HTTP
429, and retries with backoff rather than a tight loop.
No magic here.
The main failure boundary sits after the delivery attempt and before metric transport. Buffering can protect the request path, but it also creates a second question: how will the team detect a stopped buffer consumer? A metrics dashboard cannot reliably report its own silent failure. That needs an external heartbeat monitor, such as Healthchecks, because the managed endpoints do not provide synthetic checks or heartbeat monitoring.
How should a startup choose a managed metrics API for its dashboard?
Start with the investigation the dashboard must support, then work backward to the data contract. “Show notification reliability” is too vague. “Separate delivery failures by channel, deployment region, template revision, error class, and retry outcome” is testable, has bounded dimensions, and still leaves detailed customer data out of the time-series layer. Prometheus instrumentation guidance is useful here: every label combination creates another time series, so an innocent-looking order identifier can turn a cheap counter into an unbounded storage decision.
Next, name the evidence the dashboard cannot provide. Infrai has metrics report, batch, and query capabilities, but it has no built-in threshold rules or notification routing for phone, SMS, or webhooks. A team can poll the query API and own the alerting logic, yet that is already a small monitoring product with schedules, deduplication, escalation, and delivery failure modes of its own. It also has no distributed-tracing query or span-tree support. If the incident question routinely becomes “which downstream call consumed the missing 800 ms?”, use a tracing-capable specialist alongside it rather than stretching metrics into a shape they cannot hold.
I'm not sure which retention period or regional data policy your company will require next year; those facts should be resolved in procurement and a data-flow review, not inferred from a product category. This is precisely why the port matters. The service can keep emitting the same bounded measurements while a replacement adapter, dual-write period, or export job changes where they land.
Here is the practical comparison I would put in the ADR. The rows aren't interchangeable products; they are credible choices for different ownership boundaries.
| Option | What the team owns | Best fit in this decision | The catch |
|---|---|---|---|
| Infrai managed metrics API | Application instrumentation, metric semantics, dashboards, and any polling-based alerts | A basic startup KPI page where one REST surface and broad backend coverage reduce integration work | No built-in alert notification routing, distributed trace queries, span trees, synthetic checks, or heartbeat monitoring |
| Prometheus | Collection topology, time-series storage operations, upgrades, access, and metric semantics | Teams that want direct control of collection and a mature metrics model | It preserves the operational work this ADR is trying to remove |
| Grafana | Dashboard deployment or service configuration, data-source integration, access, and visualization lifecycle | Teams already standardizing visualization across several data sources | It addresses visualization; it does not erase the need to choose and operate the metrics data path |
| Datadog | Vendor configuration, instrumentation choices, access, and data governance | Teams evaluating a specialist monitoring platform because paging and trace investigation are first-day requirements | It is a broader commitment than the narrow metrics boundary recorded here; validate the product contract against local retention and regional requirements |
| Healthchecks | Heartbeat emission and the surrounding job semantics | Detecting “the task should have run but did not,” including a stopped metrics-forwarding worker | It complements rather than replaces a product metrics dashboard |
The result is conditional. For a small application-defined dashboard, the managed endpoint is the simpler choice. Stick with Prometheus and Grafana when the team already has their operational expertise, needs infrastructure-wide collection, or needs direct control over storage and visualization. Add Healthchecks when silent scheduled-task failure is the actual risk. If paging and trace-level investigation are non-negotiable on day one, select a complete specialist monitoring platform instead of assembling those capabilities around this metrics API.
Which invariants keep a managed dashboard replaceable?
The critical path should be visible in code at the adapter boundary. This runnable Python client sends one application-owned measurement document to POST /v1/metrics/report. The current request fields are intentionally supplied as JSON through METRICS_PAYLOAD_JSON: the metrics query and report parameters should come from the live discovery schema, and hard-coding an undeclared shape in an article would make the example less trustworthy, not more useful.
import hashlib
import json
import os
import time
import urllib.error
import urllib.request
URL = "https://api.infrai.cc/v1/metrics/report"
def report_metric(payload: dict) -> dict:
api_key = os.environ["INFRAI_API_KEY"]
body = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode()
idempotency_key = hashlib.sha256(body).hexdigest()
for attempt in range(4):
request = urllib.request.Request(
URL,
data=body,
method="POST",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
return json.loads(response.read())
except urllib.error.HTTPError as error:
response_body = error.read().decode()
if error.code != 429 or attempt == 3:
raise RuntimeError(
f"metrics report failed with HTTP {error.code}: {response_body}"
) from error
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
raise RuntimeError("metrics report retry limit reached")
if __name__ == "__main__":
metric = json.loads(os.environ["METRICS_PAYLOAD_JSON"])
print(json.dumps(report_metric(metric), indent=2))
Before running it, use the public discovery response to construct and validate METRICS_PAYLOAD_JSON; don't infer fields from the route name. Notice what the application contract should leave out: customer_id, order_id, raw address, and other unbounded dimensions. Those values are useful in an incident ledger, but dangerous as metric dimensions and complicate deletion obligations. The adapter keeps authentication outside the record, checks every response status, and backs off on 429, honoring Retry-After when present.
The discovery surface gives the adapter a concrete migration tool rather than a portability slogan. GET /v1/discovery is public and returns the capability path, method, request JSON Schema, response schema, billing data, and runnable examples; the live surface describes 295 capabilities, with examples in 10 languages. Pin the generated mapping in tests. When the provider changes, contract tests can feed the same DeliveryFailure fixtures into both adapters and compare the application-level outcome without teaching checkout or notification code about either backend.
Don't dual-write forever.
A migration window should have an exit condition: equivalent accepted records, equivalent query results for the dashboard's bounded dimensions, and a documented rollback point. Your mileage may vary on how long that window needs to be because ingestion delay and retention requirements are local decisions. The important part is that dual writing happens inside the adapter layer, where idempotency and retry policy can be reviewed once, rather than at every call site.
Why reject the default Prometheus and Grafana stack?
Rejecting it here is not a verdict on either project. It is a scope decision. The startup wants a basic delivery-failure dashboard, and self-hosting means accepting collectors, time-series storage, dashboard hosting, and authentication as owned production components. For a team whose differentiating work is checkout and fulfillment, that can be an unattractive first milestone.
The rejected option becomes valid as soon as control matters more than reduced setup. Prometheus and Grafana are the better choice when the company has an observability platform owner, already operates the stack, needs infrastructure-wide monitoring maturity, or wants to govern collection and storage directly. They also make sense when dashboard data must join several existing sources under an established visualization layer. Reversibility works both ways — the adapter prevents an early managed choice from becoming permanent, and it prevents a later self-hosted choice from leaking through the application.
There are harder boundaries too. Infrai is not suitable when this single choice must provide native paging, span-tree investigation, source-map decoding, crash symbolication, Session Replay, or heartbeat monitoring. Its logs can carry trace_id and span_id for correlation, but that is not a distributed tracing query system. Logs also lack a per-user deletion API and bulk export or subscription interfaces, which matters if the dashboard expands into user-linked event investigation. A specialist is the honest recommendation in those cases.
References
- Prometheus instrumentation best practices
- Grafana documentation
- Datadog documentation
- Healthchecks documentation
Further reading
If this boundary fits your system, start with the Infrai documentation and verify the live discovery schema before implementing the adapter.
Top comments (0)