diff --git a/.changeset/external-api-min-auto-granularity.md b/.changeset/external-api-min-auto-granularity.md new file mode 100644 index 0000000000..c63e6f77be --- /dev/null +++ b/.changeset/external-api-min-auto-granularity.md @@ -0,0 +1,10 @@ +--- +'@hyperdx/api': patch +--- + +docs: Document `minAutoGranularity` on metric and PromQL sources in the external +API + +`GET`, `POST` and `PUT /api/v2/sources` already accepted and returned +`minAutoGranularity`, but the OpenAPI spec omitted it, so clients generated from +the spec dropped or rejected the field. diff --git a/packages/api/openapi.json b/packages/api/openapi.json index 6c2fd20b0f..bffa35d627 100644 --- a/packages/api/openapi.json +++ b/packages/api/openapi.json @@ -5594,6 +5594,12 @@ "description": "HyperDX Source for logs associated with metrics. Optional", "nullable": true, "example": "507f1f77bcf86cd799439011" + }, + "minAutoGranularity": { + "type": "string", + "description": "Floor for the auto-inferred chart granularity. Set it to the scrape/report interval of the metrics so a short time range does not produce buckets smaller than that interval (sparse, steppy series). Only constrains \"Auto\" granularity; an explicit granularity picked on a tile is never overridden. Omit for no minimum. Uses the SQL interval format (a positive integer followed by second, minute, hour, or day), not the short form of the materialized view granularity fields.", + "pattern": "^\\d+ (second|minute|hour|day)$", + "example": "1 minute" } } }, @@ -5736,6 +5742,12 @@ "type": "string", "description": "Required by the API for all source kinds; not used by PromQL queries.", "example": "timestamp" + }, + "minAutoGranularity": { + "type": "string", + "description": "Floor for the auto-inferred chart granularity, and the scrape interval that `$__rate_interval` is computed from. Set it to the Prometheus scrape interval so a short time range does not produce buckets smaller than that interval. Only constrains \"Auto\" granularity; an explicit granularity picked on a tile is never overridden. Omit for no minimum. Uses the SQL interval format (a positive integer followed by second, minute, hour, or day), not the short form of the materialized view granularity fields.", + "pattern": "^\\d+ (second|minute|hour|day)$", + "example": "1 minute" } } }, diff --git a/packages/api/src/routers/external-api/__tests__/sources-crud.int.test.ts b/packages/api/src/routers/external-api/__tests__/sources-crud.int.test.ts index f7286a13c2..7ad19429cd 100644 --- a/packages/api/src/routers/external-api/__tests__/sources-crud.int.test.ts +++ b/packages/api/src/routers/external-api/__tests__/sources-crud.int.test.ts @@ -90,6 +90,16 @@ describe('External API v2 Sources CRUD', () => { connection: connection._id.toString(), }); + const metricSourceBody = () => ({ + kind: SourceKind.Metric, + name: 'Created Metric Source', + from: { databaseName: DEFAULT_DATABASE, tableName: '' }, + timestampValueExpression: 'TimeUnix', + resourceAttributesExpression: 'ResourceAttributes', + metricTables: { gauge: 'otel_metrics_gauge' }, + connection: connection._id.toString(), + }); + const createOtherTeamConnection = (otherTeamId: mongoose.Types.ObjectId) => Connection.create({ team: otherTeamId, @@ -257,6 +267,25 @@ describe('External API v2 Sources CRUD', () => { expect(persisted!.kind).toBe(SourceKind.Promql); }); + it('should round-trip minAutoGranularity in the SQL interval format', async () => { + const response = await authRequest('post', BASE_URL) + .send({ ...metricSourceBody(), minAutoGranularity: '1 minute' }) + .expect(200); + + expect(response.body.data.minAutoGranularity).toBe('1 minute'); + + const persisted = await Source.findById(response.body.data.id); + expect(persisted!.get('minAutoGranularity')).toBe('1 minute'); + }); + + it('should reject a short-form minAutoGranularity', async () => { + // Unlike materializedViews[].minGranularity, this field is not mapped + // from the short form, so the documented contract is the SQL interval. + await authRequest('post', BASE_URL) + .send({ ...metricSourceBody(), minAutoGranularity: '1m' }) + .expect(400); + }); + it('should return 400 when connection is not a valid id', async () => { await authRequest('post', BASE_URL) .send({ ...logSourceBody(), connection: 'my-connection-name' }) diff --git a/packages/api/src/routers/external-api/v2/sources.ts b/packages/api/src/routers/external-api/v2/sources.ts index df25307038..0a30bd7bed 100644 --- a/packages/api/src/routers/external-api/v2/sources.ts +++ b/packages/api/src/routers/external-api/v2/sources.ts @@ -720,6 +720,11 @@ function formatExternalSource(source: SourceDocument) { * description: HyperDX Source for logs associated with metrics. Optional * nullable: true * example: 507f1f77bcf86cd799439011 + * minAutoGranularity: + * type: string + * description: Floor for the auto-inferred chart granularity. Set it to the scrape/report interval of the metrics so a short time range does not produce buckets smaller than that interval (sparse, steppy series). Only constrains "Auto" granularity; an explicit granularity picked on a tile is never overridden. Omit for no minimum. Uses the SQL interval format (a positive integer followed by second, minute, hour, or day), not the short form of the materialized view granularity fields. + * pattern: '^\d+ (second|minute|hour|day)$' + * example: 1 minute * SessionSource: * type: object * required: @@ -830,6 +835,11 @@ function formatExternalSource(source: SourceDocument) { * type: string * description: Required by the API for all source kinds; not used by PromQL queries. * example: timestamp + * minAutoGranularity: + * type: string + * description: Floor for the auto-inferred chart granularity, and the scrape interval that `$__rate_interval` is computed from. Set it to the Prometheus scrape interval so a short time range does not produce buckets smaller than that interval. Only constrains "Auto" granularity; an explicit granularity picked on a tile is never overridden. Omit for no minimum. Uses the SQL interval format (a positive integer followed by second, minute, hour, or day), not the short form of the materialized view granularity fields. + * pattern: '^\d+ (second|minute|hour|day)$' + * example: 1 minute * Source: * oneOf: * - $ref: '#/components/schemas/LogSource'