A healthtech log archive has to preserve enough evidence to reconstruct an incident without turning every request into permanent noise. Short answer: choose log management by the cost of retaining a tested incident-evidence set in both Europe and the US, not by the cheapest advertised ingestion rate. Keep a small, structured operational stream searchable, move immutable evidence to object storage under an explicit retention policy, and calculate each candidate with your own daily volume, query pattern, regional requirements, and restore drills.
There is no universal cheapest option.
This architecture decision record treats signal quality as the primary axis. It doesn't rank services, because a service that looks inexpensive at ingestion can be the wrong economic choice once duplicated regions, long retention, indexing, retrieval, and staff time enter the model. In healthcare systems, a cheaper log that cannot connect an authorization decision to a request, deployment, and downstream write isn't evidence; it is storage consumption.
What must remain true when healthtech app logs cross Europe and the US?
The first invariant is reconstructability. For a customer incident, an investigator should be able to follow a pseudonymous correlation identifier from the edge request through authorization, application decisions, and the final data-layer outcome. The record needs an event timestamp, severity, service and deployment identity, operation, outcome, region, and a stable correlation field. It should not contain raw clinical content, access tokens, session cookies, or unrestricted request bodies. Redaction belongs before the network boundary — deleting sensitive fields after ingestion leaves copies in buffers, retries, or another region.
The second invariant is controlled placement. “Europe and the US” is not a checkbox; it is a statement about where the searchable copy, archive, buffers, and recovery copies may exist, plus who can retrieve them. The deployment should therefore route an event by policy, reject an unknown residency classification, and record archive writes in a manifest. A vendor's region list alone does not resolve the application's legal or contractual obligations, so counsel and the security owner still have to define the policy.
The third invariant is honest severity. RFC 5424 defines eight severity levels, from Emergency at 0 through Debug at 7. That shared vocabulary is useful, but the application must define what each level means operationally. If every validation failure is Error, the high-signal stream becomes an invoice-funded junk drawer; if an irreversible data write is merely Info, an incident timeline loses its hinge event. A sensible rule is to reserve the searchable operational tier for events that can change an on-call or investigation decision, while routine success events can enter a sampled stream or a less expensive archive.
Noise compounds.
These invariants establish the failure boundaries. A queue can fill. An exporter can receive HTTP 429 and retry. A regional endpoint can become unreachable. An archive write can complete while the search copy is delayed. None of those conditions should make the application silently discard a security-relevant decision, nor should logging failure expose patient data in an emergency fallback file. The fallback policy has to be decided before deployment: bounded local buffering with encryption, backpressure for the narrow class of audit-critical operations, and explicit loss counters for everything else. I'm not sure one backpressure rule can serve both a medication workflow and a marketing-page request; a threat model and a load test should settle that distinction.
How should a startup compare app log management across Europe and the US?
Start with one representative week, then classify bytes rather than multiplying a single daily total by 30. Separate operational events, security evidence, debug bursts, and metrics. OpenTelemetry treats metrics as runtime measurements captured at a moment in time, with metric events aggregated into metric streams; turning every counter sample into a log line wastes the semantics and usually increases noise. Keep counters, rates, and distributions in the metrics path. Keep logs for discrete decisions and context that an incident reconstruction actually needs.
Then price the full path. Use the vendors' current calculators and contracts because published prices, included allowances, and region availability can change. The model should include ingestion after filtering, indexed or searchable retention, archive storage, retrieval or query scanning, cross-region transfer, duplicate copies, and the engineering labor required to operate collectors and restore evidence. Don't assume compressed archive bytes equal billed ingest bytes. Don't assume a “retention” setting proves that a restore is usable, either.
The four names in the original comparison represent different operating boundaries. This table is deliberately not a price leaderboard; without the startup's measured byte and query distributions, one would manufacture precision.
| Candidate | Boundary worth testing | Plausible fit | Cost or evidence question to verify |
|---|---|---|---|
| Amazon CloudWatch Logs | AWS-native collection, storage, query, and export paths | An application already operated primarily in AWS | Which log class, region, query scan, export, and transfer charges apply to the measured workload? |
| Grafana Cloud Logs, based on Loki | Label-indexed log storage queried with LogQL | A team already using Grafana and willing to control label cardinality | Which labels are truly bounded, and how do ingest, retention, query, and regional terms map to the evidence set? |
| Better Stack Logs, previously called Logtail | Hosted collection and search with its own regional and retention options | A small team that values a managed workflow | Do the required ingest source, region, retention, archive, and access controls exist in the chosen plan? |
| SolarWinds Papertrail | Hosted log management centered on common log transport and search workflows | Systems with an established syslog-oriented collection path | How do searchable retention, archive duration, transfer, and restore behavior affect the complete bill? |
Treat each row as a request for evidence. Cloud coupling may reduce collector work but increase migration friction. A label-indexed design can make bounded dimensions efficient, but unbounded values such as patient_id, request_id, or a raw URL do not belong in labels; keep them in the log body and test query latency. A managed interface can reduce operational labor, though its particular regional or retention boundary may not fit the policy. A syslog-oriented path can be easy to connect, while structured-field preservation still needs an end-to-end test. Your mileage may vary — especially when debug bursts are ten times the steady-state byte rate — so replay the same sanitized corpus and the same investigation queries against every serious candidate.
The critical path is policy, buffering, and immutable evidence
The collector is not the architecture. The critical path starts with a typed event, applies redaction and routing locally, sends the narrow searchable record, and writes a canonical copy to immutable object storage with integrity metadata. Search is an index over evidence, not the sole copy of evidence. This distinction matters when a short search-retention window is economical but an incident, contract, or investigation calls for an older record.
The following Python sketch keeps the interface generic. It uses only the standard library, makes no claim about a vendor endpoint, and shows the decisions that deserve tests. Production code would use authenticated transports, an encrypted durable queue, bounded retry with jitter, and an object store whose retention controls match the policy.
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Protocol
class SearchSink(Protocol):
def send(self, event: dict[str, Any]) -> None: ...
class EvidenceArchive(Protocol):
def put(self, key: str, payload: bytes, sha256: str) -> None: ...
@dataclass(frozen=True)
class ResidencyPolicy:
allowed_regions: frozenset[str]
searchable_severities: frozenset[int]
FORBIDDEN_FIELDS = {"access_token", "clinical_note", "cookie", "password"}
def retain_event(
raw: dict[str, Any],
region: str,
policy: ResidencyPolicy,
search: SearchSink,
archive: EvidenceArchive,
) -> str:
if region not in policy.allowed_regions:
raise ValueError("unknown residency classification")
event = {key: value for key, value in raw.items() if key not in FORBIDDEN_FIELDS}
required = {"event_id", "occurred_at", "severity", "service", "outcome"}
missing = required.difference(event)
if missing:
raise ValueError(f"missing evidence fields: {sorted(missing)}")
event["archive_region"] = region
payload = json.dumps(event, sort_keys=True, separators=(",", ":")).encode()
digest = hashlib.sha256(payload).hexdigest()
day = datetime.now(timezone.utc).strftime("%Y-%m-%d")
key = f"evidence/{region}/{day}/{event['event_id']}.json"
archive.put(key=key, payload=payload, sha256=digest)
if int(event["severity"]) in policy.searchable_severities:
search.send(event)
return digest
Notice the order: validate and redact, archive, then populate the search tier. That order favors reconstructability, but it also puts archive availability on the evidence path. For an audit-critical operation, that may be the correct boundary. For ordinary diagnostic logs, enqueueing both writes to a durable regional buffer usually gives a better availability trade-off. The important part is to name the difference and test it, rather than letting one convenience library choose it implicitly.
Consider a hypothetical complaint in which a customer says a care-team permission changed at 14:03 UTC, while the application shows the expected role at 14:08. The useful reconstruction is not a dump of every framework message between those times. It is a chain: the pseudonymous subject and actor identifiers, the authorization policy version evaluated at 14:03, the decision outcome, the request correlation identifier, the application deployment, the intended data mutation, the storage acknowledgement, and any later reconciliation event. A collector receipt timestamp helps distinguish a late event from a late action. A configuration digest connects the decision to the policy that existed then. An archive manifest and object digest show which records were retained without pretending that a hash proves the business event itself was truthful. Debug lines about connection-pool housekeeping might help only after this chain exposes a timing gap, so keeping all of them searchable for months is usually a poor first move. This example also reveals a schema trap: putting the subject or request identifier into a Loki label might make one investigation convenient, but those fields have unbounded cardinality and belong in the body. The same sanitized event corpus should be replayed through every candidate, then queried by correlation identifier and time range after both one hour and the intended archive age. If the restore loses the policy version or changes timestamp precision, the inexpensive retention plan has failed the actual job.
Test the evidence.
Deploy this path with failure injection. Fill the local queue to its byte limit. Return 429 from the search sink. Deny an archive write. Rotate credentials while traffic is present. Confirm that critical operations follow the documented backpressure policy, diagnostic loss increments a visible counter, forbidden fields never appear in payloads or fallback files, and a retry does not create ambiguous duplicate events. Also test clock skew: preserve the producer timestamp and the collector receipt timestamp, because sorting only by a client clock can produce a convincing but false incident sequence.
A restore drill closes the loop. Select a past manifest, retrieve its objects into an isolated account, verify each SHA-256 digest, load a temporary index, run the standard correlation query, and record recovery time plus missing-event count. Do this on a schedule and after retention-policy changes. “We retained it” is otherwise a statement about configuration, not recoverable evidence.
Why reject single-tier searchable retention?
The rejected option is to send every app log into one searchable tier and keep it there for the full evidence period. It is operationally simple: one access model, one query surface, and no rehydration procedure. It also couples evidence duration to index economics, encourages teams to retain noisy debug traffic alongside material decisions, and makes a search-system configuration the only line of defense against accidental expiry.
The catch is that the two-tier choice is not suitable when the team cannot operate archive manifests and restore drills, or when investigators must query the entire retention period interactively with consistently low latency. In that case, stick with a managed single tier, narrow the event schema at the source, and pay for the search window you can actually test. Similarly, a self-managed Loki deployment may be valid for a team with established storage operations and a reason to own that control plane; it is a poor default for a tiny startup whose on-call engineer would also carry the health application.
The decision rule is plain. Choose the candidate that satisfies placement and access policy, survives the loss and restore tests, and produces the lowest modeled total for the retained signal set. If two candidates are close, prefer the one with fewer unowned failure boundaries. Recalculate after a major traffic shift or schema change, because cardinality and debug volume can overturn a spreadsheet without changing a vendor's headline rate.
Sources
- https://opentelemetry.io/docs/concepts/signals/metrics/
- https://datatracker.ietf.org/doc/html/rfc5424
- https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html
- https://grafana.com/docs/loki/latest/get-started/labels/
- https://betterstack.com/docs/logs/getting-started/
- https://documentation.solarwinds.com/en/success_center/papertrail/content/overview.htm











