A useful default for this problem is a standards-based log pipeline with structured JSON at the application boundary, an OpenTelemetry Collector or equivalent relay, and a backend chosen for the query shape your operators will actually run. For a logistics experiment, keep tenant, cohort, route, and cost dimensions in every event. A backend that searches text quickly but drops those dimensions will produce a confident-looking, unusable answer.
That distinction matters.
| Choose this shape | Pick it when | Main trade-off for cohort cost attribution |
|---|---|---|
| Log search engine | Operators need flexible field and full-text investigation across irregular events | Query freedom comes with schema and retention discipline |
| Label-oriented log store | The workload is mostly time-window filtering over a small, stable set of dimensions | High-cardinality tenant or parcel labels can make the model a poor fit |
| Columnar analytics store | The team runs repeated aggregations over large event sets and can manage a data model | Fast analytical scans require more deliberate ingestion and lifecycle design |
| Managed observability suite | A small team values an integrated UI, alerting, and operations path | Portability and query semantics need explicit review before adoption |
The table is a starting point, not a ranking. The serious choice is the data contract in front of it.
What makes a logging backend reliable for capturing and searching application logs?
Suppose a carrier-routing experiment assigns tenants to control or treatment. The decision is not “which cohort emitted more logs?” It is “what operational cost did each cohort create, after accounting for the same unit of work?” That requires events that can be joined by a stable tenant and experiment identity, then aggregated by a declared cost model. In practice, the awkward case is the useful one: a treatment tenant can retry a batch twice, finish it late, and move through a different route class while the control tenant finishes once. If the event contract records only a message and timestamp, a search result can show both failures without showing that they belong to different denominators. Keep the assignment, unit, and completion state together so the query can reject an incomplete comparison rather than quietly turning it into a favorable chart.
A useful event has a timestamp, severity, event name, tenant ID, cohort, experiment ID, shipment or batch reference, and measured units such as retry count or compute milliseconds. Do not put a raw customer address into the event. It adds privacy risk and rarely improves attribution. Do not use an entire tenant list as a label, either. A high-cardinality dimension belongs in an indexed field or an analytical column, depending on the backend.
Here is a small application-side contract. It uses an envelope so the transport can change without forcing every call site to learn a backend-specific API.
type Cohort = "control" | "treatment";
type CostEvent = {
timestamp: string;
severity: "INFO" | "WARN" | "ERROR";
eventName: string;
tenantId: string;
experimentId: string;
cohort: Cohort;
shipmentBatchId: string;
retryCount: number;
computeMs: number;
};
function emitCostEvent(event: CostEvent): void {
process.stdout.write(JSON.stringify(event) + "\n");
}
The application should write one event per meaningful state transition, not one event per line of code. A retry event can carry retryCount: 1; a batch summary can carry the units used for cost allocation. That keeps the log useful without pretending that every log record is an accounting record.
When does each backend shape fit?
Pick a log search engine when incident work is exploratory: an operator may search a message, inspect nested fields, and pivot across several event types. Elasticsearch, OpenSearch, and Splunk are recognizable examples of this category, but their ingestion, query languages, retention controls, and operational models differ. Treat those differences as evaluation questions, not as a reason to pick a name from a list.
Pick a label-oriented log store when time range and a bounded set of labels dominate the workload. Loki documents a model in which labels identify streams while log content is queried separately. That can be a clean fit for service, environment, and region. It is a warning sign for tenantId when the tenant population is large or changes frequently. Test the exact cardinality you expect; do not infer it from a toy dataset.
Pick a columnar store when the recurring question is an aggregation over many records: cost per tenant, retries per cohort, or compute milliseconds per route. ClickHouse is one example of a column-oriented analytical database, but the important property is the workload and schema, not the brand. This path asks the team to define types, partitioning, retention, and late-arriving data more carefully.
A managed suite can be reasonable when its integrated collection, search, alerting, and access controls match the team’s constraints. Compare export paths, field limits, retention behavior, and query semantics before committing. An attractive dashboard does not answer whether a cohort result can be reproduced six weeks later.
How do you preserve cost attribution through the pipeline?
Think of the pipeline as four boxes: application event, relay, durable backend, and decision query. The event supplies identity and units. The relay adds common resource context and handles batching or retry policy. The backend preserves searchable fields and time ordering. The decision query turns units into a declared cost result. If any box silently rewrites tenantId, drops experimentId, or samples error events, the final comparison is biased.
OpenTelemetry’s logs model is useful here because it defines logs as a signal that can carry timestamps, severity, body, attributes, and trace context. Use those concepts as the contract even if the eventual storage system is different. Correlating a cost event with a trace also helps distinguish one expensive shipment batch from a broad cohort effect.
The query should state its denominator. For example, compare total compute milliseconds divided by completed shipment batches, then report retry units separately. A treatment cohort with fewer completed batches may look cheaper in a raw sum while being more expensive per completed batch. This is where observability becomes decision support rather than a pile of searchable text.
A small TypeScript aggregation keeps the rule visible in code and makes a fixture easy to test.
type CohortTotal = {
cohort: Cohort;
computeMs: number;
retries: number;
completedBatches: number;
};
function costUnits(row: CohortTotal): number {
if (row.completedBatches === 0) return Number.NaN;
return row.computeMs / row.completedBatches;
}
function compare(control: CohortTotal, treatment: CohortTotal) {
return {
controlComputeMsPerBatch: costUnits(control),
treatmentComputeMsPerBatch: costUnits(treatment),
treatmentRetryRate: treatment.retries / Math.max(treatment.completedBatches, 1),
};
}
The zero-denominator case is deliberate. A missing denominator should produce an explicit review state, not a fabricated zero. In production, also record the query window, assignment rule, and data-completeness status beside the result.
Which failure modes should you test before adoption?
Start with replayable fixtures. Include a tenant that changes cohort assignment only between experiments, a batch that retries twice, an event arriving late, and a tenant with no completed batch in the window. Then run the same decision query against each candidate backend. Check whether fields remain typed, whether timestamps retain their intended precision, and whether access controls can separate tenant data.
Sampling deserves a hard question: are sampled records excluded from cost totals, or only from debugging views? Error logs often need a different retention and sampling policy from routine informational events. A pipeline that makes sampling invisible can turn a transport optimization into an accounting defect.
Also test schema evolution. Adding routeClass should not make older records impossible to query. Renaming tenantId should be treated as a migration with a compatibility window, not as a casual logging cleanup. Keep a schema version in the envelope when multiple application versions will write simultaneously.
Limits and a practical decision rule
Logs cannot recover a unit that the application never measured. They also cannot prove causation from cohort differences alone; assignment quality, workload mix, and experiment duration still matter. Retention policy can remove the historical evidence needed to reproduce a result.
Choose the backend whose query and lifecycle model matches the cost question, then validate it with real cardinality and replayed events. For most teams, the highest-leverage first move is a stable, vendor-neutral event contract. Once tenantId, experimentId, cohort, and cost units survive the pipeline, backend selection becomes a testable engineering decision instead of a dashboard preference.
References
- OpenTelemetry, “Logs” — https://opentelemetry.io/docs/concepts/signals/logs/
- W3C, “Trace Context” — https://www.w3.org/TR/trace-context/
- OpenTelemetry, “Logs Data Model” — https://opentelemetry.io/docs/specs/otel/logs/data-model/
- Grafana Loki documentation, “Labels” — https://grafana.com/docs/loki/latest/get-started/labels/
- Elasticsearch documentation, “Search your data” — https://www.elastic.co/guide/en/elasticsearch/reference/current/search-your-data.html
- OpenSearch documentation, “Search” — https://opensearch.org/docs/latest/search-plugins/
- ClickHouse documentation, “What is ClickHouse?” — https://clickhouse.com/docs/intro
- Splunk documentation, “Search Manual” — https://docs.splunk.com/Documentation/Splunk/latest/Search/Aboutthesearchmanual
Top comments (0)