Skip to content
Finance Breakfast San Francisco — Working AI for Your Finance Stack — May 28, 2026Request Invite

Data Streams · OpenTelemetry

Your app telemetry, next to your business data.

One OTLP endpoint. Three tables — logs, metrics, traces. Query them with the rest of your warehouse in SQL.

user@app — bash — opentelemetry

Why this matters

Telemetry is only half the question. The other half is revenue.

Datadog and Honeycomb tell you what went wrong. Joining a trace with an order tells you what it cost — and that join only works where the business data already lives.

Correlate failures with revenue

Join `traces` with `orders` by trace_id. Find out which 500 errors cost you a conversion — and which ones nobody noticed.

Measure LLM cost per outcome

Token usage in `metrics`, conversions in your business tables. One SQL query answers "what did this AI feature actually cost us per converted user?"

Quantify deploy impact

Tag every span with `deployment_environment`. Pinpoint the deploy that dropped checkout conversion 4%, not the one a Slack thread blamed.

Skip the export pipeline

No Datadog → S3 → warehouse two-day ETL. Telemetry lands in Storage 15 seconds after it leaves your app.

How it fits

A standard pipe.
A SQL-shaped destination.

Nothing custom on your side — vanilla OTel SDKs and OTLP. On our side, the same Data Streams engine that handles 140K events/sec, shaped for OpenTelemetry payloads.

Standards in, SQL out

Instrumentation

OTel SDK

Standard OpenTelemetry SDKs for Python, Node, Go, Java, .NET, Rust.

Standard protocol

OTLP endpoint

OTLP/HTTP over `http/protobuf` — the same exporter you point at Datadog.

Real-time ingest

Data Streams

Same engine as Keboola's HTTP streams — sub-15s to Storage.

Joinable in SQL

Storage tables

Three auto-created tables — logs, metrics, traces — with typed columns.

Keboola Storage

logs · metrics · traces

Three SQL-queryable tables, pre-extracted columns, and the same trace_id that travels with your spans — ready to JOIN with the orders, sessions, and users you already store.

Standards-based

Vanilla OTLP — no custom SDK, no lock-in

Pre-extracted

service, severity, trace_id, deployment_environment as columns

Joinable in SQL

trace_id is the foreign key between telemetry and business data

Three signals

Pre-shaped tables. No unwrapping JSON.

Each signal lands in its own table with the columns you'll actually query promoted to top-level — no json_extract gymnastics.

Signals

Logs

Logs

Application events, errors, warnings, debug. Severity, service, and trace_id are top-level columns so you can SELECT and JOIN without unwrapping JSON.

Pre-extracted columns

  • timestamp
  • severity
  • service
  • trace_id
  • body
  • deployment_environment
  • host_name
  • k8s_pod_name
logs.sql
-- Error rate by service, last hour
SELECT
service,
COUNT(*) AS errors
FROM logs
WHERE severity = 'ERROR'
AND timestamp > NOW() - INTERVAL '1h'
GROUP BY service
ORDER BY errors DESC;

Setup

Two env vars. That's the install.

Point your existing OTel SDK or Collector at the endpoint we generate. No custom packages, no auth dance.

01

Create a Data Stream

02

Copy the endpoint URL

03

Set the env vars

.env
export OTEL_EXPORTER_OTLP_ENDPOINT="<your-stream-endpoint>"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_SERVICE_NAME="checkout"
# pip install opentelemetry-exporter-otlp-proto-http

In practice

Three queries you couldn't write before.

Each runs against the three signal tables joined with your business data — no custom export, no warehouse round-trip.

LLM economics

What did this AI feature cost per conversion?

Join token-usage `metrics` with conversion events. Get cost-per-converted-user, not cost-per-call.

llm_cost_per_conversion.sql
WITH tokens AS (
SELECT
attributes:user_id::TEXT AS user_id,
SUM(value) AS total
FROM metrics
WHERE metric_name = 'llm.tokens.total'
GROUP BY 1
)
SELECT
COUNT(c.user_id) AS converted,
AVG(t.total) * 0.000002 AS cost_per_conv_usd
FROM conversions c
JOIN tokens t ON t.user_id = c.user_id
WHERE c.converted_at > NOW() - INTERVAL '7d';
Deploy impact

Did this morning's deploy drop conversion?

Spans carry `deployment_environment` and a build SHA. Bucket conversion by deploy and spot the regression.

deploy_impact.sql
SELECT
t.attributes:build_sha::TEXT AS build,
COUNT(DISTINCT o.order_id) AS orders,
AVG(o.amount) AS avg_order,
SUM(CASE WHEN o.status = 'failed'
THEN 1 ELSE 0 END) AS failures
FROM traces t
JOIN orders o ON o.trace_id = t.trace_id
WHERE t.operation = 'checkout.submit'
AND t.start_time > NOW() - INTERVAL '24h'
GROUP BY 1
ORDER BY orders DESC;
Cohort-aware errors

Which errors hurt our highest-LTV customers?

Severity-filtered logs joined with user LTV. Stop optimizing for noise — fix the errors your best customers hit.

errors_by_ltv.sql
SELECT
l.service,
l.body AS error,
COUNT(DISTINCT u.user_id) AS users,
SUM(u.lifetime_value) AS at_risk_revenue
FROM logs l
JOIN sessions s ON s.trace_id = l.trace_id
JOIN users u ON u.user_id = s.user_id
WHERE l.severity = 'ERROR'
AND l.timestamp > NOW() - INTERVAL '7d'
GROUP BY 1, 2
ORDER BY at_risk_revenue DESC
LIMIT 20;

And your APM?

Keep it. This runs in parallel.

We're not asking your SREs to switch tools. Use the OpenTelemetry Collector to fan out — same exporter, multiple destinations. Datadog keeps the dashboards; Keboola gets the joins.

Not a Datadog replacement

Keep your dashboards and alerts where they already work. This pipe runs in parallel — a copy of telemetry for the data team, not a replacement for SRE tooling.

OTLP standard, no lock-in

Configure the OpenTelemetry Collector once. Fan out to Datadog, Honeycomb, Grafana, and Keboola — same exporter, multiple destinations.

Lives where business data lives

Orders, sessions, conversions, LTV — already in your Keboola Storage. That's the join target. APMs can't get there; OTel into Keboola can.

Frequently Asked Questions

All three — logs, metrics, and traces — land in three pre-shaped tables in Keboola Storage. Each table has the most useful resource attributes (service, severity, trace_id, host_name, k8s_pod_name, deployment_environment) promoted to top-level columns. The raw OTLP payload is also available as an optional column for advanced querying.
OTLP over HTTP with the http/protobuf protocol. Any official OpenTelemetry SDK works — we've verified Python, Node.js, Go, Java, .NET, and Rust. The OpenTelemetry Collector is supported via its otlphttp exporter, so you can keep your existing collector and just add Keboola as one more destination.
Records typically appear in Storage within about 15 seconds of ingest. Like other Data Streams, the import conditions — time interval, batch size, record count — are configurable if you need to tune for cost vs. freshness.
Yes — and that's the recommended setup. The OpenTelemetry Collector lets you fan out to multiple destinations from a single pipeline. Keep your APM exactly as it is for dashboards and on-call, and add Keboola as a parallel exporter so the data team can do SQL joins with business data.
A regular HTTP stream gives you one endpoint and one destination table — you define the schema. The OpenTelemetry source type is opinionated: it understands OTLP, auto-creates three signal tables, and pre-extracts the columns you'll actually use. Same engine underneath, less work for OTel-shaped data.
Two stages — network ingest at the OTLP endpoint is sub-second, and the Data Streams import to your Storage table runs on the configured trigger (default: ~15 seconds). For analytical use cases this is comfortably real-time; for sub-second debugging keep your APM.
Same pricing model as Data Streams — per endpoint per month, with a PAYG option. Storage and warehouse usage for the telemetry tables follow your existing Keboola plan. There's no per-event charge.
Data Streams must be enabled on your project — contact support if you're not sure. Once it's on, the OpenTelemetry source is one of the source types when you create a new Data Stream.

Ready to put telemetry next to the data that matters?

Spin up a Data Stream with the OpenTelemetry source, paste the endpoint into your SDK, and the first JOIN works inside a coffee break.

Read the docs