The simplest event-logging API is a single structured event endpoint with a stable event name, correlation ID, timestamp, and outcome. For scheduled media imports, that small boundary gives an operator enough evidence to reconstruct a missed run without advanced infrastructure setup. It is also a small surface for an app to integrate.
Short answer: send one JSON event when an import starts, changes state, and finishes. Keep the payload boring. Put searchable facts in fields, keep the human message short, and make the sender retry-safe. Add metrics later for questions that need aggregation.
The useful mental model is before and after. Before, an import is a timer and a vague error line: “job failed.” After, it is a short timeline: started, fetched, published, or failed, all tied to the same runId. That is the difference between guessing and reconstructing.
What should the smallest event logging API contain?
Start with five required fields: event, occurredAt, runId, source, and outcome. Add itemCount when the event describes a batch. Those fields answer the first incident questions: which run was this, which source did it touch, when did it happen, and did it produce anything?
A generic event contract can stay this small:
type ImportEvent = {
event: "import.started" | "import.fetched" | "import.finished" | "import.failed";
occurredAt: string;
runId: string;
source: string;
outcome: "started" | "success" | "failure";
itemCount?: number;
errorCode?: string;
};
async function recordImportEvent(event: ImportEvent): Promise<void> {
const response = await fetch("https://events.example.invalid", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(event),
});
if (!response.ok) {
throw new Error(`event sink returned ${response.status}`);
}
}
The endpoint is deliberately generic. A team can place it behind an existing internal service, a hosted collector, or a small process that writes to its chosen store. The application only owns the contract. It does not need to know the storage query language. That keeps the initial setup legible while leaving room to change the destination.
Do not put the full feed payload in every event. That increases exposure, makes retries expensive, and buries the signal that operators need. Keep identifiers, counts, and bounded error codes; link to protected job details through a reference that follows the organization’s access rules.
Missed runs are silent.
How does this expose a stopped scheduled import?
The alert should not depend on a log line arriving at the exact moment a timer was expected to fire. A healthy run produces a finished event with a success outcome. A missing run produces no event at all, so the alerting layer compares the latest successful completion with the expected schedule. This is a temporal rule, not a text search.
For example, an hourly catalog import might use this decision rule: alert when the current time is more than 75 minutes after the last successful import.finished event. The 15-minute allowance is an explicit operating choice for queue delay and upstream slowness. It should be documented beside the rule, then tested against delayed and overlapping runs.
The event sequence is easy to scan:
const events: ImportEvent[] = [
{
event: "import.started",
occurredAt: "2026-10-30T08:00:03Z",
runId: "catalog-20261030T0800Z",
source: "partner-catalog",
outcome: "started",
},
{
event: "import.finished",
occurredAt: "2026-10-30T08:17:41Z",
runId: "catalog-20261030T0800Z",
source: "partner-catalog",
outcome: "success",
itemCount: 1842,
},
];
A count of zero is still meaningful. It says the run completed and found no publishable items. That must remain distinct from a missing completion event, which says the system has no evidence that the run finished. Conflating those states creates noisy alerts and slow investigations. In practice, this is the boundary worth protecting: a newsroom can decide what zero means, but it cannot interpret an absent record without knowing whether the scheduler, importer, or event sender stopped first.
Which trade-offs matter before adding infrastructure?
The first trade-off is delivery failure. If the event call blocks the import, the logging path becomes a new dependency in the business path. If it is fully asynchronous, a process crash can erase the final event. A practical boundary is to enqueue a small event locally, retry a bounded number of times, and let the import’s success state remain independent of the sink. The retry policy needs a duplicate key such as runId + event; receivers can then deduplicate repeated submissions.
The second trade-off is cardinality. runId is excellent for incident reconstruction but a poor label in a time-series metric. Keep high-cardinality identifiers in logs or event records. Use low-cardinality dimensions such as source and outcome for aggregate metrics. Prometheus documents this distinction through its metric naming and label guidance.
Here is the practical choice before an app adopts advanced infrastructure:
| Approach | Integration shape | Setup effort | Best fit | Main limit |
|---|---|---|---|---|
| Structured event endpoint | REST or thin client | Low | One scheduled importer | Requires a destination and retention rule |
| Local durable queue | Producer plus worker | Medium | Intermittent connectivity | Adds disk and replay operations |
| Full telemetry pipeline | SDK or collector | Higher | Several services and signals | More configuration than one job needs |
The table is a decision aid, not a product ranking. Start with the smallest boundary that preserves the incident evidence, then expand when the workflow proves it needs more.
The third is retention. Events used to reconstruct a missed import need enough history to cover the team’s investigation window. Choose that window from the schedule and incident process, not from a storage default. A seven-day view may be adequate for a daily feed; a monthly editorial archive may require longer access to run metadata.
How should the API be tested and operated?
Test the contract at the boundary. Verify that timestamps are valid ISO 8601 strings, outcomes are constrained, itemCount cannot be negative, and oversized or sensitive payload fields are rejected. Then test the workflow: a successful run emits a completion event, an upstream timeout emits a failure event, and a process restart does not turn one run into two successful completions.
Keep one fixture with a delayed run, one with zero items, and one with a repeated event. Those three cases catch more operational confusion than a large collection of happy-path payloads. Also test the alert against clock skew and overlapping runs; the latest successful completion must be associated with the right source, not merely the latest event globally.
An API is simple when its boundary is small and its failure behavior is explicit. It is not simple when every producer invents fields, sends raw payloads, and asks a person to infer the schedule from prose.
The likely objection is that a metric would be enough. A metric such as imports_completed_total is useful for rates and aggregate health, but it cannot preserve the exact run identity and bounded error context needed to replay one incident. Use events for reconstruction, metrics for trends, and alerts for decisions.
The other objection is that a hosted collector or full telemetry platform might be faster to adopt. It can be, especially when a team already has a destination and retention policy. The decision should follow the boundary: choose the smallest interface that preserves run identity, completion evidence, and an auditable retry path. Storage can change behind it.
Top comments (0)