mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-23 00:30:30 +01:00
Compare commits
2 Commits
tvats-dry-
...
issue_5452
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
915b1e5a72 | ||
|
|
6e70d881da |
@@ -5597,22 +5597,6 @@ components:
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
Querybuildertypesv5EstimateEntry:
|
||||
properties:
|
||||
database:
|
||||
type: string
|
||||
marks:
|
||||
format: int64
|
||||
type: integer
|
||||
parts:
|
||||
format: int64
|
||||
type: integer
|
||||
rows:
|
||||
format: int64
|
||||
type: integer
|
||||
table:
|
||||
type: string
|
||||
type: object
|
||||
Querybuildertypesv5ExecStats:
|
||||
description: Execution statistics for the query, including rows scanned, bytes
|
||||
scanned, and duration.
|
||||
@@ -5680,25 +5664,6 @@ components:
|
||||
- anomaly
|
||||
- fillzero
|
||||
type: string
|
||||
Querybuildertypesv5Granules:
|
||||
properties:
|
||||
initial:
|
||||
format: int64
|
||||
type: integer
|
||||
reads:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5MergeTreeRead'
|
||||
type: array
|
||||
selected:
|
||||
format: int64
|
||||
type: integer
|
||||
skipScore:
|
||||
format: double
|
||||
type: number
|
||||
skipped:
|
||||
format: int64
|
||||
type: integer
|
||||
type: object
|
||||
Querybuildertypesv5GroupByKey:
|
||||
properties:
|
||||
description:
|
||||
@@ -5721,31 +5686,6 @@ components:
|
||||
expression:
|
||||
type: string
|
||||
type: object
|
||||
Querybuildertypesv5IndexStep:
|
||||
properties:
|
||||
condition:
|
||||
type: string
|
||||
initialGranules:
|
||||
format: int64
|
||||
type: integer
|
||||
initialParts:
|
||||
format: int64
|
||||
type: integer
|
||||
keys:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
selectedGranules:
|
||||
format: int64
|
||||
type: integer
|
||||
selectedParts:
|
||||
format: int64
|
||||
type: integer
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
Querybuildertypesv5Label:
|
||||
properties:
|
||||
key:
|
||||
@@ -5769,16 +5709,6 @@ components:
|
||||
expression:
|
||||
type: string
|
||||
type: object
|
||||
Querybuildertypesv5MergeTreeRead:
|
||||
properties:
|
||||
steps:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5IndexStep'
|
||||
nullable: true
|
||||
type: array
|
||||
table:
|
||||
type: string
|
||||
type: object
|
||||
Querybuildertypesv5MetricAggregation:
|
||||
properties:
|
||||
comparisonSpaceAggregationParam:
|
||||
@@ -5823,20 +5753,6 @@ components:
|
||||
- asc
|
||||
- desc
|
||||
type: string
|
||||
Querybuildertypesv5PreviewStatement:
|
||||
properties:
|
||||
db.statement.args:
|
||||
items: {}
|
||||
type: array
|
||||
db.statement.query:
|
||||
type: string
|
||||
estimate:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5EstimateEntry'
|
||||
type: array
|
||||
granules:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Granules'
|
||||
type: object
|
||||
Querybuildertypesv5PromQuery:
|
||||
properties:
|
||||
disabled:
|
||||
@@ -6159,39 +6075,6 @@ components:
|
||||
type:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryType'
|
||||
type: object
|
||||
Querybuildertypesv5QueryPreview:
|
||||
properties:
|
||||
error: {}
|
||||
magnitudeScore:
|
||||
nullable: true
|
||||
type: number
|
||||
selectivityScore:
|
||||
nullable: true
|
||||
type: number
|
||||
statements:
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5PreviewStatement'
|
||||
type: array
|
||||
valid:
|
||||
type: boolean
|
||||
warnings:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
type: object
|
||||
Querybuildertypesv5QueryRangePreviewResponse:
|
||||
description: Response from the v5 query range preview (dry-run) endpoint. For
|
||||
each query in the composite query, returns the underlying ClickHouse statement(s)
|
||||
it renders to without executing them (one per PromQL metric selector; exactly
|
||||
one for builder/ClickHouse/trace-operator queries), with the optional EXPLAIN
|
||||
ESTIMATE and granule analysis attached per statement when requested.
|
||||
properties:
|
||||
compositeQuery:
|
||||
additionalProperties:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryPreview'
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
Querybuildertypesv5QueryRangeRequest:
|
||||
description: Request body for the v5 query range endpoint. Supports builder
|
||||
queries (traces, logs, metrics), formulas, joins, trace operators, PromQL,
|
||||
@@ -22258,78 +22141,6 @@ paths:
|
||||
summary: Query range
|
||||
tags:
|
||||
- querier
|
||||
/api/v5/query_range/preview:
|
||||
post:
|
||||
deprecated: false
|
||||
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 ClickHouse statement(s) with each statement''s
|
||||
EXPLAIN ESTIMATE (per-table parts/rows/marks) and granule index analysis (candidate/surviving
|
||||
granules, skip score, and the per-index pruning funnel), plus two top-level
|
||||
scores: selectivityScore (0-100 granule-skip selectivity; higher is better)
|
||||
and magnitudeScore (0-100 absolute scan cost; higher is cheaper). Pass ?verbose=false
|
||||
for the lightweight per-query verdict (valid/error/warnings) with no rendered
|
||||
SQL and no ClickHouse round trips. Intended for agentic/dry-run consumption:
|
||||
per-query errors are reported in the response rather than failing the whole
|
||||
request.'
|
||||
operationId: QueryRangePreviewV5
|
||||
parameters:
|
||||
- in: query
|
||||
name: verbose
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeRequest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5QueryRangePreviewResponse'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Query range preview
|
||||
tags:
|
||||
- querier
|
||||
/api/v5/substitute_vars:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -101,10 +101,6 @@ func (h *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
|
||||
h.community.QueryRange(rw, req)
|
||||
}
|
||||
|
||||
func (h *handler) QueryRangePreview(rw http.ResponseWriter, req *http.Request) {
|
||||
h.community.QueryRangePreview(rw, req)
|
||||
}
|
||||
|
||||
func (h *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request) {
|
||||
h.community.QueryRawStream(rw, req)
|
||||
}
|
||||
|
||||
@@ -98,6 +98,15 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
aiObservability := ah.Signoz.Flagger.BooleanOrEmpty(ctx, flagger.FeatureEnableAIObservability, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureEnableAIObservability.String()),
|
||||
Active: aiObservability,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
|
||||
@@ -12,8 +12,6 @@ import type {
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
QueryRangePreviewV5200,
|
||||
QueryRangePreviewV5Params,
|
||||
QueryRangeV5200,
|
||||
Querybuildertypesv5QueryRangeRequestDTO,
|
||||
RenderErrorResponseDTO,
|
||||
@@ -106,107 +104,6 @@ export const useQueryRangeV5 = <
|
||||
> => {
|
||||
return useMutation(getQueryRangeV5MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* 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 ClickHouse statement(s) with each statement's EXPLAIN ESTIMATE (per-table parts/rows/marks) and granule index analysis (candidate/surviving granules, skip score, and the per-index pruning funnel), plus two top-level scores: selectivityScore (0-100 granule-skip selectivity; higher is better) and magnitudeScore (0-100 absolute scan cost; higher is cheaper). Pass ?verbose=false for the lightweight per-query verdict (valid/error/warnings) with no rendered SQL and no ClickHouse round trips. Intended for agentic/dry-run consumption: per-query errors are reported in the response rather than failing the whole request.
|
||||
* @summary Query range preview
|
||||
*/
|
||||
export const queryRangePreviewV5 = (
|
||||
querybuildertypesv5QueryRangeRequestDTO?: BodyType<Querybuildertypesv5QueryRangeRequestDTO>,
|
||||
params?: QueryRangePreviewV5Params,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<QueryRangePreviewV5200>({
|
||||
url: `/api/v5/query_range/preview`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: querybuildertypesv5QueryRangeRequestDTO,
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getQueryRangePreviewV5MutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof queryRangePreviewV5>>,
|
||||
TError,
|
||||
{
|
||||
data?: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
params?: QueryRangePreviewV5Params;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof queryRangePreviewV5>>,
|
||||
TError,
|
||||
{
|
||||
data?: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
params?: QueryRangePreviewV5Params;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['queryRangePreviewV5'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof queryRangePreviewV5>>,
|
||||
{
|
||||
data?: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
params?: QueryRangePreviewV5Params;
|
||||
}
|
||||
> = (props) => {
|
||||
const { data, params } = props ?? {};
|
||||
|
||||
return queryRangePreviewV5(data, params);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type QueryRangePreviewV5MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof queryRangePreviewV5>>
|
||||
>;
|
||||
export type QueryRangePreviewV5MutationBody =
|
||||
| BodyType<Querybuildertypesv5QueryRangeRequestDTO>
|
||||
| undefined;
|
||||
export type QueryRangePreviewV5MutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Query range preview
|
||||
*/
|
||||
export const useQueryRangePreviewV5 = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof queryRangePreviewV5>>,
|
||||
TError,
|
||||
{
|
||||
data?: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
params?: QueryRangePreviewV5Params;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof queryRangePreviewV5>>,
|
||||
TError,
|
||||
{
|
||||
data?: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
params?: QueryRangePreviewV5Params;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getQueryRangePreviewV5MutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Replace variables in a query
|
||||
* @summary Replace variables
|
||||
|
||||
@@ -7130,32 +7130,6 @@ export interface Querybuildertypesv5ColumnDescriptorDTO {
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5EstimateEntryDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
database?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
marks?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
parts?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
rows?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
table?: string;
|
||||
}
|
||||
|
||||
export type Querybuildertypesv5ExecStatsDTOStepIntervals = {
|
||||
[key: string]: number;
|
||||
};
|
||||
@@ -7196,99 +7170,6 @@ export interface Querybuildertypesv5FormatOptionsDTO {
|
||||
formatTableResultForUI?: boolean;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5IndexStepDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
condition?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
initialGranules?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
initialParts?: number;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
keys?: string[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
selectedGranules?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
selectedParts?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5MergeTreeReadDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
steps?: Querybuildertypesv5IndexStepDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
table?: string;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5GranulesDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
initial?: number;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
reads?: Querybuildertypesv5MergeTreeReadDTO[];
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
selected?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
skipScore?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
skipped?: number;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5PreviewStatementDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
'db.statement.args'?: unknown[];
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
'db.statement.query'?: string;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
estimate?: Querybuildertypesv5EstimateEntryDTO[];
|
||||
granules?: Querybuildertypesv5GranulesDTO;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5TimeSeriesDataDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
@@ -7370,49 +7251,6 @@ export type Querybuildertypesv5QueryDataDTO =
|
||||
results?: unknown[] | null;
|
||||
});
|
||||
|
||||
export interface Querybuildertypesv5QueryPreviewDTO {
|
||||
error?: unknown;
|
||||
/**
|
||||
* @type number,null
|
||||
*/
|
||||
magnitudeScore?: number | null;
|
||||
/**
|
||||
* @type number,null
|
||||
*/
|
||||
selectivityScore?: number | null;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
statements?: Querybuildertypesv5PreviewStatementDTO[];
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
valid?: boolean;
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export type Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQueryAnyOf =
|
||||
{ [key: string]: Querybuildertypesv5QueryPreviewDTO };
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQuery =
|
||||
Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQueryAnyOf | null;
|
||||
|
||||
/**
|
||||
* Response from the v5 query range preview (dry-run) endpoint. For each query in the composite query, returns the underlying ClickHouse statement(s) it renders to without executing them (one per PromQL metric selector; exactly one for builder/ClickHouse/trace-operator queries), with the optional EXPLAIN ESTIMATE and granule analysis attached per statement when requested.
|
||||
*/
|
||||
export interface Querybuildertypesv5QueryRangePreviewResponseDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
compositeQuery?: Querybuildertypesv5QueryRangePreviewResponseDTOCompositeQuery;
|
||||
}
|
||||
|
||||
export enum Querybuildertypesv5VariableTypeDTO {
|
||||
query = 'query',
|
||||
dynamic = 'dynamic',
|
||||
@@ -11112,22 +10950,6 @@ export type QueryRangeV5200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type QueryRangePreviewV5Params = {
|
||||
/**
|
||||
* @type string
|
||||
* @description undefined
|
||||
*/
|
||||
verbose?: string;
|
||||
};
|
||||
|
||||
export type QueryRangePreviewV5200 = {
|
||||
data: Querybuildertypesv5QueryRangePreviewResponseDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ReplaceVariables200 = {
|
||||
data: Querybuildertypesv5QueryRangeRequestDTO;
|
||||
/**
|
||||
|
||||
@@ -12,4 +12,5 @@ export enum FeatureKeys {
|
||||
USE_JSON_BODY = 'use_json_body',
|
||||
USE_FINE_GRAINED_AUTHZ = 'use_fine_grained_authz',
|
||||
USE_DASHBOARD_V2 = 'use_dashboard_v2',
|
||||
EMABLE_AI_OBSERVABILITY = 'enable_ai_observability',
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -180,7 +180,7 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/ClickHouse/ch-go v0.71.0
|
||||
github.com/ClickHouse/ch-go v0.71.0 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/Yiling-J/theine-go v0.6.2 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b
|
||||
|
||||
@@ -451,23 +451,6 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v5/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 ClickHouse statement(s) with each statement's EXPLAIN ESTIMATE (per-table parts/rows/marks) and granule index analysis (candidate/surviving granules, skip score, and the per-index pruning funnel), plus two top-level scores: selectivityScore (0-100 granule-skip selectivity; higher is better) and magnitudeScore (0-100 absolute scan cost; higher is cheaper). Pass ?verbose=false for the lightweight per-query verdict (valid/error/warnings) with no rendered SQL and no ClickHouse 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),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v5/substitute_vars", handler.New(provider.authzMiddleware.ViewAccess(provider.querierHandler.ReplaceVariables), handler.OpenAPIDef{
|
||||
ID: "ReplaceVariables",
|
||||
Tags: []string{"querier"},
|
||||
|
||||
@@ -3,15 +3,16 @@ package flagger
|
||||
import "github.com/SigNoz/signoz/pkg/types/featuretypes"
|
||||
|
||||
var (
|
||||
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
|
||||
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
|
||||
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
|
||||
FeatureGetMetersFromZeus = featuretypes.MustNewName("get_meters_from_zeus")
|
||||
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
|
||||
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureUseFineGrainedAuthz = featuretypes.MustNewName("use_fine_grained_authz")
|
||||
FeatureUseDashboardV2 = featuretypes.MustNewName("use_dashboard_v2")
|
||||
FeatureUseSpanMetrics = featuretypes.MustNewName("use_span_metrics")
|
||||
FeatureKafkaSpanEval = featuretypes.MustNewName("kafka_span_eval")
|
||||
FeatureHideRootUser = featuretypes.MustNewName("hide_root_user")
|
||||
FeatureGetMetersFromZeus = featuretypes.MustNewName("get_meters_from_zeus")
|
||||
FeaturePutMetersInZeus = featuretypes.MustNewName("put_meters_in_zeus")
|
||||
FeatureUseMeterReporter = featuretypes.MustNewName("use_meter_reporter")
|
||||
FeatureUseJSONBody = featuretypes.MustNewName("use_json_body")
|
||||
FeatureUseFineGrainedAuthz = featuretypes.MustNewName("use_fine_grained_authz")
|
||||
FeatureUseDashboardV2 = featuretypes.MustNewName("use_dashboard_v2")
|
||||
FeatureEnableAIObservability = featuretypes.MustNewName("enable_ai_observability")
|
||||
)
|
||||
|
||||
func MustNewRegistry() featuretypes.Registry {
|
||||
@@ -88,6 +89,14 @@ func MustNewRegistry() featuretypes.Registry {
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
&featuretypes.Feature{
|
||||
Name: FeatureEnableAIObservability,
|
||||
Kind: featuretypes.KindBoolean,
|
||||
Stage: featuretypes.StageExperimental,
|
||||
Description: "Controls whether ai observability is enabled",
|
||||
DefaultVariant: featuretypes.MustNewName("disabled"),
|
||||
Variants: featuretypes.NewBooleanVariants(),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package clickhouseprometheus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/prometheus/prometheus/prompb"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
)
|
||||
|
||||
// statementRecorder collects the ClickHouse statements a PromQL evaluation would
|
||||
// run. It is safe for concurrent use because the Prometheus engine may evaluate
|
||||
// (and therefore Select) multiple selectors concurrently.
|
||||
type statementRecorder struct {
|
||||
mu sync.Mutex
|
||||
statements []prometheus.CapturedStatement
|
||||
}
|
||||
|
||||
func (r *statementRecorder) record(query string, args []any) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.statements = append(r.statements, prometheus.CapturedStatement{Query: query, Args: args})
|
||||
}
|
||||
|
||||
func (r *statementRecorder) Statements() []prometheus.CapturedStatement {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
out := make([]prometheus.CapturedStatement, len(r.statements))
|
||||
copy(out, r.statements)
|
||||
return out
|
||||
}
|
||||
|
||||
// captureClient is a remote.ReadClient that builds the same ClickHouse SQL as
|
||||
// the real client but records it instead of executing, returning an empty
|
||||
// result so the engine completes without touching ClickHouse. It records the
|
||||
// self-contained samples query per selector (which embeds the series-selection
|
||||
// subquery), so the recorded statement reflects the actual data read.
|
||||
type captureClient struct {
|
||||
*client
|
||||
recorder *statementRecorder
|
||||
}
|
||||
|
||||
func (c *captureClient) Read(ctx context.Context, query *prompb.Query, _ bool) (storage.SeriesSet, error) {
|
||||
// Raw-SQL passthrough ({job="rawsql", query="..."}): record the raw query.
|
||||
if len(query.Matchers) == 2 {
|
||||
var hasJob bool
|
||||
var queryString string
|
||||
for _, m := range query.Matchers {
|
||||
if m.Type == prompb.LabelMatcher_EQ && m.Name == "job" && m.Value == "rawsql" {
|
||||
hasJob = true
|
||||
}
|
||||
if m.Type == prompb.LabelMatcher_EQ && m.Name == "query" {
|
||||
queryString = m.Value
|
||||
}
|
||||
}
|
||||
if hasJob && queryString != "" {
|
||||
c.recorder.record(queryString, nil)
|
||||
return storage.EmptySeriesSet(), nil
|
||||
}
|
||||
}
|
||||
|
||||
var metricName string
|
||||
for _, matcher := range query.Matchers {
|
||||
if matcher.Name == "__name__" {
|
||||
metricName = matcher.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Build the series-selection subquery and the self-contained samples query
|
||||
// exactly as the executing path would, but only record them.
|
||||
subQuery, args, err := c.queryToClickhouseQuery(ctx, query, metricName, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
samplesQuery, samplesArgs := buildSamplesQuery(int64(query.StartTimestampMs), int64(query.EndTimestampMs), metricName, subQuery, args)
|
||||
c.recorder.record(samplesQuery, samplesArgs)
|
||||
|
||||
return storage.EmptySeriesSet(), nil
|
||||
}
|
||||
|
||||
// captureQueryable adapts the capturing read client to storage.Queryable,
|
||||
// mirroring how the real provider wraps its querier.
|
||||
type captureQueryable struct {
|
||||
inner storage.SampleAndChunkQueryable
|
||||
}
|
||||
|
||||
func (c captureQueryable) Querier(mint, maxt int64) (storage.Querier, error) {
|
||||
querier, err := c.inner.Querier(mint, maxt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return storage.NewMergeQuerier(nil, []storage.Querier{querier}, storage.ChainedSeriesMerge), nil
|
||||
}
|
||||
@@ -204,11 +204,8 @@ func (client *client) getFingerprintsFromClickhouseQuery(ctx context.Context, qu
|
||||
return fingerprints, nil
|
||||
}
|
||||
|
||||
// buildSamplesQuery renders the samples SQL (and its args) that fetches the
|
||||
// data points for the series selected by subQuery. It embeds the series
|
||||
// selection as a subquery, so the returned statement is self-contained — the
|
||||
// dry-run/preview path renders it without executing.
|
||||
func buildSamplesQuery(start int64, end int64, metricName string, subQuery string, args []any) (string, []any) {
|
||||
func (client *client) querySamples(ctx context.Context, start int64, end int64, fingerprints map[uint64][]prompb.Label, metricName string, subQuery string, args []any) ([]*prompb.TimeSeries, error) {
|
||||
ctx = client.withClickhousePrometheusContext(ctx, "querySamples")
|
||||
argCount := len(args)
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
@@ -220,13 +217,6 @@ func buildSamplesQuery(start int64, end int64, metricName string, subQuery strin
|
||||
|
||||
allArgs := append([]any{metricName}, args...)
|
||||
allArgs = append(allArgs, start, end)
|
||||
return query, allArgs
|
||||
}
|
||||
|
||||
func (client *client) querySamples(ctx context.Context, start int64, end int64, fingerprints map[uint64][]prompb.Label, metricName string, subQuery string, args []any) ([]*prompb.TimeSeries, error) {
|
||||
ctx = client.withClickhousePrometheusContext(ctx, "querySamples")
|
||||
|
||||
query, allArgs := buildSamplesQuery(start, end, metricName, subQuery, args)
|
||||
|
||||
rows, err := client.telemetryStore.ClickhouseDB().Query(ctx, query, allArgs...)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore/telemetrystoretest"
|
||||
cmock "github.com/SigNoz/clickhouse-go-mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
|
||||
@@ -64,17 +64,3 @@ func (provider *provider) Querier(mint, maxt int64) (storage.Querier, error) {
|
||||
|
||||
return storage.NewMergeQuerier(nil, []storage.Querier{querier}, storage.ChainedSeriesMerge), nil
|
||||
}
|
||||
|
||||
// CapturingStorage implements prometheus.StatementCapturer: it returns a Storage
|
||||
// that records the ClickHouse SQL each selector would run (without executing
|
||||
// it) and a recorder to read the captured statements back. A fresh recorder is
|
||||
// created per call so concurrent dry-runs don't share state.
|
||||
func (provider *provider) CapturingStorage() (storage.Queryable, prometheus.StatementRecorder) {
|
||||
recorder := &statementRecorder{}
|
||||
capture := &captureClient{
|
||||
client: &client{settings: provider.settings, telemetryStore: provider.telemetryStore},
|
||||
recorder: recorder,
|
||||
}
|
||||
queryable := remote.NewSampleAndChunkQueryableClient(capture, labels.EmptyLabels(), []*labels.Matcher{}, false, stCallback)
|
||||
return captureQueryable{inner: queryable}, recorder
|
||||
}
|
||||
|
||||
@@ -15,25 +15,3 @@ type Prometheus interface {
|
||||
Storage() storage.Queryable
|
||||
Parser() Parser
|
||||
}
|
||||
|
||||
// CapturedStatement is one underlying datastore statement that a PromQL query would
|
||||
// run, captured without executing it.
|
||||
type CapturedStatement struct {
|
||||
Query string
|
||||
Args []any
|
||||
}
|
||||
|
||||
// StatementRecorder collects the Statements captured while a PromQL query is
|
||||
// evaluated against a capturing Storage (see StatementCapturer).
|
||||
type StatementRecorder interface {
|
||||
Statements() []CapturedStatement
|
||||
}
|
||||
|
||||
// StatementCapturer is an optional capability of a Prometheus provider: it
|
||||
// returns a Storage that records the datastore statement(s) each Select would
|
||||
// run — without executing them — together with a recorder to read them back.
|
||||
// The query dry-run path discovers it via a type assertion, so providers that
|
||||
// do not implement it simply expose no underlying SQL.
|
||||
type StatementCapturer interface {
|
||||
CapturingStorage() (storage.Queryable, StatementRecorder)
|
||||
}
|
||||
|
||||
@@ -73,61 +73,6 @@ func (handler *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
render.Success(rw, http.StatusOK, queryRangeResponse)
|
||||
}
|
||||
|
||||
// QueryRangePreview is the dry-run counterpart of QueryRange. It accepts the
|
||||
// same payload, validates and renders the underlying SQL/PromQL for each query
|
||||
// without executing it, and returns the per-query statements. ?verbose defaults
|
||||
// to true: each rendered statement carries its ClickHouse EXPLAIN ESTIMATE and
|
||||
// granule index analysis (with the top-level scores). ?verbose=false returns the
|
||||
// lightweight verdict-only response with no rendered SQL.
|
||||
func (handler *handler) QueryRangePreview(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.CodeNamespace: "querier",
|
||||
instrumentationtypes.CodeFunctionName: "QueryRangePreview",
|
||||
})
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var queryRangeRequest qbtypes.QueryRangeRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&queryRangeRequest); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
// NB: validation is intentionally NOT done here. QueryRangePreview checks
|
||||
// request-level invariants (aborting on failure) and validates each query's
|
||||
// spec individually, reporting per-query structural errors in the response
|
||||
// instead of failing fast on the first one — the point of the dry-run.
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
// verbose defaults to true (full preview); ?verbose=false returns the
|
||||
// lightweight verdict-only response.
|
||||
previewParams := qbtypes.QueryRangePreviewParams{Verbose: req.URL.Query().Get("verbose")}
|
||||
previewOpts, err := previewParams.Validate()
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
preview, err := handler.querier.QueryRangePreview(ctx, orgID, &queryRangeRequest, previewOpts)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, preview)
|
||||
}
|
||||
|
||||
func (handler *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
|
||||
|
||||
@@ -194,12 +194,6 @@ func (q *builderQuery[T]) isWindowList() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Statement renders the SQL statement for the builder query without executing
|
||||
// it. It is used by the dry-run/preview path.
|
||||
func (q *builderQuery[T]) Statement(ctx context.Context) (*qbtypes.Statement, error) {
|
||||
return q.stmtBuilder.Build(ctx, q.fromMS, q.toMS, q.kind, q.spec, q.variables)
|
||||
}
|
||||
|
||||
func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error) {
|
||||
|
||||
// can we do window based pagination?
|
||||
|
||||
@@ -99,16 +99,6 @@ func (q *chSQLQuery) renderVars(query string, vars map[string]qbtypes.VariableIt
|
||||
return newQuery.String(), nil
|
||||
}
|
||||
|
||||
// Statement renders the SQL statement for the ClickHouse SQL query without
|
||||
// executing it. It is used by the dry-run/preview path.
|
||||
func (q *chSQLQuery) Statement(_ context.Context) (*qbtypes.Statement, error) {
|
||||
rendered, err := q.renderVars(q.query.Query, q.vars, q.fromMS, q.toMS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &qbtypes.Statement{Query: rendered, Args: q.args}, nil
|
||||
}
|
||||
|
||||
func (q *chSQLQuery) Execute(ctx context.Context) (*qbtypes.Result, error) {
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.QueryDuration: instrumentationtypes.DurationBucket(q.fromMS, q.toMS),
|
||||
|
||||
@@ -14,11 +14,6 @@ type Querier interface {
|
||||
QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest) (*qbtypes.QueryRangeResponse, error)
|
||||
QueryRawStream(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, client *qbtypes.RawStream)
|
||||
statsreporter.StatsCollector
|
||||
// QueryRangePreview validates and renders the queries in req without
|
||||
// executing them. opts controls dry-run behavior such as which
|
||||
// EXPLAIN variant to attach to the response; the zero value performs
|
||||
// a validation-only preview with no EXPLAIN.
|
||||
QueryRangePreview(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, opts qbtypes.QueryRangePreviewOptions) (*qbtypes.QueryRangePreviewResponse, error)
|
||||
}
|
||||
|
||||
// BucketCache is the interface for bucket-based caching.
|
||||
@@ -31,10 +26,6 @@ type BucketCache interface {
|
||||
|
||||
type Handler interface {
|
||||
QueryRange(rw http.ResponseWriter, req *http.Request)
|
||||
// QueryRangePreview is the dry-run endpoint: it validates and renders the
|
||||
// queries without executing them, optionally attaching each statement's
|
||||
// ClickHouse EXPLAIN ESTIMATE (?estimate=) and granule analysis (?granules=).
|
||||
QueryRangePreview(rw http.ResponseWriter, req *http.Request)
|
||||
QueryRawStream(rw http.ResponseWriter, req *http.Request)
|
||||
ReplaceVariables(rw http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
@@ -1,637 +0,0 @@
|
||||
package querier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
chproto "github.com/ClickHouse/ch-go/proto"
|
||||
"github.com/ClickHouse/clickhouse-go/v2"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// magnitudeReferenceRows is the estimated row count treated as "fully expensive"
|
||||
// (magnitude score 0) by magnitudeScoreFromRows. It's a heuristic reference — the
|
||||
// point past which a scan is considered maximally costly — and is deliberately a
|
||||
// single tunable constant rather than per-table, so the score is comparable
|
||||
// across queries.
|
||||
const magnitudeReferenceRows = 1e9
|
||||
|
||||
// userFacingClickHouseErrorCodes mirrors PR #10679's userFacingCHCodes: the
|
||||
// ClickHouse error codes that indicate a problem with the query itself (bad SQL,
|
||||
// unknown table/column, …) rather than a server-side/infra failure — i.e. the
|
||||
// ones that should map to invalid input (400) instead of internal (500).
|
||||
//
|
||||
// TODO(#10679): once that PR lands, delete this and have explainBindCheck call
|
||||
// the shared querier.mapClickHouseError so there's a single source of truth.
|
||||
var userFacingClickHouseErrorCodes = map[chproto.Error]bool{
|
||||
chproto.ErrSyntaxError: true,
|
||||
chproto.ErrUnknownTable: true,
|
||||
chproto.ErrUnknownDatabase: true,
|
||||
chproto.ErrUnknownIdentifier: true,
|
||||
chproto.ErrUnknownFunction: true,
|
||||
chproto.ErrUnknownAggregateFunction: true,
|
||||
chproto.ErrUnknownType: true,
|
||||
chproto.ErrUnknownStorage: true,
|
||||
chproto.ErrUnknownElementInAst: true,
|
||||
chproto.ErrUnknownTypeOfQuery: true,
|
||||
chproto.ErrIllegalTypeOfArgument: true,
|
||||
chproto.ErrIllegalColumn: true,
|
||||
chproto.ErrNumberOfArgumentsDoesntMatch: true,
|
||||
chproto.ErrTooManyArgumentsForFunction: true,
|
||||
chproto.ErrTooLessArgumentsForFunction: true,
|
||||
}
|
||||
|
||||
// statementProvider is implemented by query types that can render the
|
||||
// underlying SQL/PromQL statement without executing it.
|
||||
type statementProvider interface {
|
||||
Statement(ctx context.Context) (*qbtypes.Statement, error)
|
||||
}
|
||||
|
||||
// previewTask is one rendered ClickHouse statement queued for ClickHouse-bound
|
||||
// preview work (the granules and/or estimate analysis). stmtIdx is the index into
|
||||
// the query's Statements list that this task's results merge back into.
|
||||
type previewTask struct {
|
||||
name string
|
||||
stmtIdx int
|
||||
query string
|
||||
args []any
|
||||
}
|
||||
|
||||
type explainPlanNode struct {
|
||||
NodeType string `json:"Node Type"`
|
||||
Description string `json:"Description"`
|
||||
Indexes []explainPlanIndex `json:"Indexes"`
|
||||
Plans []explainPlanNode `json:"Plans"`
|
||||
}
|
||||
|
||||
type explainPlanIndex struct {
|
||||
Type string `json:"Type"`
|
||||
Name string `json:"Name"`
|
||||
Keys []string `json:"Keys"`
|
||||
Condition string `json:"Condition"`
|
||||
InitialParts *int64 `json:"Initial Parts"`
|
||||
SelectedParts *int64 `json:"Selected Parts"`
|
||||
InitialGranules *int64 `json:"Initial Granules"`
|
||||
SelectedGranules *int64 `json:"Selected Granules"`
|
||||
}
|
||||
|
||||
// QueryRangePreview validates each query in the composite query without
|
||||
// executing it. With opts.Verbose=false it returns a lightweight per-query
|
||||
// verdict (valid/error/warnings). With opts.Verbose=true it also renders the
|
||||
// underlying ClickHouse statement(s) each query would run and attaches, per
|
||||
// statement, the EXPLAIN ESTIMATE and granule index analysis, deriving the
|
||||
// top-level SelectivityScore and MagnitudeScore.
|
||||
func (q *querier) QueryRangePreview(
|
||||
ctx context.Context,
|
||||
_ valuer.UUID,
|
||||
req *qbtypes.QueryRangeRequest,
|
||||
opts qbtypes.QueryRangePreviewOptions,
|
||||
) (*qbtypes.QueryRangePreviewResponse, error) {
|
||||
|
||||
validationOpts, err := req.ValidateRequestScope()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dependencyQueries, err := q.constructTraceOperatorDependencyMap(req.CompositeQuery.Queries)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make(map[string]qbtypes.QueryPreview, len(req.CompositeQuery.Queries))
|
||||
|
||||
prepared := make(map[string]qbtypes.QueryPreview, len(req.CompositeQuery.Queries))
|
||||
missingMetricQuerySet := make(map[string]bool)
|
||||
for idx := range req.CompositeQuery.Queries {
|
||||
name := req.CompositeQuery.Queries[idx].GetQueryName()
|
||||
ps := qbtypes.QueryPreview{}
|
||||
|
||||
if vErr := req.CompositeQuery.Queries[idx].Validate(validationOpts...); vErr != nil {
|
||||
ps.Error = vErr
|
||||
prepared[name] = ps
|
||||
continue
|
||||
}
|
||||
|
||||
env := []qbtypes.QueryEnvelope{req.CompositeQuery.Queries[idx]}
|
||||
ps.Warnings = q.adjustStepInterval(env, req.Start, req.End)
|
||||
|
||||
missingMetricQueries, metricWarnings, mErr := q.resolveMetricMetadata(ctx, env, req.Start, req.End)
|
||||
if mErr != nil {
|
||||
// Don't abort the whole preview: report this query's error and keep
|
||||
// going so the agent sees every problem in one round trip.
|
||||
ps.Error = mErr
|
||||
} else {
|
||||
ps.Warnings = append(ps.Warnings, metricWarnings...)
|
||||
if len(missingMetricQueries) > 0 {
|
||||
missingMetricQuerySet[name] = true
|
||||
if len(metricWarnings) == 0 {
|
||||
if metricNames := missingMetricNames(env[0]); len(metricNames) > 0 {
|
||||
ps.Warnings = append(ps.Warnings, fmt.Sprintf(
|
||||
"query %q references metric(s) %s with no data available; it will return an empty result",
|
||||
name, strings.Join(metricNames, ", ")))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req.CompositeQuery.Queries[idx] = env[0]
|
||||
prepared[name] = ps
|
||||
}
|
||||
|
||||
skip := make(map[string]bool, len(prepared))
|
||||
for name, ps := range prepared {
|
||||
if ps.Error != nil || missingMetricQuerySet[name] {
|
||||
skip[name] = true
|
||||
}
|
||||
}
|
||||
providers, buildErrs := q.buildPreviewProviders(req, dependencyQueries, missingMetricQuerySet, skip)
|
||||
|
||||
// Render the statement for each query that actually executes, and collect the
|
||||
// ClickHouse-bound work (granules/estimate analyses) to run concurrently.
|
||||
var previewTasks []previewTask
|
||||
for _, query := range req.CompositeQuery.Queries {
|
||||
name := query.GetQueryName()
|
||||
ps := prepared[name]
|
||||
|
||||
// Surface a phase-1 error (e.g. a not-found metric) without rendering.
|
||||
if ps.Error != nil {
|
||||
results[name] = ps
|
||||
continue
|
||||
}
|
||||
// Every aggregation resolved to a missing metric: QueryRange returns an
|
||||
// empty result for this query and renders no SQL. Mirror that.
|
||||
if missingMetricQuerySet[name] {
|
||||
results[name] = ps
|
||||
continue
|
||||
}
|
||||
// A build error is this query's verdict — attribute it and move on
|
||||
// instead of aborting the whole preview.
|
||||
if bErr := buildErrs[name]; bErr != nil {
|
||||
ps.Error = bErr
|
||||
results[name] = ps
|
||||
continue
|
||||
}
|
||||
|
||||
provider, ok := providers[name]
|
||||
if !ok {
|
||||
// Formula/join/sub-query are valid query types that render no standalone
|
||||
// statement of their own — they're evaluated from the queries they
|
||||
// reference, which are previewed individually. Report them as valid with
|
||||
// a note rather than failing them.
|
||||
if !rendersStandaloneStatement(query.Type) {
|
||||
ps.Warnings = append(ps.Warnings, fmt.Sprintf(
|
||||
"query type %q has no standalone statement to preview; it is evaluated from the queries it references", query.Type.StringValue()))
|
||||
results[name] = ps
|
||||
continue
|
||||
}
|
||||
ps.Error = errors.NewInternalf(errors.CodeInternal, "query produced no provider")
|
||||
results[name] = ps
|
||||
continue
|
||||
}
|
||||
|
||||
stmtProvider, ok := provider.(statementProvider)
|
||||
if !ok {
|
||||
ps.Error = errors.NewInternalf(errors.CodeInternal, "query does not support preview")
|
||||
results[name] = ps
|
||||
continue
|
||||
}
|
||||
|
||||
stmt, sErr := stmtProvider.Statement(ctx)
|
||||
if sErr != nil {
|
||||
ps.Error = sErr
|
||||
results[name] = ps
|
||||
continue
|
||||
}
|
||||
|
||||
ps.Warnings = append(ps.Warnings, stmt.Warnings...)
|
||||
|
||||
// clickhouse_sql is user-authored raw SQL; rendering only substitutes
|
||||
// variables, so by itself it doesn't prove the SQL is valid. Verify it
|
||||
// parses and binds (tables/columns/types resolve) via EXPLAIN PLAN —
|
||||
// without executing. Builder/PromQL/trace-operator SQL is engine-generated
|
||||
// and well-formed by construction, so this is scoped to clickhouse_sql.
|
||||
if query.Type == qbtypes.QueryTypeClickHouseSQL {
|
||||
if bindErr := q.explainBindCheck(ctx, stmt.Query, stmt.Args); bindErr != nil {
|
||||
if errors.Ast(bindErr, errors.TypeInvalidInput) {
|
||||
ps.Error = bindErr
|
||||
results[name] = ps
|
||||
continue
|
||||
}
|
||||
// Validity unknown (infra/non-user-facing failure) — warn, don't
|
||||
// falsely mark the query invalid.
|
||||
ps.Warnings = append(ps.Warnings, "could not validate ClickHouse SQL: "+bindErr.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if !opts.Verbose {
|
||||
results[name] = ps
|
||||
continue
|
||||
}
|
||||
|
||||
if query.Type == qbtypes.QueryTypePromQL {
|
||||
if pq, ok := provider.(*promqlQuery); ok {
|
||||
sqlStmts, pErr := pq.PreviewStatements(ctx)
|
||||
if pErr != nil {
|
||||
ps.Warnings = append(ps.Warnings, "could not render underlying ClickHouse SQL: "+pErr.Error())
|
||||
} else {
|
||||
for _, s := range sqlStmts {
|
||||
ps.Statements = append(ps.Statements, qbtypes.PreviewStatement{Query: s.Query, Args: s.Args})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ps.Statements = []qbtypes.PreviewStatement{{Query: stmt.Query, Args: stmt.Args}}
|
||||
}
|
||||
|
||||
results[name] = ps
|
||||
|
||||
for j := range ps.Statements {
|
||||
previewTasks = append(previewTasks, previewTask{name: name, stmtIdx: j, query: ps.Statements[j].Query, args: ps.Statements[j].Args})
|
||||
}
|
||||
}
|
||||
|
||||
q.runPreviewTasks(ctx, previewTasks, results)
|
||||
|
||||
for name, ps := range results {
|
||||
var minSelectivity, minMagnitude *float64
|
||||
for i := range ps.Statements {
|
||||
if g := ps.Statements[i].Granules; g != nil && (minSelectivity == nil || g.SkipScore < *minSelectivity) {
|
||||
s := g.SkipScore
|
||||
minSelectivity = &s
|
||||
}
|
||||
if est := ps.Statements[i].Estimate; len(est) > 0 {
|
||||
var rows int64
|
||||
for j := range est {
|
||||
rows += est[j].Rows
|
||||
}
|
||||
if m := magnitudeScoreFromRows(rows); minMagnitude == nil || m < *minMagnitude {
|
||||
minMagnitude = &m
|
||||
}
|
||||
}
|
||||
}
|
||||
if minSelectivity != nil {
|
||||
ps.SelectivityScore = minSelectivity
|
||||
}
|
||||
if minMagnitude != nil {
|
||||
ps.MagnitudeScore = minMagnitude
|
||||
}
|
||||
results[name] = ps
|
||||
}
|
||||
|
||||
return &qbtypes.QueryRangePreviewResponse{
|
||||
CompositeQuery: results,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// missingMetricNames returns the distinct metric names referenced by a metric
|
||||
// builder query, in order of first appearance. It is used to name the metric(s)
|
||||
// in the warning attached to a fully-missing-metric query. Returns nil for any
|
||||
// non-metric query.
|
||||
func missingMetricNames(env qbtypes.QueryEnvelope) []string {
|
||||
spec, ok := env.Spec.(qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation])
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
names := make([]string, 0, len(spec.Aggregations))
|
||||
for _, agg := range spec.Aggregations {
|
||||
if agg.MetricName != "" && !slices.Contains(names, agg.MetricName) {
|
||||
names = append(names, agg.MetricName)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (q *querier) buildPreviewProviders(
|
||||
req *qbtypes.QueryRangeRequest,
|
||||
dependencyQueries map[string]bool,
|
||||
missingMetricQuerySet map[string]bool,
|
||||
skip map[string]bool,
|
||||
) (providers map[string]qbtypes.Query, errs map[string]error) {
|
||||
providers = make(map[string]qbtypes.Query)
|
||||
errs = make(map[string]error)
|
||||
|
||||
// buildQueries records analytics on the event; the preview emits none.
|
||||
event := &qbtypes.QBEvent{}
|
||||
|
||||
for _, query := range req.CompositeQuery.Queries {
|
||||
name := query.GetQueryName()
|
||||
if skip[name] {
|
||||
continue
|
||||
}
|
||||
|
||||
sub := *req // shallow copy: only CompositeQuery and RequestType are swapped
|
||||
|
||||
// deps is the set buildQueries skips within this composite: empty for a
|
||||
// standalone query (so it gets built), and the operator's referenced
|
||||
// siblings for a trace operator (so only the operator is built from it).
|
||||
var deps map[string]bool
|
||||
|
||||
switch {
|
||||
case query.GetType() == qbtypes.QueryTypeTraceOperator:
|
||||
refs, rErr := q.traceOperatorPreviewComposite(req, query)
|
||||
if rErr != nil {
|
||||
errs[name] = rErr
|
||||
continue
|
||||
}
|
||||
sub.CompositeQuery = qbtypes.CompositeQuery{Queries: refs}
|
||||
deps = dependencyQueries
|
||||
case dependencyQueries[name]:
|
||||
sub.RequestType = qbtypes.RequestTypeRaw
|
||||
sub.CompositeQuery = qbtypes.CompositeQuery{Queries: []qbtypes.QueryEnvelope{query}}
|
||||
default:
|
||||
sub.CompositeQuery = qbtypes.CompositeQuery{Queries: []qbtypes.QueryEnvelope{query}}
|
||||
}
|
||||
|
||||
built, _, bErr := q.buildQueries(&sub, deps, missingMetricQuerySet, event)
|
||||
if bErr != nil {
|
||||
errs[name] = bErr
|
||||
continue
|
||||
}
|
||||
|
||||
if provider, ok := built[name]; ok {
|
||||
providers[name] = provider
|
||||
}
|
||||
}
|
||||
return providers, errs
|
||||
}
|
||||
|
||||
// rendersStandaloneStatement reports whether a query type renders to its own
|
||||
// ClickHouse/PromQL statement the preview can build and analyze. Formula, join,
|
||||
// and sub-query are valid query types but carry no statement of their own —
|
||||
// they're evaluated from the queries they reference — so buildQueries (and hence
|
||||
// the preview) renders nothing for them. Mirrors buildQueries' switch.
|
||||
func rendersStandaloneStatement(t qbtypes.QueryType) bool {
|
||||
switch t {
|
||||
case qbtypes.QueryTypeBuilder,
|
||||
qbtypes.QueryTypePromQL,
|
||||
qbtypes.QueryTypeClickHouseSQL,
|
||||
qbtypes.QueryTypeTraceOperator:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (q *querier) traceOperatorPreviewComposite(req *qbtypes.QueryRangeRequest, operator qbtypes.QueryEnvelope) ([]qbtypes.QueryEnvelope, error) {
|
||||
spec, ok := operator.Spec.(qbtypes.QueryBuilderTraceOperator)
|
||||
if !ok {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid trace operator query spec %T", operator.Spec)
|
||||
}
|
||||
if err := spec.ParseExpression(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
referenced := make(map[string]bool)
|
||||
for _, name := range spec.CollectReferencedQueries(spec.ParsedExpression) {
|
||||
referenced[name] = true
|
||||
}
|
||||
|
||||
queries := make([]qbtypes.QueryEnvelope, 0, len(referenced)+1)
|
||||
for _, qe := range req.CompositeQuery.Queries {
|
||||
if referenced[qe.GetQueryName()] {
|
||||
queries = append(queries, qe)
|
||||
}
|
||||
}
|
||||
return append(queries, operator), nil
|
||||
}
|
||||
|
||||
func (q *querier) runPreviewTasks(ctx context.Context, tasks []previewTask, previews map[string]qbtypes.QueryPreview) {
|
||||
if len(tasks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type outcome struct {
|
||||
granules *qbtypes.Granules
|
||||
estimate []qbtypes.EstimateEntry
|
||||
warnings []string
|
||||
}
|
||||
outcomes := make([]outcome, len(tasks))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := range tasks {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
t := tasks[i]
|
||||
var out outcome
|
||||
if granules, ok, scErr := q.computeGranuleStats(ctx, t.query, t.args); scErr != nil {
|
||||
// Surface the failure instead of silently dropping the score.
|
||||
out.warnings = append(out.warnings, "could not compute query score: "+scErr.Error())
|
||||
} else if ok {
|
||||
out.granules = &granules
|
||||
}
|
||||
if estimate, eErr := q.runExplainEstimate(ctx, t.query, t.args); eErr != nil {
|
||||
// Surface the failure instead of silently dropping the output.
|
||||
out.warnings = append(out.warnings, "could not run EXPLAIN ESTIMATE: "+eErr.Error())
|
||||
} else {
|
||||
out.estimate = estimate
|
||||
}
|
||||
outcomes[i] = out
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for i := range tasks {
|
||||
ps := previews[tasks[i].name]
|
||||
if idx := tasks[i].stmtIdx; idx >= 0 && idx < len(ps.Statements) {
|
||||
if outcomes[i].granules != nil {
|
||||
ps.Statements[idx].Granules = outcomes[i].granules
|
||||
}
|
||||
if len(outcomes[i].estimate) > 0 {
|
||||
ps.Statements[idx].Estimate = outcomes[i].estimate
|
||||
}
|
||||
}
|
||||
ps.Warnings = append(ps.Warnings, outcomes[i].warnings...)
|
||||
previews[tasks[i].name] = ps
|
||||
}
|
||||
}
|
||||
|
||||
func (q *querier) runExplainEstimate(ctx context.Context, stmt string, args []any) ([]qbtypes.EstimateEntry, error) {
|
||||
rows, err := q.telemetryStore.ClickhouseDB().Query(ctx, "EXPLAIN ESTIMATE "+stmt, args...)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to run EXPLAIN ESTIMATE")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
colTypes := rows.ColumnTypes()
|
||||
var entries []qbtypes.EstimateEntry
|
||||
for rows.Next() {
|
||||
dest := make([]any, len(colTypes))
|
||||
for i, ct := range colTypes {
|
||||
dest[i] = reflect.New(ct.ScanType()).Interface()
|
||||
}
|
||||
if err := rows.Scan(dest...); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan EXPLAIN ESTIMATE row")
|
||||
}
|
||||
var entry qbtypes.EstimateEntry
|
||||
for i, ct := range colTypes {
|
||||
val := reflect.ValueOf(dest[i]).Elem().Interface()
|
||||
switch strings.ToLower(ct.Name()) {
|
||||
case "database":
|
||||
entry.Database = fmt.Sprintf("%v", val)
|
||||
case "table":
|
||||
entry.Table = fmt.Sprintf("%v", val)
|
||||
case "parts":
|
||||
entry.Parts = toInt64(val)
|
||||
case "rows":
|
||||
entry.Rows = toInt64(val)
|
||||
case "marks":
|
||||
entry.Marks = toInt64(val)
|
||||
}
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, errors.WrapInternalf(err, errors.CodeInternal, "EXPLAIN ESTIMATE row iteration failed")
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// toInt64 coerces a driver-scanned numeric value (ESTIMATE's parts/rows/marks
|
||||
// arrive as unsigned integers) to int64. A non-numeric value yields 0.
|
||||
func toInt64(v any) int64 {
|
||||
rv := reflect.ValueOf(v)
|
||||
switch rv.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return rv.Int()
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return int64(rv.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return int64(rv.Float())
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func (q *querier) explainBindCheck(ctx context.Context, stmt string, args []any) error {
|
||||
rows, err := q.telemetryStore.ClickhouseDB().Query(ctx, "EXPLAIN PLAN "+stmt, args...)
|
||||
if err != nil {
|
||||
var ex *clickhouse.Exception
|
||||
if errors.As(err, &ex) && userFacingClickHouseErrorCodes[chproto.Error(ex.Code)] {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid ClickHouse SQL: %s", ex.Message)
|
||||
}
|
||||
return err
|
||||
}
|
||||
rows.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func magnitudeScoreFromRows(rows int64) float64 {
|
||||
if rows <= 1 {
|
||||
return 100
|
||||
}
|
||||
ratio := math.Log10(float64(rows)) / math.Log10(magnitudeReferenceRows)
|
||||
if ratio < 0 {
|
||||
ratio = 0
|
||||
}
|
||||
if ratio > 1 {
|
||||
ratio = 1
|
||||
}
|
||||
return math.Round((1-ratio)*100*100) / 100 // percentage, 2 decimal places
|
||||
}
|
||||
|
||||
func (q *querier) computeGranuleStats(ctx context.Context, stmt string, args []any) (qbtypes.Granules, bool, error) {
|
||||
rows, err := q.telemetryStore.ClickhouseDB().Query(ctx, "EXPLAIN json = 1, indexes = 1 "+stmt, args...)
|
||||
if err != nil {
|
||||
return qbtypes.Granules{}, false, errors.WrapInternalf(err, errors.CodeInternal, "failed to run EXPLAIN for query score")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// json=1 emits the plan as a single JSON document; read every row and join
|
||||
// so we are robust to the driver splitting it across rows.
|
||||
var sb strings.Builder
|
||||
for rows.Next() {
|
||||
var line string
|
||||
if err := rows.Scan(&line); err != nil {
|
||||
return qbtypes.Granules{}, false, errors.WrapInternalf(err, errors.CodeInternal, "failed to scan EXPLAIN json row")
|
||||
}
|
||||
sb.WriteString(line)
|
||||
sb.WriteByte('\n')
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return qbtypes.Granules{}, false, errors.WrapInternalf(err, errors.CodeInternal, "EXPLAIN json row iteration failed")
|
||||
}
|
||||
|
||||
var plans []struct {
|
||||
Plan explainPlanNode `json:"Plan"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(sb.String()), &plans); err != nil {
|
||||
return qbtypes.Granules{}, false, errors.WrapInternalf(err, errors.CodeInternal, "failed to parse EXPLAIN json")
|
||||
}
|
||||
|
||||
var totalInitial, totalSelected int64
|
||||
var reads []qbtypes.MergeTreeRead
|
||||
for i := range plans {
|
||||
collectMergeTreeReads(&plans[i].Plan, &reads, &totalInitial, &totalSelected)
|
||||
}
|
||||
if totalInitial <= 0 {
|
||||
// No MergeTree index analysis in the plan — nothing to score.
|
||||
return qbtypes.Granules{}, false, nil
|
||||
}
|
||||
if totalSelected < 0 {
|
||||
totalSelected = 0
|
||||
}
|
||||
skippedGranules := totalInitial - totalSelected
|
||||
if skippedGranules < 0 {
|
||||
skippedGranules = 0
|
||||
}
|
||||
ratio := float64(skippedGranules) / float64(totalInitial)
|
||||
score := math.Round(ratio*100*100) / 100 // percentage, 2 decimal places
|
||||
return qbtypes.Granules{
|
||||
Initial: totalInitial,
|
||||
Selected: totalSelected,
|
||||
Skipped: skippedGranules,
|
||||
SkipScore: score,
|
||||
Reads: reads,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func derefInt64(p *int64) int64 {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
func collectMergeTreeReads(node *explainPlanNode, reads *[]qbtypes.MergeTreeRead, totalInitial, totalSelected *int64) {
|
||||
if node.NodeType == "ReadFromMergeTree" && len(node.Indexes) > 0 {
|
||||
steps := make([]qbtypes.IndexStep, 0, len(node.Indexes))
|
||||
var initial, selected *int64
|
||||
for i := range node.Indexes {
|
||||
idx := node.Indexes[i]
|
||||
if idx.InitialGranules != nil && initial == nil {
|
||||
initial = idx.InitialGranules
|
||||
}
|
||||
if idx.SelectedGranules != nil {
|
||||
selected = idx.SelectedGranules
|
||||
}
|
||||
steps = append(steps, qbtypes.IndexStep{
|
||||
Type: idx.Type,
|
||||
Name: idx.Name,
|
||||
Keys: idx.Keys,
|
||||
Condition: idx.Condition,
|
||||
InitialParts: derefInt64(idx.InitialParts),
|
||||
SelectedParts: derefInt64(idx.SelectedParts),
|
||||
InitialGranules: derefInt64(idx.InitialGranules),
|
||||
SelectedGranules: derefInt64(idx.SelectedGranules),
|
||||
})
|
||||
}
|
||||
if initial != nil && selected != nil {
|
||||
*totalInitial += *initial
|
||||
*totalSelected += *selected
|
||||
}
|
||||
*reads = append(*reads, qbtypes.MergeTreeRead{Table: node.Description, Steps: steps})
|
||||
}
|
||||
for i := range node.Plans {
|
||||
collectMergeTreeReads(&node.Plans[i], reads, totalInitial, totalSelected)
|
||||
}
|
||||
}
|
||||
@@ -220,68 +220,6 @@ func (q *promqlQuery) renderVars(query string, vars map[string]qbv5.VariableItem
|
||||
return newQuery.String(), nil
|
||||
}
|
||||
|
||||
// Statement renders the PromQL query string after variable substitution. It
|
||||
// is used by the dry-run/preview path; PromQL queries do not have a
|
||||
// SQL-style argument list.
|
||||
func (q *promqlQuery) Statement(_ context.Context) (*qbv5.Statement, error) {
|
||||
rendered, err := q.renderVars(q.query.Query, q.vars, q.tr.From, q.tr.To)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &qbv5.Statement{Query: rendered}, nil
|
||||
}
|
||||
|
||||
// PreviewStatements returns the underlying ClickHouse statement(s) this PromQL
|
||||
// query would run, captured without executing them. PromQL is evaluated by the
|
||||
// Prometheus engine rather than compiled to one SQL statement: the engine calls
|
||||
// the storage adapter's Select per metric selector, which builds ClickHouse
|
||||
// SQL. We drive the engine with a capturing Storage that records that SQL and
|
||||
// returns empty results, so nothing is read from ClickHouse. Returns nil when
|
||||
// the provider does not support capture (e.g. test doubles).
|
||||
func (q *promqlQuery) PreviewStatements(ctx context.Context) ([]prometheus.CapturedStatement, error) {
|
||||
storer, ok := q.promEngine.(prometheus.StatementCapturer)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rendered, err := q.renderVars(q.query.Query, q.vars, q.tr.From, q.tr.To)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
start := int64(querybuilder.ToNanoSecs(q.tr.From))
|
||||
end := int64(querybuilder.ToNanoSecs(q.tr.To))
|
||||
|
||||
capStorage, recorder := storer.CapturingStorage()
|
||||
qry, err := q.promEngine.Engine().NewRangeQuery(
|
||||
ctx,
|
||||
capStorage,
|
||||
nil,
|
||||
rendered,
|
||||
time.Unix(0, start),
|
||||
time.Unix(0, end),
|
||||
q.query.Step.Duration,
|
||||
)
|
||||
if err != nil {
|
||||
if e := tryEnhancePromQLExecError(err); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return nil, enhancePromQLError(rendered, err)
|
||||
}
|
||||
defer qry.Close()
|
||||
|
||||
// Evaluate against the capturing storage: this drives a Select per selector
|
||||
// (recording the SQL) but reads no data, so the result is discarded.
|
||||
if res := qry.Exec(ctx); res.Err != nil {
|
||||
if e := tryEnhancePromQLExecError(res.Err); e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return nil, errors.Newf(errors.TypeInternal, errors.CodeInternal, "query execution error: %v", res.Err)
|
||||
}
|
||||
|
||||
return recorder.Statements(), nil
|
||||
}
|
||||
|
||||
func (q *promqlQuery) Execute(ctx context.Context) (*qbv5.Result, error) {
|
||||
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
|
||||
@@ -92,6 +92,11 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
req.Start = querybuilder.ToMilliSecs(req.Start)
|
||||
req.End = querybuilder.ToMilliSecs(req.End)
|
||||
|
||||
tmplVars := req.Variables
|
||||
if tmplVars == nil {
|
||||
tmplVars = make(map[string]qbtypes.VariableItem)
|
||||
}
|
||||
|
||||
event := &qbtypes.QBEvent{
|
||||
Version: "v5",
|
||||
NumberOfQueries: len(req.CompositeQuery.Queries),
|
||||
@@ -111,6 +116,9 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
// We need to set if it is unspecified or adjust it if value is not within recommended range
|
||||
intervalWarnings := q.adjustStepInterval(req.CompositeQuery.Queries, req.Start, req.End)
|
||||
|
||||
queries := make(map[string]qbtypes.Query)
|
||||
steps := make(map[string]qbtypes.Step)
|
||||
|
||||
missingMetricQueries, metricWarnings, err := q.resolveMetricMetadata(ctx, req.CompositeQuery.Queries, req.Start, req.End)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -120,64 +128,6 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
missingMetricQuerySet[name] = true
|
||||
}
|
||||
|
||||
queries, steps, err := q.buildQueries(req, dependencyQueries, missingMetricQuerySet, event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
preseededResults := make(map[string]any)
|
||||
for _, name := range missingMetricQueries {
|
||||
switch req.RequestType {
|
||||
case qbtypes.RequestTypeTimeSeries:
|
||||
preseededResults[name] = &qbtypes.TimeSeriesData{QueryName: name}
|
||||
case qbtypes.RequestTypeScalar:
|
||||
preseededResults[name] = &qbtypes.ScalarData{QueryName: name}
|
||||
case qbtypes.RequestTypeRaw:
|
||||
preseededResults[name] = &qbtypes.RawData{QueryName: name}
|
||||
}
|
||||
}
|
||||
qbResp, qbErr := q.run(ctx, orgID, queries, req, steps, event, preseededResults)
|
||||
if qbResp != nil {
|
||||
qbResp.QBEvent = event
|
||||
if len(intervalWarnings) != 0 && req.RequestType == qbtypes.RequestTypeTimeSeries {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{
|
||||
Warnings: make([]qbtypes.QueryWarnDataAdditional, len(intervalWarnings)),
|
||||
}
|
||||
for idx := range intervalWarnings {
|
||||
qbResp.Warning.Warnings[idx] = qbtypes.QueryWarnDataAdditional{Message: intervalWarnings[idx]}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(metricWarnings) > 0 {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{}
|
||||
}
|
||||
for _, w := range metricWarnings {
|
||||
qbResp.Warning.Warnings = append(qbResp.Warning.Warnings, qbtypes.QueryWarnDataAdditional{
|
||||
Message: w,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return qbResp, qbErr
|
||||
}
|
||||
|
||||
func (q *querier) buildQueries(
|
||||
req *qbtypes.QueryRangeRequest,
|
||||
dependencyQueries map[string]bool,
|
||||
missingMetricQuerySet map[string]bool,
|
||||
event *qbtypes.QBEvent,
|
||||
) (map[string]qbtypes.Query, map[string]qbtypes.Step, error) {
|
||||
|
||||
tmplVars := req.Variables
|
||||
if tmplVars == nil {
|
||||
tmplVars = make(map[string]qbtypes.VariableItem)
|
||||
}
|
||||
|
||||
queries := make(map[string]qbtypes.Query)
|
||||
steps := make(map[string]qbtypes.Step)
|
||||
|
||||
for _, query := range req.CompositeQuery.Queries {
|
||||
queryName := query.GetQueryName()
|
||||
|
||||
@@ -190,7 +140,7 @@ func (q *querier) buildQueries(
|
||||
case qbtypes.QueryTypePromQL:
|
||||
promQuery, ok := query.Spec.(qbtypes.PromQuery)
|
||||
if !ok {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid promql query spec %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid promql query spec %T", query.Spec)
|
||||
}
|
||||
promqlQuery := newPromqlQuery(q.logger, q.promEngine, promQuery, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
|
||||
queries[promQuery.Name] = promqlQuery
|
||||
@@ -198,14 +148,14 @@ func (q *querier) buildQueries(
|
||||
case qbtypes.QueryTypeClickHouseSQL:
|
||||
chQuery, ok := query.Spec.(qbtypes.ClickHouseQuery)
|
||||
if !ok {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid clickhouse query spec %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid clickhouse query spec %T", query.Spec)
|
||||
}
|
||||
chSQLQuery := newchSQLQuery(q.logger, q.telemetryStore, chQuery, nil, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
|
||||
queries[chQuery.Name] = chSQLQuery
|
||||
case qbtypes.QueryTypeTraceOperator:
|
||||
traceOpQuery, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator)
|
||||
if !ok {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid trace operator query spec %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid trace operator query spec %T", query.Spec)
|
||||
}
|
||||
toq := &traceOperatorQuery{
|
||||
telemetryStore: q.telemetryStore,
|
||||
@@ -258,12 +208,46 @@ func (q *querier) buildQueries(
|
||||
queries[spec.Name] = bq
|
||||
steps[spec.Name] = spec.StepInterval
|
||||
default:
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported builder spec type %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported builder spec type %T", query.Spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queries, steps, nil
|
||||
preseededResults := make(map[string]any)
|
||||
for _, name := range missingMetricQueries {
|
||||
switch req.RequestType {
|
||||
case qbtypes.RequestTypeTimeSeries:
|
||||
preseededResults[name] = &qbtypes.TimeSeriesData{QueryName: name}
|
||||
case qbtypes.RequestTypeScalar:
|
||||
preseededResults[name] = &qbtypes.ScalarData{QueryName: name}
|
||||
case qbtypes.RequestTypeRaw:
|
||||
preseededResults[name] = &qbtypes.RawData{QueryName: name}
|
||||
}
|
||||
}
|
||||
qbResp, qbErr := q.run(ctx, orgID, queries, req, steps, event, preseededResults)
|
||||
if qbResp != nil {
|
||||
qbResp.QBEvent = event
|
||||
if len(intervalWarnings) != 0 && req.RequestType == qbtypes.RequestTypeTimeSeries {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{
|
||||
Warnings: make([]qbtypes.QueryWarnDataAdditional, len(intervalWarnings)),
|
||||
}
|
||||
for idx := range intervalWarnings {
|
||||
qbResp.Warning.Warnings[idx] = qbtypes.QueryWarnDataAdditional{Message: intervalWarnings[idx]}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(metricWarnings) > 0 {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{}
|
||||
}
|
||||
for _, w := range metricWarnings {
|
||||
qbResp.Warning.Warnings = append(qbResp.Warning.Warnings, qbtypes.QueryWarnDataAdditional{
|
||||
Message: w,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return qbResp, qbErr
|
||||
}
|
||||
|
||||
func (q *querier) populateQBEvent(event *qbtypes.QBEvent, queries []qbtypes.QueryEnvelope) {
|
||||
|
||||
@@ -32,12 +32,6 @@ func (q *traceOperatorQuery) Window() (uint64, uint64) {
|
||||
return q.fromMS, q.toMS
|
||||
}
|
||||
|
||||
// Statement renders the SQL statement for the trace operator query without
|
||||
// executing it. It is used by the dry-run/preview path.
|
||||
func (q *traceOperatorQuery) Statement(ctx context.Context) (*qbtypes.Statement, error) {
|
||||
return q.stmtBuilder.Build(ctx, q.fromMS, q.toMS, q.kind, q.spec, q.compositeQuery)
|
||||
}
|
||||
|
||||
func (q *traceOperatorQuery) Execute(ctx context.Context) (*qbtypes.Result, error) {
|
||||
stmt, err := q.stmtBuilder.Build(
|
||||
ctx,
|
||||
|
||||
@@ -1678,6 +1678,15 @@ func (aH *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
|
||||
Route: "",
|
||||
})
|
||||
|
||||
aiObservability := aH.Signoz.Flagger.BooleanOrEmpty(r.Context(), flagger.FeatureEnableAIObservability, evalCtx)
|
||||
featureSet = append(featureSet, &licensetypes.Feature{
|
||||
Name: valuer.NewString(flagger.FeatureEnableAIObservability.String()),
|
||||
Active: aiObservability,
|
||||
Usage: 0,
|
||||
UsageLimit: -1,
|
||||
Route: "",
|
||||
})
|
||||
|
||||
if constants.IsDotMetricsEnabled {
|
||||
for idx, feature := range featureSet {
|
||||
if feature.Name == licensetypes.DotMetricsEnabled {
|
||||
|
||||
@@ -145,7 +145,7 @@ func PrepareWhereClause(query string, opts FilterExprVisitorOpts) (PreparedWhere
|
||||
"Found %d syntax errors while parsing the search expression.",
|
||||
len(parserErrorListener.SyntaxErrors),
|
||||
)
|
||||
additionals := make([]string, 0, len(parserErrorListener.SyntaxErrors))
|
||||
additionals := make([]string, len(parserErrorListener.SyntaxErrors))
|
||||
for _, err := range parserErrorListener.SyntaxErrors {
|
||||
if err.Error() != "" {
|
||||
additionals = append(additionals, err.Error())
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/swaggest/jsonschema-go"
|
||||
@@ -65,195 +64,6 @@ type QueryRangeResponse struct {
|
||||
QBEvent *QBEvent `json:"-"`
|
||||
}
|
||||
|
||||
// QueryRangePreviewResponse describes the dry-run output of a query range
|
||||
// request. CompositeQuery mirrors the request's compositeQuery: each entry is
|
||||
// the dry-run result for one query, keyed by the same query name the request
|
||||
// used.
|
||||
type QueryRangePreviewResponse struct {
|
||||
CompositeQuery map[string]QueryPreview `json:"compositeQuery"`
|
||||
}
|
||||
|
||||
// QueryRangePreviewOptions carries per-call options for the query range
|
||||
// preview (dry-run) endpoint. The zero value produces a lightweight,
|
||||
// verdict-only preview (valid/error/warnings per query, no rendered SQL).
|
||||
type QueryRangePreviewOptions struct {
|
||||
// Verbose is the single switch for the full preview, and the HTTP endpoint
|
||||
// defaults it to TRUE. When true, each rendered statement carries its EXPLAIN
|
||||
// ESTIMATE (PreviewStatement.Estimate) and granule index analysis
|
||||
// (PreviewStatement.Granules, including the per-index funnel), and the query
|
||||
// gets both headline scores (SelectivityScore and MagnitudeScore); the two
|
||||
// analyses cost one ClickHouse EXPLAIN per statement each. When false (set via
|
||||
// ?verbose=false) every query is still validated but the response is just the
|
||||
// per-query verdict, with no rendered SQL and no ClickHouse round trips.
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
// QueryRangePreviewParams documents the query-string parameters accepted by the
|
||||
// query range preview (dry-run) endpoint.
|
||||
type QueryRangePreviewParams struct {
|
||||
// Verbose defaults to "true": the full preview — the rendered ClickHouse
|
||||
// statement(s) with each statement's EXPLAIN ESTIMATE and granule index
|
||||
// analysis, plus the top-level selectivityScore and magnitudeScore. Set
|
||||
// verbose=false for the lightweight per-query verdict (valid/error/warnings)
|
||||
// with no rendered SQL and no ClickHouse round trips.
|
||||
Verbose string `query:"verbose"`
|
||||
}
|
||||
|
||||
// PrepareJSONSchema adds description to the QueryRangePreviewResponse schema.
|
||||
func (q *QueryRangePreviewResponse) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
schema.WithDescription("Response from the v5 query range preview (dry-run) endpoint. For each query in the composite query, returns the underlying ClickHouse statement(s) it renders to without executing them (one per PromQL metric selector; exactly one for builder/ClickHouse/trace-operator queries), with the optional EXPLAIN ESTIMATE and granule analysis attached per statement when requested.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// QueryPreview is the dry-run result for a single query, keyed by query name
|
||||
// in QueryRangePreviewResponse.CompositeQuery.
|
||||
type QueryPreview struct {
|
||||
// Valid is the headline verdict for this query: true when it previewed
|
||||
// without error, false when Error is set. It is always present (derived from
|
||||
// Error at marshal time) so an agent can branch on a single boolean instead
|
||||
// of testing for the presence of the error object.
|
||||
Valid bool `json:"valid"`
|
||||
// Error describes why this query is invalid or could not be previewed; nil
|
||||
// when the query previewed successfully. It is the structured form
|
||||
// (code, message, and — when available — suggestions and invalidReferences)
|
||||
// so an agent can act on it programmatically instead of parsing a string.
|
||||
Error error `json:"error,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
// SelectivityScore is the headline selectivity for this query: the percentage
|
||||
// (0-100) of candidate granules eliminated by partition, primary-key, and
|
||||
// skip-index pruning before any data is read (higher = less data read). It is
|
||||
// the minimum of the per-statement Statements[].Granules.SkipScore values —
|
||||
// the least-selective (worst) underlying statement, which dominates cost.
|
||||
// Returned only when the granules analysis ran (?granules=true or ?verbose=true)
|
||||
// and at least one statement reads a MergeTree table. Paired with
|
||||
// MagnitudeScore as the two headline score axes.
|
||||
SelectivityScore *float64 `json:"selectivityScore,omitempty"`
|
||||
// MagnitudeScore is the headline *cost* for this query (0-100; higher = less
|
||||
// data scanned = cheaper), a separate axis from SelectivityScore. Selectivity
|
||||
// is how good the index pruning *ratio* is, while MagnitudeScore reflects the
|
||||
// *absolute* rows the query would scan (from EXPLAIN ESTIMATE), since a query
|
||||
// can prune 99% of granules and still scan billions of rows on a huge table.
|
||||
// Derived on a log scale from the estimated rows of the heaviest statement.
|
||||
// Returned only when the estimate analysis ran (?estimate=true or ?verbose=true)
|
||||
// and at least one statement has an estimate. The two scores are kept separate
|
||||
// (not fused) so a caller can see which axis — selectivity or magnitude — is
|
||||
// the problem.
|
||||
MagnitudeScore *float64 `json:"magnitudeScore,omitempty"`
|
||||
// Statements are the underlying ClickHouse statement(s) this query renders to,
|
||||
// in execution order. Builder, ClickHouse SQL, and trace-operator queries
|
||||
// render exactly one; a PromQL query renders one per metric selector (the
|
||||
// Prometheus engine issues a statement per selector). Empty for a
|
||||
// validation-only preview, a query that failed to render (see Error), or one
|
||||
// that resolves to no data (a fully-missing metric, see Warnings).
|
||||
Statements []PreviewStatement `json:"statements,omitempty"`
|
||||
}
|
||||
|
||||
// PreviewStatement is one rendered ClickHouse statement the query will execute,
|
||||
// with its bound args and — when requested — its EXPLAIN ESTIMATE (Estimate) and
|
||||
// granule breakdown (Granules). The query/args field names follow the
|
||||
// OpenTelemetry db.statement.* convention so an agent consuming the dry-run sees
|
||||
// the same keys it would on a span.
|
||||
type PreviewStatement struct {
|
||||
Query string `json:"db.statement.query"`
|
||||
Args []any `json:"db.statement.args,omitempty"`
|
||||
// Estimate is the parsed ClickHouse EXPLAIN ESTIMATE output, set only for
|
||||
// ?estimate=true (or ?verbose=true): one entry per table the statement reads,
|
||||
// each with the parts/rows/marks ClickHouse estimates it will scan. Parsed
|
||||
// into a struct (rather than the raw tab-separated table) so an agent can read
|
||||
// the absolute cost estimate programmatically — it complements the
|
||||
// ratio-based Granules.
|
||||
Estimate []EstimateEntry `json:"estimate,omitempty"`
|
||||
// Granules is the parsed granule-skip breakdown for this statement (candidate
|
||||
// vs. surviving granules and the resulting skip score). Populated only for
|
||||
// ?granules=true (or ?verbose=true) when the statement reads a MergeTree
|
||||
// table, so an agent can see why a statement is (un)selective, not just the
|
||||
// headline score.
|
||||
Granules *Granules `json:"granules,omitempty"`
|
||||
}
|
||||
|
||||
// EstimateEntry is ClickHouse's EXPLAIN ESTIMATE for one table the statement
|
||||
// reads: the parts, rows, and marks it estimates it will scan. Unlike Granules
|
||||
// (a pruning ratio), these are absolute counts, so they convey how much data a
|
||||
// statement touches in real terms.
|
||||
type EstimateEntry struct {
|
||||
Database string `json:"database"`
|
||||
Table string `json:"table"`
|
||||
Parts int64 `json:"parts"`
|
||||
Rows int64 `json:"rows"`
|
||||
Marks int64 `json:"marks"`
|
||||
}
|
||||
|
||||
// Granules is the granule-skip breakdown for one rendered statement, parsed from
|
||||
// ClickHouse's `EXPLAIN json = 1, indexes = 1` index analysis. Granules are the
|
||||
// unit of read in a MergeTree table; the fewer that survive pruning, the less
|
||||
// data the query reads. Summed across every ReadFromMergeTree node in the plan
|
||||
// so a multi-read statement is scored as a whole.
|
||||
type Granules struct {
|
||||
// Initial is the candidate granules before any pruning.
|
||||
Initial int64 `json:"initial"`
|
||||
// Selected is the granules surviving partition/primary-key/skip-index pruning
|
||||
// — the ones the query would actually read.
|
||||
Selected int64 `json:"selected"`
|
||||
// Skipped is Initial - Selected: granules eliminated before any read.
|
||||
Skipped int64 `json:"skipped"`
|
||||
// SkipScore is 100 * Skipped / Initial, rounded to two decimals (0-100;
|
||||
// higher = more selective).
|
||||
SkipScore float64 `json:"skipScore"`
|
||||
// Reads is the raw per-read index-pruning trace behind the aggregate above:
|
||||
// one entry per ReadFromMergeTree node in the plan, each listing the index
|
||||
// steps in the order ClickHouse applies them. It shows *which* index did the
|
||||
// pruning and which did nothing — a step whose selected == initial pruned no
|
||||
// granules (its index isn't engaging), and a read still selecting many
|
||||
// granules after every step is a candidate for a new index. Empty when the
|
||||
// plan exposes no MergeTree index analysis.
|
||||
Reads []MergeTreeRead `json:"reads,omitempty"`
|
||||
}
|
||||
|
||||
// MergeTreeRead is the index-pruning funnel for one ReadFromMergeTree node — one
|
||||
// physical read of one table. The Steps run in sequence, so each step's Initial*
|
||||
// matches the previous step's Selected*: the list reads as a funnel from
|
||||
// candidate parts/granules down to what survives and is actually read.
|
||||
type MergeTreeRead struct {
|
||||
// Table is the table this node reads, e.g. "signoz_logs.logs_v2".
|
||||
Table string `json:"table"`
|
||||
// Steps are the index steps applied to this read, in execution order.
|
||||
Steps []IndexStep `json:"steps"`
|
||||
}
|
||||
|
||||
// IndexStep is one index applied during a MergeTree read, with the parts and
|
||||
// granules entering it (Initial*) and surviving it (Selected*). Type is the
|
||||
// ClickHouse index kind (MinMax, Partition, PrimaryKey, or Skip); Name is set
|
||||
// for skip indexes; Keys/Condition describe what it matched on.
|
||||
type IndexStep struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
Condition string `json:"condition,omitempty"`
|
||||
InitialParts int64 `json:"initialParts"`
|
||||
SelectedParts int64 `json:"selectedParts"`
|
||||
InitialGranules int64 `json:"initialGranules"`
|
||||
SelectedGranules int64 `json:"selectedGranules"`
|
||||
}
|
||||
|
||||
// MarshalJSON renders Error as the structured error form (code, message and,
|
||||
// when present, suggestions/invalidReferences) instead of the default {} that a
|
||||
// bare error interface produces, so an agent consuming the dry-run can act on it
|
||||
// programmatically.
|
||||
func (p QueryPreview) MarshalJSON() ([]byte, error) {
|
||||
type alias QueryPreview
|
||||
out := struct {
|
||||
alias
|
||||
Error *errors.JSON `json:"error,omitempty"`
|
||||
}{alias: alias(p)}
|
||||
out.alias.Error = nil
|
||||
// Derive the verdict from the error so callers can't desync the two.
|
||||
out.Valid = p.Error == nil
|
||||
if p.Error != nil {
|
||||
out.Error = errors.AsJSON(p.Error)
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
var _ jsonschema.Preparer = &QueryRangeResponse{}
|
||||
|
||||
// PrepareJSONSchema adds description to the QueryRangeResponse schema.
|
||||
|
||||
@@ -575,82 +575,6 @@ func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRequestScope validates request-level invariants — time range,
|
||||
// request type, the raw/trace metric-query restriction, non-empty composite
|
||||
// query, unique builder query names, and not-all-disabled — WITHOUT validating
|
||||
// individual query specs, and returns the ValidationOptions for the request
|
||||
// type. The dry-run/preview path uses this so that per-query spec errors can be
|
||||
// attributed to each query (via QueryEnvelope.Validate) instead of aborting the
|
||||
// whole request on the first one, the way Validate does. The normal execution
|
||||
// path keeps using the fail-fast Validate.
|
||||
func (r *QueryRangeRequest) ValidateRequestScope() ([]ValidationOption, error) {
|
||||
if r.RequestType != RequestTypeRawStream && r.Start >= r.End {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "start time must be before end time")
|
||||
}
|
||||
|
||||
var opts []ValidationOption
|
||||
switch r.RequestType {
|
||||
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace, RequestTypeTimeSeries, RequestTypeScalar:
|
||||
opts = GetValidationOptions(r.RequestType)
|
||||
default:
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid request type: %s", r.RequestType).
|
||||
WithAdditional("Valid request types are: raw, timeseries, scalar")
|
||||
}
|
||||
|
||||
if r.RequestType == RequestTypeRaw || r.RequestType == RequestTypeRawStream || r.RequestType == RequestTypeTrace {
|
||||
for _, envelope := range r.CompositeQuery.Queries {
|
||||
if envelope.GetSignal() == telemetrytypes.SignalMetrics {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "raw request type is not supported for metric queries")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.CompositeQuery.Queries) == 0 {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "at least one query is required")
|
||||
}
|
||||
|
||||
// Builder query names must be unique across the composite query.
|
||||
queryNames := make(map[string]bool)
|
||||
for _, envelope := range r.CompositeQuery.Queries {
|
||||
if envelope.Type == QueryTypeBuilder || envelope.Type == QueryTypeSubQuery {
|
||||
name := envelope.GetQueryName()
|
||||
if name != "" {
|
||||
if queryNames[name] {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "duplicate query name '%s'", name)
|
||||
}
|
||||
queryNames[name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.validateAllQueriesNotDisabled(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// Validate parses and validates the preview (dry-run) query-string parameters
|
||||
// and returns the per-call options the preview engine consumes. Verbose defaults
|
||||
// to TRUE (the full preview) and accepts true/1/false/0; any other value is
|
||||
// rejected as invalid input.
|
||||
func (p *QueryRangePreviewParams) Validate() (QueryRangePreviewOptions, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(p.Verbose)) {
|
||||
case "", "true", "1":
|
||||
return QueryRangePreviewOptions{Verbose: true}, nil
|
||||
case "false", "0":
|
||||
return QueryRangePreviewOptions{Verbose: false}, nil
|
||||
}
|
||||
return QueryRangePreviewOptions{}, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid verbose value %q (allowed: true, false)", p.Verbose)
|
||||
}
|
||||
|
||||
// Validate validates a single query envelope's spec. It is the per-query
|
||||
// counterpart to QueryRangeRequest.ValidateRequestScope, used by the dry-run to
|
||||
// report each query's structural error independently.
|
||||
func (e QueryEnvelope) Validate(opts ...ValidationOption) error {
|
||||
return validateQueryEnvelope(e, opts...)
|
||||
}
|
||||
|
||||
// validateAllQueriesNotDisabled validates that at least one query in the composite query is enabled.
|
||||
func (r *QueryRangeRequest) validateAllQueriesNotDisabled() error {
|
||||
for _, envelope := range r.CompositeQuery.Queries {
|
||||
|
||||
Reference in New Issue
Block a user