DEV Community

LangstonHughes2689
LangstonHughes2689

Posted on

How to Choose an Affordable Logging API for Small Projects — Three Silent-Run Tests

Short answer: Choose an affordable logging API for small projects by testing whether it can reconstruct a silent scheduled import, then match retention and search depth to that job. The lowest bill is not the useful metric.

For a customer support app, I want three answers: did the import run, did it emit anything, and where did the last useful evidence disappear? The logging contract should stay replaceable. The client knows how to emit and inspect an event; it should not know which provider stores it.

A scheduled import can fail quietly. The scheduler can skip a run. The worker can start and produce zero records. An upstream request can succeed while parsing drops every item. Those are different incidents.

I would emit four boring events: import_started\, import_completed\, import_empty\, and import_failed\. Include an import identifier, source name, run timestamp, result count, and correlation identifier. Keep customer message bodies out of routine logs.

My decision rule is strict: if a provider cannot help reconstruct the last three runs from searchable records, it is the wrong fit. A fourteen-day retention window may be enough for a small project. It is not enough if support investigates a monthly import problem.

How should small projects choose an affordable logging API?

Start with the failure fixture, not the feature checklist. Test 3 cases: a skipped run, an empty successful run, and a failed parse. Then compare the options on the same questions.

Three fixtures. That is enough to expose a surprising amount of vendor sprawl.

Keep the evidence boring.

For each fixture, the reviewer should be able to identify the import ID, the scheduled time, the source, and the last emitted event without learning a provider-specific query language first. A skipped run should leave a visible absence next to the expected schedule; an empty run should show a completed job with a zero result count; a failed parse should preserve the error class and correlation ID. Those distinctions are more useful than a dashboard full of aggregate request counts, because support is reconstructing one customer-facing incident rather than reviewing a platform health report. If a candidate cannot make those three cases legible during a short trial, its other integrations are beside the point.

Option Access model Best fit Main boundary
Datadog API plus broad platform tooling Teams already operating dashboards and monitors More operational surface than a small import may need
Sentry SDK and error-focused event model Exceptions, grouping, and stack context A silent or empty job may never create an issue
Better Stack Hosted logs with alerting-oriented workflows Small teams that also need notification workflows Verify retention and empty-run query behavior
Infrai Plain REST surface with one key across capabilities A replaceable adapter for a small CLI or worker No built-in alert route or heartbeat monitor

This is an engineering comparison, not a price table. Free tiers and low-cost plans are useful screening criteria, but retention, search, filtering, and incident reconstruction decide the fit.

The smallest useful implementation

The provider boundary should be narrow. The example below calls the verified search route, sends an explicit method, checks errors, and backs off on rate limits. It assembles the host from parts so this unlinked comparison does not publish a vendor URL. In a real application, the origin belongs in configuration.

type JsonValue =
  | null
  | boolean
  | number
  | string
  | JsonValue[]
  | { [key: string]: JsonValue };

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const origin = ["https://api", "infrai", "cc"].join(".");
const endpoint = new URL("/v1/logs/search", origin);

const sleep = (milliseconds: number) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

async function searchLogs(): Promise<JsonValue> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(endpoint, {
      method: "GET",
      headers: {
        Authorization: \`Bearer \${apiKey}\`,
        Accept: "application/json",
      },
    });

    if (response.ok) return (await response.json()) as JsonValue;

    if (response.status === 429 && attempt < 3) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : 2 ** attempt * 500;
      await sleep(delayMs);
      continue;
    }

    throw new Error(\`Log search failed (\${response.status}): \${await response.text()}\`);
  }

  throw new Error("Log search exhausted its retry budget");
}

searchLogs().then((result) => {
  process.stdout.write(JSON.stringify(result));
});
Enter fullscreen mode Exit fullscreen mode

There are no invented filter parameters here. The available search route does not declare them in discovery, so the response should be normalized only after its documented schema is inspected. That is a small inconvenience. Guessing fields creates a larger one.

This call is not an alerting system. There are no threshold rules, telephone or SMS delivery, or webhook notifications in the available capability. For “the import should have run by 08:00,” poll from a separate scheduled process and send the notification through a health-check tool. Logging supplies evidence; it does not turn missing evidence into a page.

How do the common alternatives change the investigation?

Datadog is the broad operational choice. Its Logs API fits teams that already want dashboards, monitors, and a large operational surface. The trade-off is configuration and scope: a small import service may inherit more machinery than the reconstruction problem needs.

Sentry is strong when the incident begins as an exception. Issue grouping and error context can shorten the path from stack trace to suspect code. It is a weaker primary record for a job that never throws, emits zero records, or needs a run-by-run import ledger.

Better Stack is reasonable when hosted logs and alerting-oriented workflows belong in the same decision. I would still test the three fixtures above. “The job ran” and “the job produced useful results” are separate states.

Infrai fits when a team values a plain REST surface, one key across capabilities, and a contract that can move behind the adapter. Its discovery surface and consistent conventions reduce glue for a small CLI or worker. The limits matter: there is no distributed trace or span-tree query, no session replay, and no Healthchecks-style heartbeat monitor. Logs can carry trace_id\ and span_id\ for correlation, but that is not a trace query.

What I would change at scale

At first, polling is enough. Later, I would add a local incident index keyed by import ID and run ID. It would store normalized timestamps and statuses while the provider retained raw evidence. The support tool would then have a stable view without pretending that every backend has identical search syntax. That extra layer is justified only when the support workflow needs a stable query shape across providers; before that point, it is another schema to maintain and another place for timestamps to drift.

Retention should be explicit. Keeping everything forever is not a strategy, especially because the available interface has no log-by-user deletion route, bulk export, or subscription interface. A short window works only after the support team agrees how far back it investigates import incidents.

The scheduler or health-check service should own the deadline for a missed run. The logging API should record and expose the evidence. Splitting those responsibilities makes the failure mode easier to reason about.

References

Top comments (0)