mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-30 14:56:15 +00:00
HIP-0132 unified o11y_logs, o11y_traces and o11y_metrics into one `event`
database and dropped them. The commit that restored the reads repointed
three files and said so; it was thirty. The rest went on naming databases
that no longer exist, so trace funnels, the Kafka and queue panels, the
v3/v4 logs and traces builders and the PromQL adapter each answered
Code: 81 UNKNOWN_DATABASE, and the golden tests agreed with them because
they pinned the same dead SQL.
pkg/telemetryplane is the one address for the name, imports nothing, and
is reachable from every depth that needs it. Its test walks the tree and
fails the build on a literal that names a dropped database, tests
included — a golden encoding broken SQL is a second vote for the bug, not
coverage.
Two service views turned a failed read into an offer to instrument an
already-instrumented application. A failure now shows the failure, and
onboarding is offered on a successful empty read.
Cherry-picked from salvage/blue-wt-o11y-readers 83ecbec03c.
504 lines
18 KiB
Go
504 lines
18 KiB
Go
package o11yapiserver
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"github.com/hanzoai/o11y/pkg/http/handler"
|
|
"github.com/hanzoai/o11y/pkg/http/routing"
|
|
"github.com/hanzoai/o11y/pkg/telemetrylogs"
|
|
"github.com/hanzoai/o11y/pkg/telemetryplane"
|
|
"github.com/hanzoai/o11y/pkg/telemetrytraces"
|
|
"github.com/hanzoai/o11y/pkg/types"
|
|
qbtypes "github.com/hanzoai/o11y/pkg/types/querybuildertypes/querybuildertypesv5"
|
|
)
|
|
|
|
// The Datastore-SQL request examples below are the ONLY documentation a customer
|
|
// has for writing raw SQL against the plane, and they are copied verbatim into
|
|
// real queries. They named o11y_traces and o11y_logs — databases HIP-0132 dropped
|
|
// — so every customer who followed the docs wrote a query that could only answer
|
|
// UNKNOWN_DATABASE. An example that teaches a dead name is worse than no example.
|
|
//
|
|
// They are composed from the same constants the readers use, so an example
|
|
// cannot drift from the plane again.
|
|
const (
|
|
exSpan = telemetryplane.DBName + "." + telemetrytraces.SpanTableName
|
|
exSpanResource = telemetryplane.DBName + "." + telemetrytraces.SpanResourceTableName
|
|
exLog = telemetryplane.DBName + "." + telemetrylogs.LogTableName
|
|
exLogResource = telemetryplane.DBName + "." + telemetrylogs.LogResourceTableName
|
|
)
|
|
|
|
// DUAL DISPATCH. These registrations stay: the standalone server reaches them
|
|
// directly, and the composed binary reaches the SAME handlers through the typed
|
|
// query-engine ops in the repo root's querycore.go (querierQueryRange,
|
|
// querierQueryRangePreview, querierReplaceVariables), which relay here. Deleting
|
|
// either half drops one of the two deployments.
|
|
func (provider *provider) addQuerierRoutes(router routing.Router) {
|
|
router.Post("/v1/o11y/query_range", handler.New(provider.authzMiddleware.ViewAccess(provider.querierHandler.QueryRange), handler.OpenAPIDef{
|
|
ID: "QueryRangeV5",
|
|
Tags: []string{"querier"},
|
|
Summary: "Query range",
|
|
Description: "Execute a composite query over a time range. Supports builder queries (traces, logs, metrics), formulas, trace operators, PromQL, and Datastore SQL.",
|
|
Request: new(qbtypes.QueryRangeRequest),
|
|
RequestContentType: "application/json",
|
|
RequestExamples: []handler.OpenAPIExample{
|
|
{
|
|
Name: "traces_time_series",
|
|
Summary: "Time series: count spans grouped by service",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "time_series",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "traces",
|
|
"aggregations": []any{
|
|
map[string]any{"expression": "count()", "alias": "span_count"},
|
|
},
|
|
"stepInterval": "60s",
|
|
"filter": map[string]any{"expression": "service.name = 'frontend'"},
|
|
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
|
|
"order": []any{map[string]any{"key": map[string]any{"name": "span_count"}, "direction": "desc"}},
|
|
"limit": 10,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "logs_time_series",
|
|
Summary: "Time series: count error logs grouped by service",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "time_series",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "logs",
|
|
"aggregations": []any{
|
|
map[string]any{"expression": "count()", "alias": "log_count"},
|
|
},
|
|
"stepInterval": "60s",
|
|
"filter": map[string]any{"expression": "severity_text = 'ERROR'"},
|
|
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
|
|
"order": []any{map[string]any{"key": map[string]any{"name": "log_count"}, "direction": "desc"}},
|
|
"limit": 10,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "metrics_gauge_time_series",
|
|
Summary: "Time series: latest gauge value averaged across series",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "time_series",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "metrics",
|
|
"aggregations": []any{
|
|
map[string]any{"metricName": "system.cpu.utilization", "timeAggregation": "latest", "spaceAggregation": "avg"},
|
|
},
|
|
"stepInterval": "60s",
|
|
"groupBy": []any{map[string]any{"name": "host.name", "fieldContext": "resource"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "metrics_rate_time_series",
|
|
Summary: "Time series: rate of cumulative counter",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "time_series",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "metrics",
|
|
"aggregations": []any{
|
|
map[string]any{"metricName": "http.server.duration.count", "timeAggregation": "rate", "spaceAggregation": "sum"},
|
|
},
|
|
"stepInterval": 120,
|
|
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "metrics_histogram_time_series",
|
|
Summary: "Time series: p99 latency from histogram",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "time_series",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "metrics",
|
|
"aggregations": []any{
|
|
map[string]any{"metricName": "http.server.duration.bucket", "spaceAggregation": "p99"},
|
|
},
|
|
"stepInterval": "60s",
|
|
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "logs_raw",
|
|
Summary: "Raw: fetch raw log records",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "raw",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "logs",
|
|
"filter": map[string]any{"expression": "severity_text = 'ERROR'"},
|
|
"selectFields": []any{
|
|
map[string]any{"name": "body", "fieldContext": "log"},
|
|
map[string]any{"name": "service.name", "fieldContext": "resource"},
|
|
},
|
|
"order": []any{
|
|
map[string]any{"key": map[string]any{"name": "timestamp", "fieldContext": "log"}, "direction": "desc"},
|
|
map[string]any{"key": map[string]any{"name": "id"}, "direction": "desc"},
|
|
},
|
|
"limit": 50,
|
|
"offset": 0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "traces_raw",
|
|
Summary: "Raw: fetch raw span records",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "raw",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "traces",
|
|
"filter": map[string]any{"expression": "service.name = 'frontend' AND has_error = true"},
|
|
"selectFields": []any{
|
|
map[string]any{"name": "name", "fieldContext": "span"},
|
|
map[string]any{"name": "duration_nano", "fieldContext": "span"},
|
|
},
|
|
"order": []any{
|
|
map[string]any{"key": map[string]any{"name": "timestamp", "fieldContext": "span"}, "direction": "desc"},
|
|
},
|
|
"limit": 100,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "traces_scalar",
|
|
Summary: "Scalar: total span count",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "scalar",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "traces",
|
|
"aggregations": []any{
|
|
map[string]any{"expression": "count()", "alias": "span_count"},
|
|
},
|
|
"filter": map[string]any{"expression": "service.name = 'frontend'"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "logs_scalar",
|
|
Summary: "Scalar: total error log count",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "scalar",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "logs",
|
|
"aggregations": []any{
|
|
map[string]any{"expression": "count()", "alias": "error_count"},
|
|
},
|
|
"filter": map[string]any{"expression": "severity_text = 'ERROR'"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "metrics_scalar",
|
|
Summary: "Scalar: single reduced metric value",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "scalar",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "metrics",
|
|
"aggregations": []any{
|
|
map[string]any{"metricName": "http.server.duration.count", "timeAggregation": "rate", "spaceAggregation": "sum", "reduceTo": "sum"},
|
|
},
|
|
"stepInterval": "60s",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "formula",
|
|
Summary: "Formula: error rate from two trace queries",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "time_series",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "A",
|
|
"signal": "traces",
|
|
"aggregations": []any{map[string]any{"expression": "countIf(has_error = true)"}},
|
|
"stepInterval": "60s",
|
|
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
|
|
},
|
|
},
|
|
map[string]any{
|
|
"type": "builder_query",
|
|
"spec": map[string]any{
|
|
"name": "B",
|
|
"signal": "traces",
|
|
"aggregations": []any{map[string]any{"expression": "count()"}},
|
|
"stepInterval": "60s",
|
|
"groupBy": []any{map[string]any{"name": "service.name", "fieldContext": "resource"}},
|
|
},
|
|
},
|
|
map[string]any{
|
|
"type": "builder_formula",
|
|
"spec": map[string]any{
|
|
"name": "error_rate",
|
|
"expression": "A / B * 100",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "promql",
|
|
Summary: "PromQL: request rate with UTF-8 metric name",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "time_series",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "promql",
|
|
"spec": map[string]any{
|
|
"name": "request_rate",
|
|
"query": "sum(rate({\"http.server.duration.count\"}[5m])) by (\"service.name\")",
|
|
"step": 60,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "datastore_sql_traces_time_series",
|
|
Summary: "Datastore SQL: traces time series with resource filter",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "time_series",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "datastore_sql",
|
|
"spec": map[string]any{
|
|
"name": "span_rate",
|
|
"query": "WITH __resource_filter AS (" +
|
|
" SELECT fingerprint FROM " + exSpanResource +
|
|
" WHERE seen_at_ts_bucket_start >= $start_timestamp - 1800 AND seen_at_ts_bucket_start <= $end_timestamp" +
|
|
" ) SELECT toStartOfInterval(timestamp, INTERVAL 60 SECOND) AS ts, count() AS value" +
|
|
" FROM " + exSpan +
|
|
" WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)" +
|
|
" AND timestamp >= $start_datetime AND timestamp <= $end_datetime" +
|
|
" AND ts_bucket_start >= $start_timestamp - 1800 AND ts_bucket_start <= $end_timestamp" +
|
|
" GROUP BY ts ORDER BY ts",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "datastore_sql_logs_raw",
|
|
Summary: "Datastore SQL: raw logs with resource filter",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "raw",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "datastore_sql",
|
|
"spec": map[string]any{
|
|
"name": "recent_errors",
|
|
"query": "WITH __resource_filter AS (" +
|
|
" SELECT fingerprint FROM " + exLogResource +
|
|
" WHERE seen_at_ts_bucket_start >= $start_timestamp - 1800 AND seen_at_ts_bucket_start <= $end_timestamp" +
|
|
" ) SELECT timestamp, body" +
|
|
" FROM " + exLog +
|
|
" WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)" +
|
|
" AND timestamp >= $start_timestamp_nano AND timestamp <= $end_timestamp_nano" +
|
|
" AND ts_bucket_start >= $start_timestamp - 1800 AND ts_bucket_start <= $end_timestamp" +
|
|
" AND severity_text = 'ERROR'" +
|
|
" ORDER BY timestamp DESC LIMIT 100",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "datastore_sql_traces_scalar",
|
|
Summary: "Datastore SQL: scalar aggregate with resource filter",
|
|
Value: map[string]any{
|
|
"schemaVersion": "v1",
|
|
"start": 1640995200000,
|
|
"end": 1640998800000,
|
|
"requestType": "scalar",
|
|
"compositeQuery": map[string]any{
|
|
"queries": []any{
|
|
map[string]any{
|
|
"type": "datastore_sql",
|
|
"spec": map[string]any{
|
|
"name": "total_spans",
|
|
"query": "WITH __resource_filter AS (" +
|
|
" SELECT fingerprint FROM " + exSpanResource +
|
|
" WHERE seen_at_ts_bucket_start >= $start_timestamp - 1800 AND seen_at_ts_bucket_start <= $end_timestamp" +
|
|
" ) SELECT count() AS value" +
|
|
" FROM " + exSpan +
|
|
" WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)" +
|
|
" AND timestamp >= $start_datetime AND timestamp <= $end_datetime" +
|
|
" AND ts_bucket_start >= $start_timestamp - 1800 AND ts_bucket_start <= $end_timestamp",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Response: new(qbtypes.QueryRangeResponse),
|
|
ResponseContentType: "application/json",
|
|
SuccessStatusCode: http.StatusOK,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest},
|
|
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
|
}))
|
|
|
|
router.Post("/v1/o11y/query_range/preview", handler.New(provider.authzMiddleware.ViewAccess(provider.querierHandler.QueryRangePreview), handler.OpenAPIDef{
|
|
ID: "QueryRangePreviewV5",
|
|
Tags: []string{"querier"},
|
|
Summary: "Query range preview",
|
|
Description: "Validate a composite query without executing it. Accepts the same payload as the query range endpoint. By default (verbose=true) returns, for each query, the rendered underlying Datastore statement(s) with each statement's EXPLAIN ESTIMATE (per-table parts/rows/marks) and granule index analysis (candidate/surviving granules and the per-index pruning funnel). Pass ?verbose=false for the lightweight per-query verdict (valid/error/warnings) with no rendered SQL and no Datastore round trips. Intended for agentic/dry-run consumption: per-query errors are reported in the response rather than failing the whole request.",
|
|
Request: new(qbtypes.QueryRangeRequest),
|
|
RequestQuery: new(qbtypes.QueryRangePreviewParams),
|
|
RequestContentType: "application/json",
|
|
Response: new(qbtypes.QueryRangePreviewResponse),
|
|
ResponseContentType: "application/json",
|
|
SuccessStatusCode: http.StatusOK,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest},
|
|
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
|
}))
|
|
|
|
router.Post("/v1/o11y/substitute_vars", handler.New(provider.authzMiddleware.ViewAccess(provider.querierHandler.ReplaceVariables), handler.OpenAPIDef{
|
|
ID: "ReplaceVariables",
|
|
Tags: []string{"querier"},
|
|
Summary: "Replace variables",
|
|
Description: "Replace variables in a query",
|
|
Request: new(qbtypes.QueryRangeRequest),
|
|
RequestContentType: "application/json",
|
|
Response: new(qbtypes.QueryRangeRequest),
|
|
ResponseContentType: "application/json",
|
|
SuccessStatusCode: http.StatusOK,
|
|
ErrorStatusCodes: []int{http.StatusBadRequest},
|
|
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
|
}))
|
|
}
|