Short answer: choose a logging API that can reconstruct one pricing decision without retaining every request forever. For a healthtech application rolling out a new pricing rule behind a flag, the valuable unit is a bounded decision record: which rule version answered, what was displayed or charged, and which downstream action followed. A low ingestion bill is irrelevant if the records cannot connect a disputed invoice to the flag evaluation that produced it.
How should small projects choose an affordable logging API?
Start with the reconstruction question, not an API catalog. Six weeks after a rollout, can an engineer answer these questions from retained data? Which pricing rule version was active? Which non-sensitive inputs were used? Did the flag service return a decision, time out, or use a default? Was the result persisted, displayed, or sent to billing? Which release and request trace connect those events?
That is a narrow contract, and it is more useful than verbose application logging. OpenTelemetry treats logs as a signal that can correlate with traces and metrics, while its log data model provides a common structured vocabulary. A timestamp and a human-readable message are weak evidence when retries and concurrent requests share an account.
I would record an immutable pricing-decision event alongside ordinary operational logs. The amount is in minor units so decimal rendering cannot change the audit record. The event carries identifiers, not a patient name, diagnosis, or free-form request payload. A log sink is another place where sensitive information can be copied, searched, exported, and retained.
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class PricingDecision:
rule_version: str
flag_revision: str
decision: str
amount_minor: Optional[int]
currency: str
trace_id: str
request_id: str
outcome: str
def as_log(event: PricingDecision) -> dict:
return {
"event.name": "pricing.decision",
"pricing.rule_version": event.rule_version,
"feature.flag_revision": event.flag_revision,
"pricing.decision": event.decision,
"pricing.amount_minor": event.amount_minor,
"pricing.currency": event.currency,
"trace_id": event.trace_id,
"request_id": event.request_id,
"event.outcome": event.outcome,
}
The cost is mostly retention multiplied by noise
An API bill is shaped by accepted event volume and size, searchable retention, and any export or archive added after ingestion. For a small project, the dominant term is often repeated debug output: request bodies, polling responses, retrying stack traces, and one line per internal helper.
Costs hide in the defaults.
That is why I measure a representative day before selecting an API, then separate routine diagnostics from the decision evidence needed for a pricing incident. A useful calculation includes the serialized size of each event class, daily count, searchable retention, export volume, retries, and the number of rollout days. If the team changes only the provider while keeping noisy payloads and long retention, the bill may barely move; if it removes all detail, the next dispute becomes guesswork. The useful boundary is a small, structured event that can be retained longer than routine access noise without carrying a patient chart.
Here is the failure I want the design to survive. A flag revision changes a recurring-care price for 10% of eligible accounts. A support ticket arrives after an invoice is created, but the visible amount and the current flag state look correct. The investigator needs the request trace, the evaluated revision, the decision reason, the amount in minor units, and the billing write outcome from the same request. If those facts are split between an unstructured application file, a short-lived flag audit page, and a provider dashboard that does not export the original fields, the team cannot distinguish a stale configuration read from a billing retry or a display-only discrepancy. The answer is not to keep every payload permanently. It is to keep the small decision event long enough, preserve the revision that was actually read, and make the failure path observable. That gives the incident review a chain of evidence while keeping clinical context out of the log stream.
Keep it boring.
Measure a representative day. Count events by class, estimate serialized bytes, and multiply by the rollout window. Keep the arithmetic boring. If 98% of records are routine access noise, shrinking a decision record from 1.2 KB to 900 bytes will not move the total as much as expiring routine access records after 7 days instead of 30. The exact break-even point depends on the API's pricing model, so the design should expose the inputs rather than hide them behind “affordable.”
I separate three destinations: a short-lived operational stream for debugging, a compact decision ledger for incident reconstruction, and an export subject to the application's retention policy. They can share a schema while using different sampling and retention rules. A failed rollout should not force the team to retain every successful health check for the same period.
The trade-off is real. If the ledger stores only the final amount, an investigator may not explain eligibility. If it stores every eligibility input, it may retain more sensitive data than the incident requires. My boundary is a stable subject reference, policy version, coarse reason codes, and final decision; raw inputs cross the logging boundary only when a documented control requires them. Redaction after ingestion is a poor substitute for deciding what should be collected.
Can the flag rollout be reconstructed after the fact?
A pricing flag creates two timelines: configuration changes and application decisions. Logging only the latter leaves a gap. Logging only the former cannot show whether requests observed the new revision. Join them with an explicit revision identifier.
A rollout event should identify the flag, revision, actor or automation identity, effective time, target rule, and change reason. A decision event should carry the revision actually evaluated. Emit the decision after evaluation and before a side effect such as creating an invoice. If the billing write fails, that failure references the same trace and request identifiers, but the log must not claim that payment succeeded.
This is where a small logging API earns its place. I look for structured fields, stable timestamps, trace-context propagation, a documented ingestion failure mode, and an export format another system can read. I also test whether the client blocks the request path, queues locally, drops on overload, or retries. Each behavior changes the incident story. A blocking logger can add latency; an asynchronous logger can lose a record during process termination; retries can duplicate it.
The application should treat logging as evidence, not as the transaction itself. The pricing decision and the charge need their own correctness guarantees. A log event can say that a charge attempt was emitted, but it cannot make that attempt durable.
A small selection test exposes dangerous gaps
I would run four tests with synthetic data against any candidate API. Use a fake account identifier and a non-sensitive rule. First, send one decision event with a trace identifier and verify that fields remain separately searchable. Second, send it twice and determine whether duplicates are visible or silently collapsed. Third, force the documented timeout or rejection path and verify that the application receives a bounded error without turning a logging failure into a pricing failure. Fourth, request expiration for the test subject and confirm which copies disappear, including exports.
The third test catches a common trap. Teams select an API because the happy-path call is tiny, then discover that its failure behavior is synchronous and expensive or invisible to operators. In email, SMS, and OTP systems, I learned to distrust designs that treat accepted, delivered, and completed as one event. Pricing needs the same discipline: evaluated, persisted, billed, and displayed are different states.
from typing import Protocol
class LogSink(Protocol):
def emit(self, event: dict) -> None: ...
def record_decision(sink: LogSink, event: dict) -> None:
allowed = {
"event.name", "pricing.rule_version",
"feature.flag_revision", "pricing.decision",
"pricing.amount_minor", "pricing.currency",
"trace_id", "request_id", "event.outcome",
}
sink.emit({k: v for k, v in event.items() if k in allowed})
The adapter is intentionally uninteresting. It rejects accidental payload expansion, leaves authentication outside pricing code, and makes failure behavior unit-testable. I would add a test asserting that a patient identifier cannot pass through this function, even if a caller adds one to the input dictionary.
Make retention a decision before the incident
A useful policy has separate clocks. Keep decision evidence long enough to investigate a pricing dispute or rollout regression under the application's legal and operational requirements. Keep high-volume request diagnostics for a shorter period. Delete or aggregate data when its purpose expires, and document exceptions. Exact durations belong to the organization and jurisdiction; they are not a universal property of a logging API.
For a small project, this policy is also a cost control. Route only the bounded decision event to the longer-lived destination while sampling or expiring routine diagnostics. The price is less forensic detail for an old, ordinary request. That is an intentional loss. If the team cannot name what it gives up, retention was inherited from a default.
The selection criteria are plain: transparent volume accounting, structured and correlated records, bounded client failure behavior, deletion controls, exportability, and enough operational visibility to prove the rollout was observed. Compare those properties with a fixed synthetic workload and a written retention policy. Do not compare a headline rate against an imagined workload.
For this healthtech case, the sound design explains a disputed amount without collecting a patient chart in the log stream. That is a better definition of affordable than the smallest initial invoice.
Further reading
- OpenTelemetry, “Logs signal concepts”: https://opentelemetry.io/docs/concepts/signals/logs/
- OpenTelemetry, “Logs data model”: https://opentelemetry.io/docs/specs/otel/logs/data-model/
- OpenTelemetry, “Context propagation”: https://opentelemetry.io/docs/concepts/context-propagation/
Top comments (0)