Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tender-cloths-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/api': minor
---

feat: Add POST /api/v2/search endpoint for querying raw log and trace rows programmatically
230 changes: 230 additions & 0 deletions packages/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
{
"name": "Webhooks",
"description": "Endpoints for managing webhooks"
},
{
"name": "Search",
"description": "Endpoints for querying raw data from log and trace sources"
}
],
"components": {
Expand Down Expand Up @@ -2608,6 +2612,96 @@
}
}
},
"SearchRequest": {
"type": "object",
"required": [
"sourceId"
],
"properties": {
"sourceId": {
"type": "string",
"description": "Source ID to query. Call GET /api/v2/sources to list available sources.\nThe source determines the underlying ClickHouse table (e.g. otel.otel_logs,\notel.otel_traces) and its column schema.\n",
"example": "69b46cb0d964ce2d0b9506a8"
},
"startTime": {
"type": "string",
"format": "date-time",
"description": "Start of the query window (ISO 8601). Defaults to 15 minutes before endTime. Must be before endTime.\n",
"example": "2026-05-10T00:00:00Z"
},
"endTime": {
"type": "string",
"format": "date-time",
"description": "End of the query window (ISO 8601). Defaults to now.",
"example": "2026-05-10T01:00:00Z"
},
"where": {
"type": "string",
"maxLength": 8192,
"default": "",
"description": "Row filter expression. The language is controlled by whereLanguage.\n\nLucene examples (default):\n SeverityText:ERROR\n pipedream.pipeline_name:my-pipeline AND SeverityText:ERROR\n Body:timeout\n\nSQL examples (whereLanguage: \"sql\"):\n SeverityText = 'ERROR'\n `pipedream.pipeline_name` = 'my-pipeline'\n",
"example": "SeverityText:ERROR"
},
"whereLanguage": {
"type": "string",
"enum": [
"lucene",
"sql"
],
"example": "lucene",
"default": "lucene",
"description": "Language used for the where filter. Default is lucene."
},
"select": {
"type": "string",
"maxLength": 4096,
"default": "",
"description": "Comma-separated list of ClickHouse column expressions to include in\neach result row. When omitted the source's default select expression\nis used.\n\nEach entry is a ClickHouse SQL expression executed under the team's\ndatabase user. Semicolons and subqueries (SELECT keyword) are\nrejected; use column references, map lookups, or function calls only.\n\nHyperDX rewrites known attribute column names to their materialized\nequivalents automatically; you can still pass the logical name.\n",
"example": "Timestamp,SeverityText,Body,pipedream.pipeline_name"
},
"orderBy": {
"type": "string",
"maxLength": 1024,
"description": "ClickHouse ORDER BY expression. When omitted the source's default\nordering (typically timestamp DESC) is used.\n",
"example": "Timestamp DESC"
},
"maxResults": {
"type": "integer",
"minimum": 1,
"maximum": 2000,
"default": 100,
"description": "Maximum number of rows to return. Default is 100, max is 2000."
},
"offset": {
"type": "integer",
"minimum": 0,
"maximum": 10000,
"default": 0,
"description": "Number of rows to skip (best-effort offset pagination). Default is\n0, max is 10000. Offset pagination is non-deterministic when\nmultiple rows share the same timestamp; for reliable deep paging\nfilter by the last Timestamp value returned in the previous page\ninstead of using a large offset.\n"
}
}
},
"SearchRow": {
"type": "object",
"description": "A single result row. Keys correspond to the requested columns (or the source default columns when columns was omitted). Values are strings or numbers as returned by ClickHouse.\n",
"additionalProperties": true
},
"SearchResponse": {
"type": "object",
"properties": {
"data": {
"type": "array",
"description": "Array of result rows. Each row is an object with keys corresponding to the requested columns.",
"items": {
"$ref": "#/components/schemas/SearchRow"
}
},
"rows": {
"type": "integer",
"description": "Number of rows in this response (not total matching rows)."
}
}
},
"QuerySetting": {
"type": "object",
"required": [
Expand Down Expand Up @@ -4884,6 +4978,142 @@
}
}
},
"/api/v2/search": {
"post": {
"summary": "Search Raw Logs and Traces",
"description": "Fetch individual log or trace rows from a HyperDX source.\n\nThis endpoint mirrors the \"search\" panel mode in the HyperDX UI.\nHyperDX applies the same query optimizations used in the UI:\n - Named attribute columns (e.g. \"pipedream.pipeline_name\") are\n rewritten to their indexed materialized equivalents when the\n source schema exposes them, avoiding slow Map lookups.\n - Rows are ordered by timestamp descending (most recent first).\n - The source's built-in PREWHERE / partition pruning is applied.\n\nAuthentication: Bearer token (personal API key from Team Settings).\n",
"operationId": "searchEvents",
"tags": [
"Search"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchRequest"
},
"examples": {
"recentErrors": {
"summary": "Recent errors for a service",
"value": {
"sourceId": "69b46cb0d964ce2d0b9506a8",
"startTime": "2026-05-10T00:00:00Z",
"endTime": "2026-05-10T01:00:00Z",
"where": "SeverityText:ERROR",
"select": "Timestamp,SeverityText,Body,ServiceName",
"maxResults": 50
}
},
"pipedreamTaskLogs": {
"summary": "Pipedream task logs with materialized column filter",
"value": {
"sourceId": "69b46cb0d964ce2d0b9506a8",
"startTime": "2026-05-10T00:00:00Z",
"endTime": "2026-05-10T01:00:00Z",
"where": "pipedream.pipeline_name:my-pipeline",
"select": "Timestamp,SeverityText,Body,pipedream.pipeline_name,pipedream.stage_name,pipedream.task_index",
"maxResults": 200
}
},
"traceSearch": {
"summary": "Slow spans for a service",
"value": {
"sourceId": "69b46cb0d964ce2d0b9508b2",
"startTime": "2026-05-10T00:00:00Z",
"endTime": "2026-05-10T01:00:00Z",
"where": "ServiceName:my-service AND Duration:>1000000000",
"select": "Timestamp,TraceId,SpanId,SpanName,ServiceName,Duration,StatusCode",
"maxResults": 100
}
},
"paginated": {
"summary": "Paginated fetch",
"value": {
"sourceId": "69b46cb0d964ce2d0b9506a8",
"startTime": "2026-05-10T00:00:00Z",
"endTime": "2026-05-10T01:00:00Z",
"maxResults": 100,
"offset": 100
}
}
}
}
}
},
"responses": {
"200": {
"description": "Matching rows returned successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchResponse"
},
"example": {
"data": [
{
"Timestamp": "2026-05-10T00:01:23.456789000Z",
"SeverityText": "ERROR",
"Body": "connection refused: redis:6379",
"ServiceName": "api-service"
}
],
"rows": 1
}
}
}
},
"400": {
"description": "Invalid request parameters or query error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
}
}
},
"401": {
"description": "Missing or invalid API key"
},
"404": {
"description": "Source or connection not found",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
}
}
},
"500": {
"description": "Server error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/api/v2/sources": {
"get": {
"summary": "List Sources",
Expand Down
3 changes: 3 additions & 0 deletions packages/api/src/mcp/tools/query/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export function parseTimeRange(
error: 'Invalid startTime or endTime: must be valid ISO 8601 strings',
};
}
if (startDate > endDate) {
return { error: 'startTime must not be after endTime' };
}
Comment thread
vinzee marked this conversation as resolved.
Resolved
return { startDate, endDate };
}

Expand Down
Loading
Loading