Telemetry
Browse docs
GuidesUpdated July 28, 2026Reviewed by the Telemetry editorial and product teams5 min read

Use this doc with your coding agent

Copy a Telemetry prompt and ask Claude Code, Codex, or Cursor to instrument the workflow covered here.

On this page
  1. First write down the semantic contract
  2. A shared example event
  3. Concept map
  4. Example 1: filter recent errors
  5. LogQL
  6. KQL
  7. SPL
  8. SQL
  9. Example 2: calculate error rate by route
  10. Example 3: build a time chart
  11. Example 4: replace a pipeline with common table expressions
  12. Parsing is the migration risk
  13. Vendor-specific features that need redesign
  14. Validation and cutover

Migrate LogQL, KQL, and SPL Queries to SQL

Moving an operational query to SQL is a data-model migration, not a find-and-replace exercise. LogQL begins with log streams and label selectors, Kusto Query Language uses a tabular pipeline, and Splunk SPL transforms search results through commands. SQL begins with relations and makes selection, grouping, joins, and projection explicit.

The safest migration preserves the question and result contract before changing syntax.

First write down the semantic contract

For every query, record:

  • the decision it supports;
  • the source and time window;
  • the row or event grain;
  • parsed fields and their types;
  • grouping dimensions;
  • numerator and denominator;
  • null, duplicate, retry, and late-arrival rules;
  • expected columns and sort order.

Run the old and new queries over the same bounded interval. Compare totals first, then group-level results, then representative raw rows. A result that looks visually similar can still use a different denominator.

A shared example event

The translations below assume one typed row per completed API request:

{
  "timestamp": "2026-07-28T16:04:00Z",
  "event_name": "api_request_completed",
  "service": "checkout-api",
  "route": "/v1/orders/:id",
  "status_code": 503,
  "latency_ms": 842,
  "request_id": "req_01J...",
  "release": "2026.07.28"
}

If the old system stores only a message such as "GET /v1/orders/123 returned 503 in 842ms", first add a parser or change instrumentation. SQL cannot recover a stable route template or reliable numeric type that was never recorded.

Concept map

Intent LogQL KQL SPL SQL
Choose source stream selector table expression index and source search FROM table
Filter rows line or label filter where search or where WHERE
Parse fields parser expression parse, extend rex, spath, eval preferably typed columns before query
Select columns line format project fields or table SELECT
Aggregate metric query summarize stats or timechart aggregates plus GROUP BY
Pipeline ` ` stages ` ` operators
Time bucket range vector bin() timechart span= date_trunc()

This table maps intent, not exact equivalence. For example, a Loki label participates in stream indexing and cardinality constraints; a SQL column does not automatically have the same storage behavior.

Example 1: filter recent errors

LogQL

{service="checkout-api"} | json | status_code >= 500

KQL

ApiRequestCompleted
| where Timestamp > ago(1h)
| where Service == "checkout-api" and StatusCode >= 500
| project Timestamp, Route, StatusCode, LatencyMs, RequestId
| order by Timestamp desc

SPL

index=production service=checkout-api status_code>=500 earliest=-1h
| table _time route status_code latency_ms request_id
| sort - _time

SQL

SELECT
  timestamp_utc,
  route,
  status_code,
  latency_ms,
  request_id
FROM api_request_completed
WHERE timestamp_utc >= now() - INTERVAL '1 hour'
  AND service = 'checkout-api'
  AND status_code >= 500
ORDER BY timestamp_utc DESC
LIMIT 200;

The explicit limit protects an exploratory query from returning an unbounded incident window. It is not part of an aggregate report.

Example 2: calculate error rate by route

Pipeline languages often make the numerator easy to see while the denominator is implied by an earlier stage. Keep both in the SQL result:

SELECT
  route,
  COUNT(*) AS requests,
  SUM(CASE WHEN status_code >= 500 THEN 1 ELSE 0 END) AS errors,
  ROUND(
    100.0 * SUM(CASE WHEN status_code >= 500 THEN 1 ELSE 0 END)
      / NULLIF(COUNT(*), 0),
    2
  ) AS error_rate_pct
FROM api_request_completed
WHERE timestamp_utc >= now() - INTERVAL '24 hours'
  AND service = 'checkout-api'
GROUP BY route
ORDER BY error_rate_pct DESC, requests DESC;

Do not translate a LogQL count_over_time into COUNT(*) until you confirm that one parsed log entry corresponds to one SQL row and that retries or multi-line messages do not change the grain.

Example 3: build a time chart

KQL summarize ... by bin(Timestamp, 5m), SPL timechart span=5m, and LogQL range aggregations all express a time series. In DataFusion SQL, a portable first step is an hour bucket:

SELECT
  date_trunc('hour', timestamp_utc) AS hour,
  release,
  COUNT(*) AS requests,
  approx_percentile_cont(latency_ms, 0.95) AS p95_latency_ms
FROM api_request_completed
WHERE timestamp_utc >= now() - INTERVAL '7 days'
GROUP BY date_trunc('hour', timestamp_utc), release
ORDER BY hour ASC, release ASC;

Choose a bucket supported by the engine and appropriate for the event volume. Exclude or annotate the newest bucket when it is incomplete.

Example 4: replace a pipeline with common table expressions

Pipelines are readable because each stage transforms the prior table. SQL common table expressions can preserve that shape:

WITH recent_requests AS (
  SELECT *
  FROM api_request_completed
  WHERE timestamp_utc >= now() - INTERVAL '24 hours'
    AND service = 'checkout-api'
),
route_summary AS (
  SELECT
    route,
    COUNT(*) AS requests,
    SUM(CASE WHEN status_code >= 500 THEN 1 ELSE 0 END) AS errors
  FROM recent_requests
  GROUP BY route
)
SELECT
  route,
  requests,
  errors,
  ROUND(100.0 * errors / NULLIF(requests, 0), 2) AS error_rate_pct
FROM route_summary
WHERE requests >= 100
ORDER BY error_rate_pct DESC;

Name stages for their meaning rather than step1 or filtered. The resulting query remains easy to review and test.

Parsing is the migration risk

Existing queries may depend on regular expressions, JSON extraction, automatic field discovery, or vendor-specific search-time parsing. Inventory each derived field:

Derived field New event field Type Fallback during cutover
request method method string category parse old message
route template route string category map raw path to template
response code status_code integer cast parsed value
duration latency_ms integer normalize seconds or microseconds
release release string category enrich from deployment metadata

Run dual collection until the typed field is present and correct across producers. Do not remove the old parser because a single happy-path service matched.

Vendor-specific features that need redesign

Some constructs should not be forced into generic SQL:

  • LogQL stream labels and unwrap operations combine storage selection with parsing.
  • KQL has rich dynamic-value, time-series, and anomaly functions.
  • SPL has search-time knowledge objects, transactions, and command-specific behavior.
  • Each system applies different default timezones, null semantics, limits, and approximate aggregation algorithms.

Preserve a vendor query when it is the best tool for an incident or data type. The goal is a dependable SQL event model for shared questions, not language purity.

Refer to the current first-party references for LogQL query examples, Kusto query operators, and the Splunk Search Reference.

Validation and cutover

For each migrated query:

  1. freeze a representative interval;
  2. compare source row counts;
  3. compare distinct operation identifiers;
  4. compare null and parse-failure counts;
  5. compare every output group, not only the grand total;
  6. explain accepted differences;
  7. run both dashboards through at least one normal traffic cycle;
  8. keep a rollback link to the old query until users accept the new result.

Use Troubleshooting SQL Queries for engine and type problems, Migrate Logs to Structured Events for instrumentation rollout, and the SQL cookbook for complete contracts and visual outputs.