mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-04 15:31:08 +00:00
Compare commits
3 Commits
test/qb-e2
...
nv/distr-q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16efc6a01c | ||
|
|
d36c4ba843 | ||
|
|
85ef5e8e3f |
@@ -9565,9 +9565,9 @@ components:
|
||||
Querybuildertypesv5AggregationMeta:
|
||||
properties:
|
||||
buckets:
|
||||
description: The ascending upper bounds of the value bands, set only for
|
||||
heatmap results; each point then carries one count per bound plus a trailing
|
||||
overflow count in values.
|
||||
description: The ascending upper bounds of the value buckets, set for heatmap
|
||||
and distribution results; each point then carries one count per bound
|
||||
plus a trailing overflow count in values.
|
||||
items:
|
||||
format: double
|
||||
type: number
|
||||
@@ -9722,6 +9722,56 @@ components:
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
Querybuildertypesv5Distribution:
|
||||
properties:
|
||||
counts:
|
||||
description: One count per bound in meta.buckets plus a trailing overflow
|
||||
count, summed over every step of the requested time range.
|
||||
items:
|
||||
format: double
|
||||
type: number
|
||||
nullable: true
|
||||
type: array
|
||||
labels:
|
||||
description: The group-by field values that identify this distribution.
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Label'
|
||||
type: array
|
||||
type: object
|
||||
Querybuildertypesv5DistributionAggregation:
|
||||
properties:
|
||||
alias:
|
||||
description: The alias of the aggregation, or the internal name __result_N,
|
||||
with N its index, when no alias was given.
|
||||
type: string
|
||||
distributions:
|
||||
description: One distribution per group-by combination.
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5Distribution'
|
||||
nullable: true
|
||||
type: array
|
||||
index:
|
||||
description: The zero-based position of the aggregation in the query.
|
||||
type: integer
|
||||
meta:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5AggregationMeta'
|
||||
description: Display metadata for the aggregation, including the bucket
|
||||
upper bounds the counts are positional against.
|
||||
type: object
|
||||
Querybuildertypesv5DistributionData:
|
||||
properties:
|
||||
aggregations:
|
||||
description: One entry per aggregation of the query. The order is not guaranteed;
|
||||
match entries to aggregations by index.
|
||||
items:
|
||||
$ref: '#/components/schemas/Querybuildertypesv5DistributionAggregation'
|
||||
nullable: true
|
||||
type: array
|
||||
queryName:
|
||||
description: The name of the query that produced this result.
|
||||
type: string
|
||||
type: object
|
||||
Querybuildertypesv5ExecStats:
|
||||
description: 'Execution cost of the request: rows and bytes scanned and execution
|
||||
time. A measure of cost, not of whether data exists.'
|
||||
@@ -10545,10 +10595,12 @@ components:
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5TimeSeriesData'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5ScalarData'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5RawData'
|
||||
- $ref: '#/components/schemas/Querybuildertypesv5DistributionData'
|
||||
properties:
|
||||
results:
|
||||
description: 'One result per enabled query, typed by the request type: TimeSeriesData
|
||||
for time_series, ScalarData for scalar and RawData for raw requests.'
|
||||
for time_series and heatmap, ScalarData for scalar, RawData for raw and
|
||||
DistributionData for distribution requests.'
|
||||
items: {}
|
||||
nullable: true
|
||||
type: array
|
||||
@@ -10738,8 +10790,9 @@ components:
|
||||
type: object
|
||||
Querybuildertypesv5QueryRangeResponse:
|
||||
description: 'Response from the v5 query range endpoint. The data.results array
|
||||
contains typed results depending on the requestType: TimeSeriesData for time_series,
|
||||
ScalarData for scalar, or RawData for raw requests.'
|
||||
contains typed results depending on the requestType: TimeSeriesData for time_series
|
||||
and heatmap, ScalarData for scalar, RawData for raw, or DistributionData for
|
||||
distribution requests.'
|
||||
properties:
|
||||
data:
|
||||
allOf:
|
||||
@@ -10836,6 +10889,7 @@ components:
|
||||
- raw_stream
|
||||
- trace
|
||||
- heatmap
|
||||
- distribution
|
||||
type: string
|
||||
Querybuildertypesv5ScalarData:
|
||||
properties:
|
||||
@@ -10947,8 +11001,8 @@ components:
|
||||
format: double
|
||||
type: number
|
||||
values:
|
||||
description: The per-band counts for heatmap results, in the order of meta.buckets
|
||||
plus a trailing overflow count.
|
||||
description: The per-bucket counts for heatmap results, in the order of
|
||||
meta.buckets plus a trailing overflow count.
|
||||
items:
|
||||
format: double
|
||||
type: number
|
||||
|
||||
@@ -6941,6 +6941,7 @@ export enum Querybuildertypesv5RequestTypeDTO {
|
||||
raw_stream = 'raw_stream',
|
||||
trace = 'trace',
|
||||
heatmap = 'heatmap',
|
||||
distribution = 'distribution',
|
||||
}
|
||||
/**
|
||||
* The query kind.
|
||||
@@ -10977,7 +10978,7 @@ export interface Querybuildertypesv5TimeSeriesValueDTO {
|
||||
value?: number;
|
||||
/**
|
||||
* @type array
|
||||
* @description The per-band counts for heatmap results, in the order of meta.buckets plus a trailing overflow count.
|
||||
* @description The per-bucket counts for heatmap results, in the order of meta.buckets plus a trailing overflow count.
|
||||
*/
|
||||
values?: number[];
|
||||
}
|
||||
@@ -11598,7 +11599,7 @@ export interface PromotetypesPostableIndexMaterializedPathsDTO {
|
||||
export interface Querybuildertypesv5AggregationMetaDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @description The ascending upper bounds of the value bands, set only for heatmap results; each point then carries one count per bound plus a trailing overflow count in values.
|
||||
* @description The ascending upper bounds of the value buckets, set for heatmap and distribution results; each point then carries one count per bound plus a trailing overflow count in values.
|
||||
*/
|
||||
buckets?: number[];
|
||||
/**
|
||||
@@ -11719,6 +11720,54 @@ export interface Querybuildertypesv5ColumnDescriptorDTO {
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5DistributionDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One count per bound in meta.buckets plus a trailing overflow count, summed over every step of the requested time range.
|
||||
*/
|
||||
counts?: number[] | null;
|
||||
/**
|
||||
* @type array
|
||||
* @description The group-by field values that identify this distribution.
|
||||
*/
|
||||
labels?: Querybuildertypesv5LabelDTO[];
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5DistributionAggregationDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @description The alias of the aggregation, or the internal name __result_N, with N its index, when no alias was given.
|
||||
*/
|
||||
alias?: string;
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One distribution per group-by combination.
|
||||
*/
|
||||
distributions?: Querybuildertypesv5DistributionDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @description The zero-based position of the aggregation in the query.
|
||||
*/
|
||||
index?: number;
|
||||
/**
|
||||
* @description Display metadata for the aggregation, including the bucket upper bounds the counts are positional against.
|
||||
*/
|
||||
meta?: Querybuildertypesv5AggregationMetaDTO;
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5DistributionDataDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One entry per aggregation of the query. The order is not guaranteed; match entries to aggregations by index.
|
||||
*/
|
||||
aggregations?: Querybuildertypesv5DistributionAggregationDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
* @description The name of the query that produced this result.
|
||||
*/
|
||||
queryName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The step interval in seconds that was used for each query, keyed by query name.
|
||||
*/
|
||||
@@ -11974,21 +12023,28 @@ export type Querybuildertypesv5QueryDataDTO =
|
||||
| (Querybuildertypesv5TimeSeriesDataDTO & {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series, ScalarData for scalar and RawData for raw requests.
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
|
||||
*/
|
||||
results?: unknown[] | null;
|
||||
})
|
||||
| (Querybuildertypesv5ScalarDataDTO & {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series, ScalarData for scalar and RawData for raw requests.
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
|
||||
*/
|
||||
results?: unknown[] | null;
|
||||
})
|
||||
| (Querybuildertypesv5RawDataDTO & {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series, ScalarData for scalar and RawData for raw requests.
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
|
||||
*/
|
||||
results?: unknown[] | null;
|
||||
})
|
||||
| (Querybuildertypesv5DistributionDataDTO & {
|
||||
/**
|
||||
* @type array,null
|
||||
* @description One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests.
|
||||
*/
|
||||
results?: unknown[] | null;
|
||||
});
|
||||
@@ -12122,7 +12178,7 @@ export interface Querybuildertypesv5QueryRangeRequestDTO {
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series, ScalarData for scalar, or RawData for raw requests.
|
||||
* Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, or DistributionData for distribution requests.
|
||||
*/
|
||||
export interface Querybuildertypesv5QueryRangeResponseDTO {
|
||||
/**
|
||||
|
||||
@@ -481,8 +481,7 @@ func (bc *bucketCache) mergeBuckets(ctx context.Context, buckets []*qbtypes.Cach
|
||||
|
||||
// Merge values based on type
|
||||
var mergedValue any
|
||||
switch resultType {
|
||||
case qbtypes.RequestTypeTimeSeries, qbtypes.RequestTypeHeatmap:
|
||||
if resultType.IsBuiltFromTimeSeries() {
|
||||
mergedValue = bc.mergeTimeSeriesValues(ctx, buckets)
|
||||
// Raw and Scalar types are not cached, so no merge needed
|
||||
}
|
||||
@@ -629,8 +628,8 @@ func (bc *bucketCache) isEmptyResult(result *qbtypes.Result) (isEmpty bool, isFi
|
||||
return true, false
|
||||
}
|
||||
|
||||
switch result.Type {
|
||||
case qbtypes.RequestTypeTimeSeries, qbtypes.RequestTypeHeatmap:
|
||||
switch {
|
||||
case result.Type.IsBuiltFromTimeSeries():
|
||||
if tsData, ok := result.Value.(*qbtypes.TimeSeriesData); ok {
|
||||
// No aggregations at all means truly empty
|
||||
if len(tsData.Aggregations) == 0 {
|
||||
@@ -665,7 +664,7 @@ func (bc *bucketCache) isEmptyResult(result *qbtypes.Result) (isEmpty bool, isFi
|
||||
return !hasValues, !hasValues && totalSeries > 0
|
||||
}
|
||||
|
||||
case qbtypes.RequestTypeRaw, qbtypes.RequestTypeScalar, qbtypes.RequestTypeTrace:
|
||||
default:
|
||||
// Raw and scalar data are not cached
|
||||
return true, false
|
||||
}
|
||||
@@ -768,8 +767,8 @@ func (bc *bucketCache) trimResultToFluxBoundary(result *qbtypes.Result, fluxBoun
|
||||
Warnings: result.Warnings,
|
||||
}
|
||||
|
||||
switch result.Type {
|
||||
case qbtypes.RequestTypeTimeSeries, qbtypes.RequestTypeHeatmap:
|
||||
switch {
|
||||
case result.Type.IsBuiltFromTimeSeries():
|
||||
// Trim time series data
|
||||
if tsData, ok := result.Value.(*qbtypes.TimeSeriesData); ok && tsData != nil {
|
||||
trimmedData := &qbtypes.TimeSeriesData{}
|
||||
@@ -812,7 +811,7 @@ func (bc *bucketCache) trimResultToFluxBoundary(result *qbtypes.Result, fluxBoun
|
||||
trimmedResult.Value = trimmedData
|
||||
}
|
||||
|
||||
case qbtypes.RequestTypeRaw, qbtypes.RequestTypeScalar, qbtypes.RequestTypeTrace:
|
||||
default:
|
||||
// Don't cache raw or scalar data
|
||||
return nil
|
||||
}
|
||||
@@ -854,8 +853,8 @@ func (bc *bucketCache) filterResultToTimeRange(result *qbtypes.Result, q qbtypes
|
||||
}
|
||||
}
|
||||
|
||||
switch result.Type {
|
||||
case qbtypes.RequestTypeTimeSeries, qbtypes.RequestTypeHeatmap:
|
||||
switch {
|
||||
case result.Type.IsBuiltFromTimeSeries():
|
||||
if tsData, ok := result.Value.(*qbtypes.TimeSeriesData); ok {
|
||||
filteredData := &qbtypes.TimeSeriesData{
|
||||
Aggregations: make([]*qbtypes.AggregationBucket, 0, len(tsData.Aggregations)),
|
||||
|
||||
@@ -428,10 +428,10 @@ func (q *builderQuery[T]) narrowWindowByTraceID(ctx context.Context, fromMS, toM
|
||||
// emptyResultFor returns an empty result payload appropriate for the given kind.
|
||||
func emptyResultFor(kind qbtypes.RequestType, queryName string) *qbtypes.Result {
|
||||
var value any
|
||||
switch kind {
|
||||
case qbtypes.RequestTypeTimeSeries, qbtypes.RequestTypeHeatmap:
|
||||
switch {
|
||||
case kind.IsBuiltFromTimeSeries():
|
||||
value = &qbtypes.TimeSeriesData{QueryName: queryName}
|
||||
case qbtypes.RequestTypeScalar:
|
||||
case kind == qbtypes.RequestTypeScalar:
|
||||
value = &qbtypes.ScalarData{QueryName: queryName}
|
||||
default:
|
||||
value = &qbtypes.RawData{QueryName: queryName}
|
||||
@@ -484,7 +484,7 @@ func (q *builderQuery[T]) executeWithContext(ctx context.Context, query string,
|
||||
kind := q.kind
|
||||
// all metric queries are time series then reduced if required, except
|
||||
// heatmaps, whose statement returns a row per bucket rather than per point
|
||||
if q.spec.Signal == telemetrytypes.SignalMetrics && kind != qbtypes.RequestTypeHeatmap {
|
||||
if q.spec.Signal == telemetrytypes.SignalMetrics && !kind.CountsIntoBuckets() {
|
||||
kind = qbtypes.RequestTypeTimeSeries
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ func labelValue(val any) string {
|
||||
// * Time-series - *qbtypes.TimeSeriesData
|
||||
// * Scalar - *qbtypes.ScalarData
|
||||
// * Raw - *qbtypes.RawData
|
||||
// * Distribution- *qbtypes.DistributionData.
|
||||
// * Heatmap and distribution - *qbtypes.TimeSeriesData with one count per bucket at each point.
|
||||
func consume(rows driver.Rows, kind qbtypes.RequestType, queryWindow *qbtypes.TimeRange, step qbtypes.Step, queryName string) (any, error) {
|
||||
var (
|
||||
payload any
|
||||
@@ -94,7 +94,7 @@ func consume(rows driver.Rows, kind qbtypes.RequestType, queryWindow *qbtypes.Ti
|
||||
payload, err = readAsTimeSeries(rows, queryWindow, step, queryName)
|
||||
case qbtypes.RequestTypeScalar:
|
||||
payload, err = readAsScalar(rows, queryName)
|
||||
case qbtypes.RequestTypeHeatmap:
|
||||
case qbtypes.RequestTypeHeatmap, qbtypes.RequestTypeDistribution:
|
||||
payload, err = readAsHeatmap(rows, queryWindow, step, queryName)
|
||||
case qbtypes.RequestTypeRaw, qbtypes.RequestTypeTrace, qbtypes.RequestTypeRawStream:
|
||||
payload, err = readAsRaw(rows, queryName)
|
||||
|
||||
@@ -98,6 +98,15 @@ func (q *querier) postProcessResults(ctx context.Context, orgID valuer.UUID, res
|
||||
// Filter out disabled queries
|
||||
typedResults = q.filterDisabledQueries(typedResults, req)
|
||||
|
||||
// Summed only once the bucket axis is final.
|
||||
if req.RequestType == qbtypes.RequestTypeDistribution {
|
||||
for _, result := range typedResults {
|
||||
if tsData, ok := result.Value.(*qbtypes.TimeSeriesData); ok {
|
||||
result.Value = tsData.ToDistributionData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply table formatting for UI if requested
|
||||
if req.FormatOptions != nil && req.FormatOptions.FormatTableResultForUI && req.RequestType == qbtypes.RequestTypeScalar {
|
||||
|
||||
@@ -225,7 +234,7 @@ func postProcessMetricQuery(
|
||||
}
|
||||
}
|
||||
|
||||
if req.RequestType == qbtypes.RequestTypeHeatmap && config.HeatmapBucketing != nil {
|
||||
if req.RequestType.CountsIntoBuckets() && config.HeatmapBucketing != nil {
|
||||
if tsData, ok := result.Value.(*qbtypes.TimeSeriesData); ok {
|
||||
resolveHeatmapBucketAxis(tsData, *config.HeatmapBucketing)
|
||||
}
|
||||
@@ -360,7 +369,7 @@ func (q *querier) applyFormulas(ctx context.Context, results map[string]*qbtypes
|
||||
result = q.applySeriesLimit(result, formula.Limit, formula.Order)
|
||||
results[name] = result
|
||||
}
|
||||
case qbtypes.RequestTypeHeatmap:
|
||||
case qbtypes.RequestTypeHeatmap, qbtypes.RequestTypeDistribution:
|
||||
// The queries a formula reads were run as time series, so what
|
||||
// arrives here is one value per group per timestamp.
|
||||
result := q.processTimeSeriesFormula(ctx, results, formula, req)
|
||||
|
||||
@@ -134,9 +134,7 @@ func newPromqlQuery(
|
||||
}
|
||||
|
||||
func (q *promqlQuery) Fingerprint() string {
|
||||
switch q.requestType {
|
||||
case qbv5.RequestTypeTimeSeries, qbv5.RequestTypeHeatmap:
|
||||
default:
|
||||
if !q.requestType.IsBuiltFromTimeSeries() {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -358,7 +356,7 @@ func collectExecStats(began time.Time, statsMu *sync.Mutex, rowsScanned, bytesSc
|
||||
}
|
||||
|
||||
func (q *promqlQuery) toResult(matrix promql.Matrix, warnings []string, began time.Time, statsMu *sync.Mutex, rowsScanned, bytesScanned *uint64) (*qbv5.Result, error) {
|
||||
if q.requestType == qbv5.RequestTypeHeatmap {
|
||||
if q.requestType.CountsIntoBuckets() {
|
||||
return q.toResultForHeatmap(matrix, warnings, began, statsMu, rowsScanned, bytesScanned)
|
||||
}
|
||||
return q.toResultForTimeSeriesAndScalar(matrix, warnings, began, statsMu, rowsScanned, bytesScanned), nil
|
||||
|
||||
@@ -151,12 +151,12 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
|
||||
preseededResults := make(map[string]any)
|
||||
for _, name := range missingMetricQueries {
|
||||
switch req.RequestType {
|
||||
case qbtypes.RequestTypeTimeSeries, qbtypes.RequestTypeHeatmap:
|
||||
switch {
|
||||
case req.RequestType.IsBuiltFromTimeSeries():
|
||||
preseededResults[name] = &qbtypes.TimeSeriesData{QueryName: name}
|
||||
case qbtypes.RequestTypeScalar:
|
||||
case req.RequestType == qbtypes.RequestTypeScalar:
|
||||
preseededResults[name] = &qbtypes.ScalarData{QueryName: name}
|
||||
case qbtypes.RequestTypeRaw:
|
||||
case req.RequestType == qbtypes.RequestTypeRaw:
|
||||
preseededResults[name] = &qbtypes.RawData{QueryName: name}
|
||||
}
|
||||
}
|
||||
@@ -281,7 +281,7 @@ func (q *querier) buildQueries(
|
||||
continue
|
||||
}
|
||||
requestType := req.RequestType
|
||||
if requestType == qbtypes.RequestTypeHeatmap && spec.Disabled {
|
||||
if requestType.CountsIntoBuckets() && spec.Disabled {
|
||||
// A disabled query in a heatmap request feeds a formula, and the
|
||||
// formula converts time series into heatmap data, so its inputs
|
||||
// run as time series queries.
|
||||
@@ -432,7 +432,7 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, orgID valuer.UUID,
|
||||
}
|
||||
// Only the enabled query is used to render the heatmap, so bucket
|
||||
// options are only applied to the enabled query.
|
||||
if requestType == qbtypes.RequestTypeHeatmap && !spec.Disabled {
|
||||
if requestType.CountsIntoBuckets() && !spec.Disabled {
|
||||
if err := spec.Aggregations[i].VerifyAndApplyBucketOptions(spec.BucketOptions); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -655,16 +655,16 @@ func (q *querier) run(
|
||||
if result == nil || result.Value == nil {
|
||||
return false
|
||||
}
|
||||
switch result.Type {
|
||||
case qbtypes.RequestTypeScalar:
|
||||
switch {
|
||||
case result.Type == qbtypes.RequestTypeScalar:
|
||||
if val, ok := result.Value.(*qbtypes.ScalarData); ok && val != nil {
|
||||
return len(val.Data) != 0
|
||||
}
|
||||
case qbtypes.RequestTypeRaw:
|
||||
case result.Type == qbtypes.RequestTypeRaw:
|
||||
if val, ok := result.Value.(*qbtypes.RawData); ok && val != nil {
|
||||
return len(val.Rows) != 0
|
||||
}
|
||||
case qbtypes.RequestTypeTimeSeries, qbtypes.RequestTypeHeatmap:
|
||||
case result.Type.IsBuiltFromTimeSeries():
|
||||
if val, ok := result.Value.(*qbtypes.TimeSeriesData); ok && val != nil {
|
||||
if len(val.Aggregations) != 0 {
|
||||
anyNonEmpty := false
|
||||
@@ -984,8 +984,7 @@ func (q *querier) mergeResults(cached *qbtypes.Result, fresh []*qbtypes.Result)
|
||||
}
|
||||
|
||||
// Merge all fresh results including the first one
|
||||
switch merged.Type {
|
||||
case qbtypes.RequestTypeTimeSeries, qbtypes.RequestTypeHeatmap:
|
||||
if merged.Type.IsBuiltFromTimeSeries() {
|
||||
// Pass nil as cached value to ensure proper merging of all fresh results
|
||||
merged.Value = q.mergeTimeSeriesResults(nil, fresh)
|
||||
}
|
||||
@@ -1007,8 +1006,7 @@ func (q *querier) mergeResults(cached *qbtypes.Result, fresh []*qbtypes.Result)
|
||||
return merged
|
||||
}
|
||||
|
||||
switch merged.Type {
|
||||
case qbtypes.RequestTypeTimeSeries, qbtypes.RequestTypeHeatmap:
|
||||
if merged.Type.IsBuiltFromTimeSeries() {
|
||||
merged.Value = q.mergeTimeSeriesResults(cached.Value.(*qbtypes.TimeSeriesData), fresh)
|
||||
}
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ func rewriteQueryForHistogramCTE(requestType qbtypes.RequestType, query qbtypes.
|
||||
})
|
||||
|
||||
query.Aggregations = slices.Clone(query.Aggregations)
|
||||
if query.Aggregations[0].SpaceAggregation.IsPercentile() && requestType != qbtypes.RequestTypeHeatmap {
|
||||
if query.Aggregations[0].SpaceAggregation.IsPercentile() && !requestType.CountsIntoBuckets() {
|
||||
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationRate
|
||||
} else {
|
||||
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationIncrease
|
||||
@@ -818,7 +818,7 @@ func (b *StatementBuilder) BuildFinalSelect(
|
||||
args = append(args, a...)
|
||||
}
|
||||
|
||||
if requestType == qbtypes.RequestTypeHeatmap {
|
||||
if requestType.CountsIntoBuckets() {
|
||||
return buildHeatmapFinalSelect(combined, args, query)
|
||||
}
|
||||
return buildAggregationFinalSelect(combined, args, query)
|
||||
|
||||
@@ -526,7 +526,7 @@ func TestInvalidateUnknownPluginKind(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHeatmapPanelQueryKinds pins the panel allowlist to what validateHeatmap
|
||||
// TestHeatmapPanelQueryKinds pins the panel allowlist to what validateBucketCounting
|
||||
// accepts in querybuildertypesv5: everything but a trace operator.
|
||||
func TestHeatmapPanelQueryKinds(t *testing.T) {
|
||||
testCases := []struct {
|
||||
|
||||
@@ -81,7 +81,7 @@ func (b *BucketOptions) ToHeatmapBucketing() HeatmapBucketing {
|
||||
return resolved
|
||||
}
|
||||
|
||||
// This cannot be called in validateHeatmap cuz metric type is resolved in querier.go.
|
||||
// This cannot be called in validateBucketCounting cuz metric type is resolved in querier.go.
|
||||
func (a *MetricAggregation) VerifyAndApplyBucketOptions(bucketOptions *BucketOptions) error {
|
||||
switch a.Type {
|
||||
case metrictypes.HistogramType:
|
||||
@@ -298,6 +298,31 @@ func addHeatmapBucketsWithNoCountsForAggregation(aggBucket *AggregationBucket, b
|
||||
aggBucket.Meta.Buckets = denseUpperBounds
|
||||
}
|
||||
|
||||
// ToDistributionData sums every series over all its steps, keeping the bucket
|
||||
// axis. A partial step counts too, since what it holds was observed.
|
||||
func (t *TimeSeriesData) ToDistributionData() *DistributionData {
|
||||
data := &DistributionData{QueryName: t.QueryName, Aggregations: make([]*DistributionAggregation, 0, len(t.Aggregations))}
|
||||
for _, aggBucket := range t.Aggregations {
|
||||
if aggBucket == nil {
|
||||
continue
|
||||
}
|
||||
aggregation := &DistributionAggregation{Index: aggBucket.Index, Alias: aggBucket.Alias, Meta: aggBucket.Meta, Distributions: make([]*Distribution, 0, len(aggBucket.Series))}
|
||||
for _, series := range aggBucket.Series {
|
||||
counts := make([]float64, len(aggBucket.Meta.Buckets)+1)
|
||||
for _, point := range series.Values {
|
||||
for i, count := range point.Values {
|
||||
if i < len(counts) {
|
||||
counts[i] += count
|
||||
}
|
||||
}
|
||||
}
|
||||
aggregation.Distributions = append(aggregation.Distributions, &Distribution{Labels: series.Labels, Counts: counts})
|
||||
}
|
||||
data.Aggregations = append(data.Aggregations, aggregation)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// calculateIndexOfUpperBound and calculateUpperBoundAtIndex are inverses over
|
||||
// the axis being returned, so they read h.LogScale rather than the MaxLogScale
|
||||
// ClickHouse bucketed at: k * maxValue / numBuckets on a linear axis,
|
||||
|
||||
@@ -39,7 +39,8 @@ var (
|
||||
RequestTypeRawStream = RequestType{valuer.NewString("raw_stream")}
|
||||
// [][]any, Specialized SQL result set, paginated.
|
||||
RequestTypeTrace = RequestType{valuer.NewString("trace")}
|
||||
// []Bucket (struct{Lower,Upper,Count float64}), example: histogram.
|
||||
// DistributionData: a heatmap summed over its steps, one set of counts per
|
||||
// group against the aggregation's bucket upper bounds, example: histogram.
|
||||
RequestTypeDistribution = RequestType{valuer.NewString("distribution")}
|
||||
// TimeSeriesData carrying one count per histogram bucket at each timestamp,
|
||||
// with the shared bucket upper bounds on the aggregation's meta.
|
||||
@@ -55,6 +56,14 @@ func (r RequestType) IsAggregation() bool {
|
||||
return r == RequestTypeTimeSeries || r == RequestTypeScalar || r == RequestTypeDistribution || r == RequestTypeHeatmap
|
||||
}
|
||||
|
||||
func (r RequestType) CountsIntoBuckets() bool {
|
||||
return r == RequestTypeHeatmap || r == RequestTypeDistribution
|
||||
}
|
||||
|
||||
func (r RequestType) IsBuiltFromTimeSeries() bool {
|
||||
return r == RequestTypeTimeSeries || r.CountsIntoBuckets()
|
||||
}
|
||||
|
||||
// Enum implements jsonschema.Enum; returns the acceptable values for RequestType.
|
||||
func (RequestType) Enum() []any {
|
||||
return []any{
|
||||
@@ -64,6 +73,6 @@ func (RequestType) Enum() []any {
|
||||
RequestTypeRawStream,
|
||||
RequestTypeTrace,
|
||||
RequestTypeHeatmap,
|
||||
// RequestTypeDistribution,
|
||||
RequestTypeDistribution,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ type QueryWarnDataAdditional struct {
|
||||
}
|
||||
|
||||
type QueryData struct {
|
||||
Results []any `json:"results" description:"One result per enabled query, typed by the request type: TimeSeriesData for time_series, ScalarData for scalar and RawData for raw requests."`
|
||||
Results []any `json:"results" description:"One result per enabled query, typed by the request type: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw and DistributionData for distribution requests."`
|
||||
}
|
||||
|
||||
var _ jsonschema.OneOfExposer = QueryData{}
|
||||
@@ -53,6 +53,7 @@ func (QueryData) JSONSchemaOneOf() []any {
|
||||
TimeSeriesData{},
|
||||
ScalarData{},
|
||||
RawData{},
|
||||
DistributionData{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +129,7 @@ var _ jsonschema.Preparer = &QueryRangeResponse{}
|
||||
|
||||
// PrepareJSONSchema adds description to the QueryRangeResponse schema.
|
||||
func (q *QueryRangeResponse) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
schema.WithDescription("Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series, ScalarData for scalar, or RawData for raw requests.")
|
||||
schema.WithDescription("Response from the v5 query range endpoint. The data.results array contains typed results depending on the requestType: TimeSeriesData for time_series and heatmap, ScalarData for scalar, RawData for raw, or DistributionData for distribution requests.")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -258,7 +259,7 @@ func (a *AggregationBucket) TrimAxisToCountedBuckets() {
|
||||
|
||||
type AggregationMeta struct {
|
||||
Unit string `json:"unit,omitempty" description:"The unit of the aggregated values, taken from metric metadata when known."`
|
||||
Buckets []float64 `json:"buckets,omitempty" description:"The ascending upper bounds of the value bands, set only for heatmap results; each point then carries one count per bound plus a trailing overflow count in values."`
|
||||
Buckets []float64 `json:"buckets,omitempty" description:"The ascending upper bounds of the value buckets, set for heatmap and distribution results; each point then carries one count per bound plus a trailing overflow count in values."`
|
||||
}
|
||||
|
||||
type TimeSeries struct {
|
||||
@@ -375,7 +376,26 @@ type TimeSeriesValue struct {
|
||||
|
||||
Partial bool `json:"partial,omitempty" description:"True when the step is only partly covered by the requested time range, so the value is incomplete."`
|
||||
|
||||
Values []float64 `json:"values,omitempty" description:"The per-band counts for heatmap results, in the order of meta.buckets plus a trailing overflow count."`
|
||||
Values []float64 `json:"values,omitempty" description:"The per-bucket counts for heatmap results, in the order of meta.buckets plus a trailing overflow count."`
|
||||
}
|
||||
|
||||
// DistributionData is a heatmap summed over its steps: one set of counts per
|
||||
// group, positional against the aggregation's bucket upper bounds.
|
||||
type DistributionData struct {
|
||||
QueryName string `json:"queryName" description:"The name of the query that produced this result."`
|
||||
Aggregations []*DistributionAggregation `json:"aggregations" description:"One entry per aggregation of the query. The order is not guaranteed; match entries to aggregations by index."`
|
||||
}
|
||||
|
||||
type DistributionAggregation struct {
|
||||
Index int `json:"index" description:"The zero-based position of the aggregation in the query."`
|
||||
Alias string `json:"alias" description:"The alias of the aggregation, or the internal name __result_N, with N its index, when no alias was given."`
|
||||
Meta AggregationMeta `json:"meta,omitempty" description:"Display metadata for the aggregation, including the bucket upper bounds the counts are positional against."`
|
||||
Distributions []*Distribution `json:"distributions" description:"One distribution per group-by combination."`
|
||||
}
|
||||
|
||||
type Distribution struct {
|
||||
Labels []*Label `json:"labels,omitempty" description:"The group-by field values that identify this distribution."`
|
||||
Counts []float64 `json:"counts" description:"One count per bound in meta.buckets plus a trailing overflow count, summed over every step of the requested time range."`
|
||||
}
|
||||
|
||||
type ColumnType struct {
|
||||
|
||||
@@ -584,7 +584,7 @@ func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
|
||||
|
||||
// Validate request type
|
||||
switch r.RequestType {
|
||||
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace, RequestTypeTimeSeries, RequestTypeScalar, RequestTypeHeatmap:
|
||||
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace, RequestTypeTimeSeries, RequestTypeScalar, RequestTypeHeatmap, RequestTypeDistribution:
|
||||
opts = append(opts, GetValidationOptions(r.RequestType)...)
|
||||
default:
|
||||
return errors.NewInvalidInputf(
|
||||
@@ -592,11 +592,11 @@ func (r *QueryRangeRequest) Validate(opts ...ValidationOption) error {
|
||||
"invalid request type: %s",
|
||||
r.RequestType,
|
||||
).WithAdditional(
|
||||
"Valid request types are: raw, timeseries, scalar, heatmap",
|
||||
"Valid request types are: raw, timeseries, scalar, heatmap, distribution",
|
||||
)
|
||||
}
|
||||
|
||||
if err := r.validateHeatmap(); err != nil {
|
||||
if err := r.validateBucketCounting(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -637,14 +637,14 @@ func (r *QueryRangeRequest) ValidateRequestScope() ([]ValidationOption, error) {
|
||||
|
||||
var opts []ValidationOption
|
||||
switch r.RequestType {
|
||||
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace, RequestTypeTimeSeries, RequestTypeScalar, RequestTypeHeatmap:
|
||||
case RequestTypeRaw, RequestTypeRawStream, RequestTypeTrace, RequestTypeTimeSeries, RequestTypeScalar, RequestTypeHeatmap, RequestTypeDistribution:
|
||||
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, heatmap")
|
||||
WithAdditional("Valid request types are: raw, timeseries, scalar, heatmap, distribution")
|
||||
}
|
||||
|
||||
if err := r.validateHeatmap(); err != nil {
|
||||
if err := r.validateBucketCounting(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -865,14 +865,14 @@ func validateQueryEnvelope(envelope QueryEnvelope, opts ...ValidationOption) err
|
||||
}
|
||||
}
|
||||
|
||||
func (r *QueryRangeRequest) validateHeatmap() error {
|
||||
if r.RequestType != RequestTypeHeatmap {
|
||||
func (r *QueryRangeRequest) validateBucketCounting() error {
|
||||
if !r.RequestType.CountsIntoBuckets() {
|
||||
for _, envelope := range r.CompositeQuery.Queries {
|
||||
if extractEnabledBucketOptions(envelope) != nil {
|
||||
return errors.NewInvalidInputf(
|
||||
errors.CodeInvalidInput,
|
||||
"bucketOptions are only supported for heatmap requests, got %s",
|
||||
r.RequestType,
|
||||
"bucketOptions are only supported for heatmap and distribution requests, got %s",
|
||||
r.RequestType.StringValue(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -881,7 +881,7 @@ func (r *QueryRangeRequest) validateHeatmap() error {
|
||||
|
||||
if r.FormatOptions != nil && r.FormatOptions.FillGaps {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"fillGaps is not supported for heatmap requests: an absent column means collection stopped, which a zero-filled column would hide")
|
||||
"fillGaps is not supported for %s requests: an absent column means collection stopped, which a zero-filled column would hide", r.RequestType.StringValue())
|
||||
}
|
||||
|
||||
enabled := 0
|
||||
@@ -892,7 +892,7 @@ func (r *QueryRangeRequest) validateHeatmap() error {
|
||||
|
||||
switch spec := envelope.Spec.(type) {
|
||||
case QueryBuilderQuery[MetricAggregation]:
|
||||
if err := validateHeatmapQuery(spec.Functions, spec.Having); err != nil {
|
||||
if err := validateBucketCountingQuery(r.RequestType, spec.Functions, spec.Having); err != nil {
|
||||
return err
|
||||
}
|
||||
if spec.Disabled {
|
||||
@@ -900,7 +900,7 @@ func (r *QueryRangeRequest) validateHeatmap() error {
|
||||
}
|
||||
enabled++
|
||||
case QueryBuilderFormula:
|
||||
if err := validateHeatmapQuery(spec.Functions, spec.Having); err != nil {
|
||||
if err := validateBucketCountingQuery(r.RequestType, spec.Functions, spec.Having); err != nil {
|
||||
return err
|
||||
}
|
||||
if spec.Disabled {
|
||||
@@ -925,7 +925,7 @@ func (r *QueryRangeRequest) validateHeatmap() error {
|
||||
"heatmaps are not supported for the logs and traces signals yet")
|
||||
default:
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"heatmap requests support one metrics builder query, one formula over them, one clickhouse query, or one promql query, got %q", envelope.Type.StringValue())
|
||||
"%s requests support one metrics builder query, one formula over them, one clickhouse query, or one promql query, got %q", r.RequestType.StringValue(), envelope.Type.StringValue())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -937,11 +937,11 @@ func (r *QueryRangeRequest) validateHeatmap() error {
|
||||
"at least one query is required")
|
||||
case enabled == 0:
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"a heatmap needs one enabled query, but every query is disabled").
|
||||
"a %s needs one enabled query, but every query is disabled", r.RequestType.StringValue()).
|
||||
WithAdditional("Enable the query whose distribution you want to plot")
|
||||
case enabled > 1:
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"a heatmap renders one distribution, but %d queries are enabled", enabled).
|
||||
"%s requests take one enabled query, but %d are enabled", r.RequestType.StringValue(), enabled).
|
||||
WithAdditional(
|
||||
"Disable the queries you don't want to plot, keeping only the one whose distribution you want to show",
|
||||
"A formula can stay enabled with the queries it reads disabled",
|
||||
@@ -1011,18 +1011,19 @@ func (b *BucketOptions) validateBucketOptions() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateHeatmapQuery refuses the per-query settings that cannot mean anything
|
||||
// on a heatmap. It runs on disabled queries too: a disabled query is a formula
|
||||
// input, so whatever it does still reaches the cells.
|
||||
func validateHeatmapQuery(functions []Function, having *Having) error {
|
||||
// validateBucketCountingQuery refuses the per-query settings that cannot mean
|
||||
// anything once values are counted into buckets. It runs on disabled queries
|
||||
// too: a disabled query is a formula input, so whatever it does still reaches
|
||||
// the counts.
|
||||
func validateBucketCountingQuery(requestType RequestType, functions []Function, having *Having) error {
|
||||
if len(functions) > 0 {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"functions are not supported for heatmap requests: a heatmap point is a count per bucket, not a single value")
|
||||
"functions are not supported for %s requests: a heatmap point is a count per bucket, not a single value", requestType.StringValue())
|
||||
}
|
||||
|
||||
if having != nil && having.Expression != "" {
|
||||
return errors.NewInvalidInputf(errors.CodeInvalidInput,
|
||||
"having is not supported for heatmap requests: it filters individual cells, which breaks the cumulative differencing")
|
||||
"having is not supported for %s requests: it filters individual cells, which breaks the cumulative differencing", requestType.StringValue())
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1030,7 +1031,7 @@ func validateHeatmapQuery(functions []Function, having *Having) error {
|
||||
|
||||
func GetValidationOptions(requestType RequestType) []ValidationOption {
|
||||
switch requestType {
|
||||
case RequestTypeTimeSeries, RequestTypeHeatmap:
|
||||
case RequestTypeTimeSeries, RequestTypeHeatmap, RequestTypeDistribution:
|
||||
return []ValidationOption{WithSkipSelectFieldValidation(), WithTimestampGroupByValidation()}
|
||||
case RequestTypeScalar:
|
||||
return []ValidationOption{WithSkipSelectFieldValidation(), WithReduceToValidation()}
|
||||
|
||||
53
tests/fixtures/querier.py
vendored
53
tests/fixtures/querier.py
vendored
@@ -3,6 +3,7 @@ from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from fixtures import types
|
||||
@@ -34,6 +35,7 @@ class RequestType:
|
||||
SCALAR = "scalar"
|
||||
TABLE = "table"
|
||||
HEATMAP = "heatmap"
|
||||
DISTRIBUTION = "distribution"
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -413,9 +415,9 @@ def get_all_series(response_json: dict, query_name: str) -> list[dict]:
|
||||
return aggregations[0].get("series", [])
|
||||
|
||||
|
||||
def get_heatmap_buckets(response_json: dict, query_name: str) -> list[float]:
|
||||
"""The ascending bucket upper bounds a heatmap result's counts are positional against.
|
||||
Each point holds one more count than there are bounds: the trailing one is the open-above overflow."""
|
||||
def get_buckets(response_json: dict, query_name: str) -> list[float]:
|
||||
"""The ascending bucket upper bounds a heatmap or distribution result's counts are positional against.
|
||||
A point or distribution holds one more count than there are bounds: the trailing one is the open-above overflow."""
|
||||
results = response_json.get("data", {}).get("data", {}).get("results", [])
|
||||
result = find_named_result(results, query_name)
|
||||
if not result:
|
||||
@@ -888,6 +890,51 @@ def assert_identical_query_response(response1: requests.Response, response2: req
|
||||
assert response1.json()["data"]["data"]["results"] == response2.json()["data"]["data"]["results"], "Response data do not match"
|
||||
|
||||
|
||||
def assert_distribution_sums_heatmap(
|
||||
signoz: types.SigNoz,
|
||||
token: str,
|
||||
start_ms: int,
|
||||
end_ms: int,
|
||||
queries: list[dict],
|
||||
heatmap: requests.Response,
|
||||
*,
|
||||
no_cache: bool = True,
|
||||
) -> None:
|
||||
"""A distribution request over the queries a heatmap was drawn from answers with that heatmap's
|
||||
columns added up: the same aggregations and buckets, and for each series one count per bucket
|
||||
plus the overflow under the same labels, with nothing time-shaped."""
|
||||
response = make_query_request(signoz, token, start_ms, end_ms, queries, request_type=RequestType.DISTRIBUTION, no_cache=no_cache)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
data = response.json()
|
||||
assert data["data"]["type"] == RequestType.DISTRIBUTION, f"response type: {data['data']['type']}"
|
||||
|
||||
def labels_of(item: dict) -> tuple:
|
||||
return tuple(sorted((label["key"]["name"], str(label["value"])) for label in item.get("labels") or []))
|
||||
|
||||
heatmap_results = {result["queryName"]: result for result in heatmap.json()["data"]["data"]["results"]}
|
||||
distribution_results = {result["queryName"]: result for result in data["data"]["data"]["results"]}
|
||||
assert set(distribution_results) == set(heatmap_results), f"distribution results {sorted(distribution_results)}, heatmap results {sorted(heatmap_results)}"
|
||||
|
||||
for name, heatmap_result in heatmap_results.items():
|
||||
heatmap_aggregations = heatmap_result.get("aggregations") or []
|
||||
distribution_aggregations = distribution_results[name].get("aggregations") or []
|
||||
assert [(aggregation["index"], aggregation["alias"], aggregation.get("meta", {})) for aggregation in distribution_aggregations] == [(aggregation["index"], aggregation["alias"], aggregation.get("meta", {})) for aggregation in heatmap_aggregations], (
|
||||
f"{name}: distribution and heatmap aggregations differ in index, alias or meta"
|
||||
)
|
||||
|
||||
for heatmap_aggregation, distribution_aggregation in zip(heatmap_aggregations, distribution_aggregations):
|
||||
buckets = heatmap_aggregation.get("meta", {}).get("buckets", [])
|
||||
series_by_labels = {labels_of(series): series for series in heatmap_aggregation.get("series") or []}
|
||||
distributions_by_labels = {labels_of(distribution): distribution for distribution in distribution_aggregation.get("distributions") or []}
|
||||
assert set(distributions_by_labels) == set(series_by_labels), f"{name}: distribution groups {sorted(distributions_by_labels)}, heatmap series {sorted(series_by_labels)}"
|
||||
|
||||
for labels, series in series_by_labels.items():
|
||||
distribution = distributions_by_labels[labels]
|
||||
assert set(distribution) <= {"labels", "counts"}, f"{name} {dict(labels)}: distribution fields {sorted(distribution)}"
|
||||
summed = [sum(column["values"][slot] for column in series.get("values") or [] if slot < len(column.get("values") or [])) for slot in range(len(buckets) + 1)]
|
||||
assert distribution["counts"] == pytest.approx(summed), f"{name} {dict(labels)}: distribution counts {distribution['counts']}, heatmap columns add up to {summed}"
|
||||
|
||||
|
||||
# we already create the evolution for resource during schema migration
|
||||
# since we have to create test data around it, we need to get the evolution time
|
||||
def get_resource_evolution_time(signoz: types.SigNoz, signal: str) -> datetime:
|
||||
|
||||
@@ -11,6 +11,7 @@ from fixtures.meter import MeterSample
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import (
|
||||
RequestType,
|
||||
assert_distribution_sums_heatmap,
|
||||
assert_identical_query_response,
|
||||
build_builder_query,
|
||||
build_formula_query,
|
||||
@@ -18,7 +19,7 @@ from fixtures.querier import (
|
||||
build_log_bucket_options,
|
||||
get_all_series,
|
||||
get_all_warnings,
|
||||
get_heatmap_buckets,
|
||||
get_buckets,
|
||||
get_heatmap_columns,
|
||||
index_series_by_label,
|
||||
make_query_request,
|
||||
@@ -58,12 +59,13 @@ def test_gauge_heatmap(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [build_builder_query("A", metric_name, "max", "max", group_by=["host"], bucket_options=build_log_bucket_options(0))]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[build_builder_query("A", metric_name, "max", "max", group_by=["host"], bucket_options=build_log_bucket_options(0))],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
@@ -72,7 +74,7 @@ def test_gauge_heatmap(
|
||||
# a group by gives one series per host, and all of them are counted against
|
||||
# this one axis. 128 is on it so the lowest bucket holding anything reads as
|
||||
# (128, 256] rather than as everything at or below 256
|
||||
assert get_heatmap_buckets(data, "A") == pytest.approx([128.0, 256.0, 512.0, 1024.0])
|
||||
assert get_buckets(data, "A") == pytest.approx([128.0, 256.0, 512.0, 1024.0])
|
||||
|
||||
columns_by_host = {host: sorted(series["values"], key=lambda column: column["timestamp"]) for host, series in index_series_by_label(get_all_series(data, "A"), "host").items()}
|
||||
assert len(columns_by_host) == len(value_by_host)
|
||||
@@ -96,6 +98,8 @@ def test_gauge_heatmap(
|
||||
for minute in range(minutes):
|
||||
assert [sum(columns[minute]["values"][slot] for columns in columns_by_host.values()) for slot in range(5)] == [0, 8, 8, 8, 0]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
def test_sum_heatmap(
|
||||
signoz: types.SigNoz,
|
||||
@@ -126,18 +130,19 @@ def test_sum_heatmap(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [build_builder_query("A", metric_name, "max", "max", temporality="cumulative", group_by=["endpoint"], bucket_options=build_log_bucket_options(0))]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[build_builder_query("A", metric_name, "max", "max", temporality="cumulative", group_by=["endpoint"], bucket_options=build_log_bucket_options(0))],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
data = response.json()
|
||||
assert get_heatmap_buckets(data, "A") == pytest.approx([64.0, 128.0, 256.0, 512.0, 1024.0])
|
||||
assert get_buckets(data, "A") == pytest.approx([64.0, 128.0, 256.0, 512.0, 1024.0])
|
||||
|
||||
columns_by_endpoint = {endpoint: sorted(series["values"], key=lambda column: column["timestamp"]) for endpoint, series in index_series_by_label(get_all_series(data, "A"), "endpoint").items()}
|
||||
assert len(columns_by_endpoint) == len(value_by_endpoint)
|
||||
@@ -154,6 +159,8 @@ def test_sum_heatmap(
|
||||
for minute in range(minutes):
|
||||
assert [sum(columns[minute]["values"][slot] for columns in columns_by_endpoint.values()) for slot in range(6)] == [0, 8, 0, 0, 8, 0]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
def test_meter_heatmap(
|
||||
signoz: types.SigNoz,
|
||||
@@ -183,12 +190,13 @@ def test_meter_heatmap(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [build_builder_query("A", metric_name, "sum", "sum", source="meter", temporality="delta", step_interval=3600, bucket_options=build_linear_bucket_options(1000, 10))]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[build_builder_query("A", metric_name, "sum", "sum", source="meter", temporality="delta", step_interval=3600, bucket_options=build_linear_bucket_options(1000, 10))],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
@@ -197,13 +205,15 @@ def test_meter_heatmap(
|
||||
# 1000 split into 10 buckets gives (0, 100], (100, 200], ... (900, 1000]:
|
||||
# 100 lands in the first, 250 in the third, and 1500 is past 1000 so it
|
||||
# counts in the overflow
|
||||
assert get_heatmap_buckets(data, "A") == pytest.approx([0.0, 100.0, 200.0, 300.0])
|
||||
assert get_buckets(data, "A") == pytest.approx([0.0, 100.0, 200.0, 300.0])
|
||||
assert [column["values"] for column in get_heatmap_columns(data, "A")] == [
|
||||
[0, 1, 0, 0, 0],
|
||||
[0, 0, 0, 1, 0],
|
||||
[0, 0, 0, 0, 1],
|
||||
]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
def test_histogram_heatmap(
|
||||
signoz: types.SigNoz,
|
||||
@@ -225,12 +235,13 @@ def test_histogram_heatmap(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [build_builder_query("A", metric_name, "increase", "p50", group_by=["le"], filter_expression='endpoint = "/health"')]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[build_builder_query("A", metric_name, "increase", "p50", group_by=["le"], filter_expression='endpoint = "/health"')],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
@@ -239,14 +250,21 @@ def test_histogram_heatmap(
|
||||
# a histogram's axis is its recorded `le` bounds exactly, since nothing is
|
||||
# known about what sits between two of them; `le=+Inf` has no finite bound
|
||||
# and is counted in the trailing overflow slot
|
||||
assert get_heatmap_buckets(data, "A") == [1000, 1500, 2000, 4000, 5000, 6000, 8000]
|
||||
assert get_buckets(data, "A") == [1000, 1500, 2000, 4000, 5000, 6000, 8000]
|
||||
|
||||
columns = get_heatmap_columns(data, "A")
|
||||
assert columns
|
||||
for column in columns:
|
||||
assert len(column["values"]) == 8
|
||||
assert all(count >= 0 for count in column["values"])
|
||||
assert any(sum(column["values"]) > 0 for column in columns)
|
||||
# the file records /health for an hour, one sample a minute, and each `le`
|
||||
# counter grows by one more per minute than the one below it. the first
|
||||
# sample has no increase, so over the other 59 minutes the 1000 counter
|
||||
# grows by 11 through 69, the sum of which is 2360, and every bucket above
|
||||
# it holds exactly one observation a minute, 59 in all
|
||||
assert [sum(column["values"][slot] for column in columns) for slot in range(8)] == [2360, 59, 59, 59, 59, 59, 59, 59]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
def test_linear_buckets(
|
||||
@@ -279,12 +297,13 @@ def test_linear_buckets(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [build_builder_query("A", metric_name, "max", "max", bucket_options=build_linear_bucket_options(1000, 10))]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[build_builder_query("A", metric_name, "max", "max", bucket_options=build_linear_bucket_options(1000, 10))],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
@@ -292,7 +311,7 @@ def test_linear_buckets(
|
||||
data = response.json()
|
||||
# 200 is on the axis although nothing reached it, so the gap between 100 and
|
||||
# 300 renders as a gap, and 0 is on it so the lowest bucket reads as (0, 100]
|
||||
assert get_heatmap_buckets(data, "A") == pytest.approx([0.0, 100.0, 200.0, 300.0])
|
||||
assert get_buckets(data, "A") == pytest.approx([0.0, 100.0, 200.0, 300.0])
|
||||
|
||||
columns = get_heatmap_columns(data, "A")
|
||||
assert [column["values"] for column in columns] == [
|
||||
@@ -301,6 +320,8 @@ def test_linear_buckets(
|
||||
[0, 0, 0, 0, 1],
|
||||
]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bucket_options, expected_buckets, expected_columns",
|
||||
@@ -348,22 +369,25 @@ def test_zero_bucket(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [build_builder_query("A", metric_name, "max", "max", bucket_options=bucket_options)]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[build_builder_query("A", metric_name, "max", "max", bucket_options=bucket_options)],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
data = response.json()
|
||||
assert get_heatmap_buckets(data, "A") == pytest.approx(expected_buckets)
|
||||
assert get_buckets(data, "A") == pytest.approx(expected_buckets)
|
||||
|
||||
columns = get_heatmap_columns(data, "A")
|
||||
assert [column["values"] for column in columns] == expected_columns
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
def test_single_bucket(
|
||||
signoz: types.SigNoz,
|
||||
@@ -392,12 +416,13 @@ def test_single_bucket(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [build_builder_query("A", metric_name, "max", "max")]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[build_builder_query("A", metric_name, "max", "max")],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
@@ -405,13 +430,15 @@ def test_single_bucket(
|
||||
# a single bucket leaves no gap to span, and still gets the rung below it so
|
||||
# its own lower bound is stated
|
||||
data = response.json()
|
||||
assert get_heatmap_buckets(data, "A") == pytest.approx([256 * 2 ** (-1 / 16), 256.0])
|
||||
assert get_buckets(data, "A") == pytest.approx([256 * 2 ** (-1 / 16), 256.0])
|
||||
assert [column["values"] for column in get_heatmap_columns(data, "A")] == [
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
# the values seeded below are 256 and 512, so each axis here runs from the rung
|
||||
# under the bucket holding 256 to the one holding 512 at that option's
|
||||
@@ -458,18 +485,19 @@ def test_bucket_option_limits(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [build_builder_query("A", metric_name, "max", "max", bucket_options=bucket_options)]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[build_builder_query("A", metric_name, "max", "max", bucket_options=bucket_options)],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
data = response.json()
|
||||
assert get_heatmap_buckets(data, "A") == pytest.approx(expected_buckets)
|
||||
assert get_buckets(data, "A") == pytest.approx(expected_buckets)
|
||||
|
||||
columns = get_heatmap_columns(data, "A")
|
||||
assert len(columns) == len(values)
|
||||
@@ -477,6 +505,8 @@ def test_bucket_option_limits(
|
||||
assert len(column["values"]) == len(expected_buckets) + 1
|
||||
assert sum(column["values"]) == 1
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
def test_bucket_options_come_from_the_enabled_query(
|
||||
signoz: types.SigNoz,
|
||||
@@ -505,15 +535,16 @@ def test_bucket_options_come_from_the_enabled_query(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [
|
||||
build_builder_query("A", metric_name, "max", "max", disabled=True, bucket_options=build_linear_bucket_options(1024, 512)),
|
||||
build_builder_query("B", metric_name, "max", "max", bucket_options=build_log_bucket_options(-4)),
|
||||
]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[
|
||||
build_builder_query("A", metric_name, "max", "max", disabled=True, bucket_options=build_linear_bucket_options(1024, 512)),
|
||||
build_builder_query("B", metric_name, "max", "max", bucket_options=build_log_bucket_options(-4)),
|
||||
],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
@@ -521,12 +552,14 @@ def test_bucket_options_come_from_the_enabled_query(
|
||||
data = response.json()
|
||||
# B's axis, one bucket per 16x. A's 512 linear buckets to 1024 would have
|
||||
# put 130 of them between 256 and 512 alone
|
||||
assert get_heatmap_buckets(data, "B") == pytest.approx([1, 2**16])
|
||||
assert get_buckets(data, "B") == pytest.approx([1, 2**16])
|
||||
assert [column["values"] for column in get_heatmap_columns(data, "B")] == [
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
def test_formula_heatmap(
|
||||
signoz: types.SigNoz,
|
||||
@@ -568,22 +601,25 @@ def test_formula_heatmap(
|
||||
# a formula over the same query has to land its counts on the same axis. the
|
||||
# scale on the disabled input is out of range: only the query the heatmap
|
||||
# draws states an axis, so nothing reads that one
|
||||
queries = [
|
||||
build_builder_query("A", metric_name, "max", "max", disabled=True, bucket_options={"kind": "log", "spec": {"scale": 5}}),
|
||||
build_formula_query("F1", "A", bucket_options=build_log_bucket_options(2)),
|
||||
]
|
||||
from_formula = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[
|
||||
build_builder_query("A", metric_name, "max", "max", disabled=True, bucket_options={"kind": "log", "spec": {"scale": 5}}),
|
||||
build_formula_query("F1", "A", bucket_options=build_log_bucket_options(2)),
|
||||
],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert from_formula.status_code == HTTPStatus.OK, from_formula.text
|
||||
|
||||
assert get_heatmap_buckets(from_formula.json(), "F1") == pytest.approx(get_heatmap_buckets(from_metric.json(), "A"))
|
||||
assert get_buckets(from_formula.json(), "F1") == pytest.approx(get_buckets(from_metric.json(), "A"))
|
||||
assert [column["values"] for column in get_heatmap_columns(from_formula.json(), "F1")] == [column["values"] for column in get_heatmap_columns(from_metric.json(), "A")]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, from_formula)
|
||||
|
||||
|
||||
def test_promql_heatmap(
|
||||
signoz: types.SigNoz,
|
||||
@@ -601,18 +637,19 @@ def test_promql_heatmap(
|
||||
insert_metrics(Metrics.load_from_file(HISTOGRAM_COUNTERS_FILE, base_time=datetime.fromtimestamp((start_ms - MINUTE_MS) / 1000, tz=UTC)))
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [{"type": "promql", "spec": {"name": "A", "query": "sum by (le) (increase(heatmap_request_duration_bucket[2m]))", "step": 60}}]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[{"type": "promql", "spec": {"name": "A", "query": "sum by (le) (increase(heatmap_request_duration_bucket[2m]))", "step": 60}}],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
data = response.json()
|
||||
assert get_heatmap_buckets(data, "A") == [1, 2, 4]
|
||||
assert get_buckets(data, "A") == [1, 2, 4]
|
||||
|
||||
series = get_all_series(data, "A")
|
||||
assert len(series) == 1
|
||||
@@ -628,6 +665,8 @@ def test_promql_heatmap(
|
||||
[6, 0, 8, 4],
|
||||
]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
def test_promql_heatmap_with_no_data(
|
||||
signoz: types.SigNoz,
|
||||
@@ -638,12 +677,13 @@ def test_promql_heatmap_with_no_data(
|
||||
start_ms = end_ms - MINUTE_MS
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [{"type": "promql", "spec": {"name": "A", "query": "sum by (le) (increase(promql_heatmap_bucket_never_written[2m]))", "step": 60}}]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[{"type": "promql", "spec": {"name": "A", "query": "sum by (le) (increase(promql_heatmap_bucket_never_written[2m]))", "step": 60}}],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
# a window holding nothing is not a query that can never draw a heatmap, so
|
||||
@@ -651,9 +691,11 @@ def test_promql_heatmap_with_no_data(
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
data = response.json()
|
||||
assert get_heatmap_buckets(data, "A") == []
|
||||
assert get_buckets(data, "A") == []
|
||||
assert get_heatmap_columns(data, "A") == []
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
def test_clickhouse_heatmap(
|
||||
signoz: types.SigNoz,
|
||||
@@ -683,29 +725,30 @@ def test_clickhouse_heatmap(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [
|
||||
{
|
||||
"type": "clickhouse_sql",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": (
|
||||
"SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) AS ts, "
|
||||
"ceil(value / 10) * 10 - 10 AS `__bucket_min`, "
|
||||
"ceil(value / 10) * 10 AS `__bucket_max`, "
|
||||
"toFloat64(count()) AS `__result_0` "
|
||||
"FROM signoz_metrics.distributed_samples_v4 "
|
||||
f"WHERE metric_name = '{metric_name}' "
|
||||
"GROUP BY ts, `__bucket_min`, `__bucket_max`"
|
||||
),
|
||||
"disabled": False,
|
||||
},
|
||||
}
|
||||
]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
int(start.timestamp() * 1000),
|
||||
int(now.timestamp() * 1000),
|
||||
[
|
||||
{
|
||||
"type": "clickhouse_sql",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": (
|
||||
"SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) AS ts, "
|
||||
"ceil(value / 10) * 10 - 10 AS `__bucket_min`, "
|
||||
"ceil(value / 10) * 10 AS `__bucket_max`, "
|
||||
"toFloat64(count()) AS `__result_0` "
|
||||
"FROM signoz_metrics.distributed_samples_v4 "
|
||||
f"WHERE metric_name = '{metric_name}' "
|
||||
"GROUP BY ts, `__bucket_min`, `__bucket_max`"
|
||||
),
|
||||
"disabled": False,
|
||||
},
|
||||
}
|
||||
],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
@@ -714,13 +757,15 @@ def test_clickhouse_heatmap(
|
||||
# 20 and 80 are on the axis although no row ended there, so the two ranges
|
||||
# the query skipped hold a count of their own rather than widening the
|
||||
# buckets above them
|
||||
assert get_heatmap_buckets(data, "A") == [10, 20, 30, 80, 90]
|
||||
assert get_buckets(data, "A") == [10, 20, 30, 80, 90]
|
||||
# a clickhouse heatmap takes no bucketOptions, so the counts land exactly
|
||||
# where the query put them, plus the overflow slot
|
||||
assert [column["values"] for column in get_heatmap_columns(data, "A")] == [
|
||||
[3, 0, 7, 0, 2, 0],
|
||||
]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, int(start.timestamp() * 1000), int(now.timestamp() * 1000), queries, response)
|
||||
|
||||
|
||||
def test_clickhouse_heatmap_with_no_rows(
|
||||
signoz: types.SigNoz,
|
||||
@@ -728,37 +773,42 @@ def test_clickhouse_heatmap_with_no_rows(
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
start_ms = int((now - timedelta(minutes=2)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
queries = [
|
||||
{
|
||||
"type": "clickhouse_sql",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": (
|
||||
"SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) AS ts, "
|
||||
"toFloat64(0) AS `__bucket_min`, toFloat64(10) AS `__bucket_max`, toFloat64(count()) AS `__result_0` "
|
||||
"FROM signoz_metrics.distributed_samples_v4 "
|
||||
"WHERE metric_name = 'test_heatmap_clickhouse_never_written' "
|
||||
"GROUP BY ts"
|
||||
),
|
||||
"disabled": False,
|
||||
},
|
||||
}
|
||||
]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
int((now - timedelta(minutes=2)).timestamp() * 1000),
|
||||
int(now.timestamp() * 1000),
|
||||
[
|
||||
{
|
||||
"type": "clickhouse_sql",
|
||||
"spec": {
|
||||
"name": "A",
|
||||
"query": (
|
||||
"SELECT toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), INTERVAL 60 SECOND) AS ts, "
|
||||
"toFloat64(0) AS `__bucket_min`, toFloat64(10) AS `__bucket_max`, toFloat64(count()) AS `__result_0` "
|
||||
"FROM signoz_metrics.distributed_samples_v4 "
|
||||
"WHERE metric_name = 'test_heatmap_clickhouse_never_written' "
|
||||
"GROUP BY ts"
|
||||
),
|
||||
"disabled": False,
|
||||
},
|
||||
}
|
||||
],
|
||||
start_ms,
|
||||
end_ms,
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
data = response.json()
|
||||
assert get_heatmap_buckets(data, "A") == []
|
||||
assert get_buckets(data, "A") == []
|
||||
assert get_heatmap_columns(data, "A") == []
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
def test_cached_heatmap_matches_uncached(
|
||||
signoz: types.SigNoz,
|
||||
@@ -812,9 +862,11 @@ def test_cached_heatmap_matches_uncached(
|
||||
# 256 and 4096 are 16x apart, which the axis covers at 16 buckets per 2x plus
|
||||
# the rung under the lowest, and every column holds the one value its minute
|
||||
# recorded
|
||||
assert len(get_heatmap_buckets(uncached.json(), "A")) == 66
|
||||
assert len(get_buckets(uncached.json(), "A")) == 66
|
||||
assert [sum(column["values"]) for column in get_heatmap_columns(uncached.json(), "A")] == [1] * 30
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, wide_start_ms, wide_end_ms, query, uncached)
|
||||
|
||||
|
||||
def test_metric_with_no_data(
|
||||
signoz: types.SigNoz,
|
||||
@@ -822,16 +874,19 @@ def test_metric_with_no_data(
|
||||
get_token: Callable[[str, str], str],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
start_ms = int((now - timedelta(minutes=30)).timestamp() * 1000)
|
||||
end_ms = int(now.timestamp() * 1000)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
missing_metric = "test_heatmap_metric_that_is_never_written"
|
||||
|
||||
queries = [build_builder_query("A", missing_metric, "max", "max", bucket_options=build_log_bucket_options(0))]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
int((now - timedelta(minutes=30)).timestamp() * 1000),
|
||||
int(now.timestamp() * 1000),
|
||||
[build_builder_query("A", missing_metric, "max", "max", bucket_options=build_log_bucket_options(0))],
|
||||
start_ms,
|
||||
end_ms,
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
# a metric with nothing in the window carries no type to choose an axis
|
||||
@@ -839,10 +894,12 @@ def test_metric_with_no_data(
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
data = response.json()
|
||||
assert get_heatmap_buckets(data, "A") == []
|
||||
assert get_buckets(data, "A") == []
|
||||
assert get_heatmap_columns(data, "A") == []
|
||||
assert any(missing_metric in warning["message"] for warning in get_all_warnings(data))
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bucket_options, expected_buckets",
|
||||
@@ -881,18 +938,19 @@ def test_negative_values(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [build_builder_query("A", metric_name, "max", "max", bucket_options=bucket_options)]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[build_builder_query("A", metric_name, "max", "max", bucket_options=bucket_options)],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
data = response.json()
|
||||
assert get_heatmap_buckets(data, "A") == pytest.approx(expected_buckets)
|
||||
assert get_buckets(data, "A") == pytest.approx(expected_buckets)
|
||||
|
||||
columns = get_heatmap_columns(data, "A")
|
||||
assert [column["values"] for column in columns] == [
|
||||
@@ -902,6 +960,8 @@ def test_negative_values(
|
||||
[0, 0, 1, 0],
|
||||
]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
|
||||
# a formula's output is bucketed after the query runs rather than by it, and
|
||||
# has to put everything at or below zero where a metric query does: in the zero
|
||||
@@ -943,21 +1003,22 @@ def test_formula_negative_values(
|
||||
)
|
||||
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
queries = [
|
||||
build_builder_query("A", metric_name, "max", "max", disabled=True),
|
||||
build_formula_query("F1", "A", bucket_options=bucket_options),
|
||||
]
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms,
|
||||
end_ms,
|
||||
[
|
||||
build_builder_query("A", metric_name, "max", "max", disabled=True),
|
||||
build_formula_query("F1", "A", bucket_options=bucket_options),
|
||||
],
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
data = response.json()
|
||||
assert get_heatmap_buckets(data, "F1") == pytest.approx(expected_buckets)
|
||||
assert get_buckets(data, "F1") == pytest.approx(expected_buckets)
|
||||
|
||||
columns = get_heatmap_columns(data, "F1")
|
||||
assert [column["values"] for column in columns] == [
|
||||
@@ -966,3 +1027,5 @@ def test_formula_negative_values(
|
||||
[1, 0, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
]
|
||||
|
||||
assert_distribution_sums_heatmap(signoz, token, start_ms, end_ms, queries, response)
|
||||
|
||||
@@ -24,6 +24,11 @@ MINUTE_MS = 60_000
|
||||
|
||||
METRIC_NAME = "test_heatmap_rejections"
|
||||
QUERY = [build_builder_query("A", METRIC_NAME, "max", "max")]
|
||||
request_types = pytest.mark.parametrize(
|
||||
"request_type",
|
||||
[RequestType.HEATMAP, RequestType.DISTRIBUTION],
|
||||
ids=["heatmap", "distribution"],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -46,7 +51,7 @@ QUERY = [build_builder_query("A", METRIC_NAME, "max", "max")]
|
||||
pytest.param(
|
||||
QUERY,
|
||||
{"format_options": {"formatTableResultForUI": False, "fillGaps": True}},
|
||||
"fillGaps is not supported for heatmap requests",
|
||||
"fillGaps is not supported for {request_type} requests",
|
||||
id="fill_gaps",
|
||||
),
|
||||
pytest.param(
|
||||
@@ -55,7 +60,7 @@ QUERY = [build_builder_query("A", METRIC_NAME, "max", "max")]
|
||||
build_builder_query("B", METRIC_NAME, "min", "min"),
|
||||
],
|
||||
{},
|
||||
"a heatmap renders one distribution, but 2 queries are enabled",
|
||||
"{request_type} requests take one enabled query, but 2 are enabled",
|
||||
id="two_enabled_queries",
|
||||
),
|
||||
pytest.param(
|
||||
@@ -65,13 +70,13 @@ QUERY = [build_builder_query("A", METRIC_NAME, "max", "max")]
|
||||
build_formula_query("F1", "B"),
|
||||
],
|
||||
{},
|
||||
"a heatmap renders one distribution, but 2 queries are enabled",
|
||||
"{request_type} requests take one enabled query, but 2 are enabled",
|
||||
id="a_formula_beside_an_enabled_query",
|
||||
),
|
||||
pytest.param(
|
||||
[build_builder_query("A", METRIC_NAME, "max", "max", disabled=True)],
|
||||
{},
|
||||
"a heatmap needs one enabled query, but every query is disabled",
|
||||
"a {request_type} needs one enabled query, but every query is disabled",
|
||||
id="only_a_disabled_query",
|
||||
),
|
||||
# an empty body is refused while resources are extracted from it, before
|
||||
@@ -85,7 +90,7 @@ QUERY = [build_builder_query("A", METRIC_NAME, "max", "max")]
|
||||
pytest.param(
|
||||
[build_builder_query("A", METRIC_NAME, "max", "max", functions=[build_function("absolute")])],
|
||||
{},
|
||||
"functions are not supported for heatmap requests",
|
||||
"functions are not supported for {request_type} requests",
|
||||
id="functions_on_the_query",
|
||||
),
|
||||
pytest.param(
|
||||
@@ -94,7 +99,7 @@ QUERY = [build_builder_query("A", METRIC_NAME, "max", "max")]
|
||||
build_formula_query("F1", "A"),
|
||||
],
|
||||
{},
|
||||
"functions are not supported for heatmap requests",
|
||||
"functions are not supported for {request_type} requests",
|
||||
id="functions_on_a_disabled_formula_input",
|
||||
),
|
||||
pytest.param(
|
||||
@@ -103,7 +108,7 @@ QUERY = [build_builder_query("A", METRIC_NAME, "max", "max")]
|
||||
build_formula_query("F1", "A", functions=[build_function("absolute")]),
|
||||
],
|
||||
{},
|
||||
"functions are not supported for heatmap requests",
|
||||
"functions are not supported for {request_type} requests",
|
||||
id="functions_on_the_formula",
|
||||
),
|
||||
pytest.param(
|
||||
@@ -120,7 +125,7 @@ QUERY = [build_builder_query("A", METRIC_NAME, "max", "max")]
|
||||
}
|
||||
],
|
||||
{},
|
||||
"having is not supported for heatmap requests",
|
||||
"having is not supported for {request_type} requests",
|
||||
id="having_on_the_query",
|
||||
),
|
||||
pytest.param(
|
||||
@@ -190,13 +195,15 @@ QUERY = [build_builder_query("A", METRIC_NAME, "max", "max")]
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_heatmap_request_is_rejected(
|
||||
@request_types
|
||||
def test_request_is_rejected(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
queries: list[dict],
|
||||
request_options: dict,
|
||||
expected_message: str,
|
||||
request_type: str,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
@@ -207,11 +214,11 @@ def test_heatmap_request_is_rejected(
|
||||
int((now - timedelta(minutes=30)).timestamp() * 1000),
|
||||
int(now.timestamp() * 1000),
|
||||
queries,
|
||||
request_type=RequestType.HEATMAP,
|
||||
request_type=request_type,
|
||||
**request_options,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert expected_message in get_error_message(response.json())
|
||||
assert expected_message.format(request_type=request_type) in get_error_message(response.json()), get_error_message(response.json())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -237,7 +244,7 @@ def test_bucket_options_outside_a_heatmap(
|
||||
request_type=request_type,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert "bucketOptions are only supported for heatmap requests" in get_error_message(response.json())
|
||||
assert "bucketOptions are only supported for heatmap and distribution requests" in get_error_message(response.json()), get_error_message(response.json())
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -256,11 +263,13 @@ def test_bucket_options_outside_a_heatmap(
|
||||
),
|
||||
],
|
||||
)
|
||||
@request_types
|
||||
def test_clickhouse_missing_bucket_columns_are_rejected(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
columns: list[str],
|
||||
request_type: str,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
start = now - timedelta(minutes=2)
|
||||
@@ -282,16 +291,18 @@ def test_clickhouse_missing_bucket_columns_are_rejected(
|
||||
},
|
||||
}
|
||||
],
|
||||
request_type=RequestType.HEATMAP,
|
||||
request_type=request_type,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert 'a heatmap needs a "__bucket_min" and a "__bucket_max" column' in get_error_message(response.json())
|
||||
assert 'a heatmap needs a "__bucket_min" and a "__bucket_max" column' in get_error_message(response.json()), get_error_message(response.json())
|
||||
|
||||
|
||||
@request_types
|
||||
def test_clickhouse_one_bucket_cut_two_ways_is_rejected(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
request_type: str,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
start = now - timedelta(minutes=2)
|
||||
@@ -313,7 +324,7 @@ def test_clickhouse_one_bucket_cut_two_ways_is_rejected(
|
||||
},
|
||||
}
|
||||
],
|
||||
request_type=RequestType.HEATMAP,
|
||||
request_type=request_type,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
|
||||
@@ -326,10 +337,12 @@ def test_clickhouse_one_bucket_cut_two_ways_is_rejected(
|
||||
)
|
||||
|
||||
|
||||
@request_types
|
||||
def test_clickhouse_one_bucket_cut_two_ways_across_groups_is_rejected(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
request_type: str,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
start = now - timedelta(minutes=2)
|
||||
@@ -351,7 +364,7 @@ def test_clickhouse_one_bucket_cut_two_ways_across_groups_is_rejected(
|
||||
},
|
||||
}
|
||||
],
|
||||
request_type=RequestType.HEATMAP,
|
||||
request_type=request_type,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
|
||||
@@ -364,11 +377,13 @@ def test_clickhouse_one_bucket_cut_two_ways_across_groups_is_rejected(
|
||||
)
|
||||
|
||||
|
||||
@request_types
|
||||
def test_promql_returning_no_le_is_rejected(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
request_type: str,
|
||||
) -> None:
|
||||
metric = f"promql_heatmap_gauge_{uuid4().hex[:8]}"
|
||||
end_ms = (int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000) // MINUTE_MS) * MINUTE_MS
|
||||
@@ -397,17 +412,19 @@ def test_promql_returning_no_le_is_rejected(
|
||||
start_ms,
|
||||
end_ms,
|
||||
[{"type": "promql", "spec": {"name": "A", "query": metric, "step": 60}}],
|
||||
request_type=RequestType.HEATMAP,
|
||||
request_type=request_type,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert "no `le` labels to build a bucket axis from" in get_error_message(response.json())
|
||||
assert "no `le` labels to build a bucket axis from" in get_error_message(response.json()), get_error_message(response.json())
|
||||
|
||||
|
||||
@request_types
|
||||
def test_histogram_rejects_bucket_options(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_metrics: Callable[[list[Metrics]], None],
|
||||
request_type: str,
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
start_ms = int((now - timedelta(minutes=65)).timestamp() * 1000)
|
||||
@@ -429,7 +446,7 @@ def test_histogram_rejects_bucket_options(
|
||||
start_ms,
|
||||
end_ms,
|
||||
[build_builder_query("A", metric_name, "increase", "p50", bucket_options=build_log_bucket_options(2))],
|
||||
request_type=RequestType.HEATMAP,
|
||||
request_type=request_type,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.BAD_REQUEST, response.text
|
||||
assert "bucketOptions are not supported for histogram metrics" in get_error_message(response.json())
|
||||
assert "bucketOptions are not supported for histogram metrics" in get_error_message(response.json()), get_error_message(response.json())
|
||||
|
||||
@@ -10,10 +10,11 @@ from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.metrics import Metrics
|
||||
from fixtures.querier import (
|
||||
RequestType,
|
||||
assert_distribution_sums_heatmap,
|
||||
assert_identical_query_response,
|
||||
build_builder_query,
|
||||
build_linear_bucket_options,
|
||||
get_heatmap_buckets,
|
||||
get_buckets,
|
||||
get_heatmap_columns,
|
||||
make_query_request,
|
||||
)
|
||||
@@ -65,7 +66,7 @@ def test_builder_narrowing_to_half_the_range(
|
||||
# the whole range first, which is what puts its axis in the cache
|
||||
whole_range = make_query_request(signoz, token, start_time_ms, end_time_ms, query, request_type=RequestType.HEATMAP, no_cache=False)
|
||||
assert whole_range.status_code == HTTPStatus.OK, whole_range.text
|
||||
assert get_heatmap_buckets(whole_range.json(), "A") == pytest.approx([100, 200, 300, 400, 500, 600, 700, 800, 900])
|
||||
assert get_buckets(whole_range.json(), "A") == pytest.approx([100, 200, 300, 400, 500, 600, 700, 800, 900])
|
||||
assert [column["values"] for column in get_heatmap_columns(whole_range.json(), "A")] == [
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
@@ -78,6 +79,7 @@ def test_builder_narrowing_to_half_the_range(
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
]
|
||||
assert_distribution_sums_heatmap(signoz, token, start_time_ms, end_time_ms, query, whole_range, no_cache=False)
|
||||
|
||||
half_start_ms = start_time_ms + first_minute * MINUTE_MS
|
||||
half_end_ms = half_start_ms + 5 * MINUTE_MS
|
||||
@@ -88,7 +90,7 @@ def test_builder_narrowing_to_half_the_range(
|
||||
assert uncached.status_code == HTTPStatus.OK, uncached.text
|
||||
|
||||
for source, response in (("uncached", uncached), ("from cache", from_cache)):
|
||||
assert get_heatmap_buckets(response.json(), "A") == pytest.approx(expected_buckets), source
|
||||
assert get_buckets(response.json(), "A") == pytest.approx(expected_buckets), source
|
||||
assert [column["values"] for column in get_heatmap_columns(response.json(), "A")] == [
|
||||
[0, 1, 0],
|
||||
[0, 1, 0],
|
||||
@@ -98,6 +100,7 @@ def test_builder_narrowing_to_half_the_range(
|
||||
], source
|
||||
|
||||
assert_identical_query_response(from_cache, uncached)
|
||||
assert_distribution_sums_heatmap(signoz, token, half_start_ms, half_end_ms, query, uncached, no_cache=False)
|
||||
|
||||
|
||||
def test_builder_narrowing_a_histogram(
|
||||
@@ -145,7 +148,7 @@ def test_builder_narrowing_a_histogram(
|
||||
# the whole range first, which is what puts its axis in the cache
|
||||
whole_range = make_query_request(signoz, token, start_time_ms, end_time_ms, query, request_type=RequestType.HEATMAP, no_cache=False)
|
||||
assert whole_range.status_code == HTTPStatus.OK, whole_range.text
|
||||
assert get_heatmap_buckets(whole_range.json(), "A") == [1, 2, 4, 8]
|
||||
assert get_buckets(whole_range.json(), "A") == [1, 2, 4, 8]
|
||||
assert [column["values"] for column in get_heatmap_columns(whole_range.json(), "A")] == [
|
||||
[10, 0, 0, 0, 0],
|
||||
[10, 0, 0, 0, 0],
|
||||
@@ -158,6 +161,7 @@ def test_builder_narrowing_a_histogram(
|
||||
[0, 0, 0, 10, 0],
|
||||
[0, 0, 0, 10, 0],
|
||||
]
|
||||
assert_distribution_sums_heatmap(signoz, token, start_time_ms, end_time_ms, query, whole_range, no_cache=False)
|
||||
|
||||
# even though this shortened time range has no data below 4, all histogram
|
||||
# buckets are still returned back
|
||||
@@ -169,7 +173,7 @@ def test_builder_narrowing_a_histogram(
|
||||
assert uncached.status_code == HTTPStatus.OK, uncached.text
|
||||
|
||||
for source, response in (("uncached", uncached), ("from cache", from_cache)):
|
||||
assert get_heatmap_buckets(response.json(), "A") == [1, 2, 4, 8], source
|
||||
assert get_buckets(response.json(), "A") == [1, 2, 4, 8], source
|
||||
assert [column["values"] for column in get_heatmap_columns(response.json(), "A")] == [
|
||||
[0, 0, 0, 10, 0],
|
||||
[0, 0, 0, 10, 0],
|
||||
@@ -179,6 +183,7 @@ def test_builder_narrowing_a_histogram(
|
||||
], source
|
||||
|
||||
assert_identical_query_response(from_cache, uncached)
|
||||
assert_distribution_sums_heatmap(signoz, token, half_start_ms, end_time_ms, query, uncached, no_cache=False)
|
||||
|
||||
|
||||
def test_builder_shortening_the_time_range_at_the_end(
|
||||
@@ -222,13 +227,14 @@ def test_builder_shortening_the_time_range_at_the_end(
|
||||
|
||||
# 100 wide buckets, and the two maxes are 150 and 850, so the axis runs from
|
||||
# the bottom of (100, 200] to the top of (800, 900]
|
||||
assert get_heatmap_buckets(base_query.json(), "A") == pytest.approx([100, 200, 300, 400, 500, 600, 700, 800, 900])
|
||||
assert get_buckets(base_query.json(), "A") == pytest.approx([100, 200, 300, 400, 500, 600, 700, 800, 900])
|
||||
base_columns = get_heatmap_columns(base_query.json(), "A")
|
||||
assert [column["values"] for column in base_columns] == [
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
]
|
||||
assert [column.get("partial", False) for column in base_columns] == [False, False]
|
||||
assert_distribution_sums_heatmap(signoz, token, start_time_ms, end_time_ms_base_query, query, base_query, no_cache=False)
|
||||
|
||||
from_cache = make_query_request(signoz, token, start_time_ms, end_time_ms_shortened_query, query, request_type=RequestType.HEATMAP, no_cache=False)
|
||||
assert from_cache.status_code == HTTPStatus.OK, from_cache.text
|
||||
@@ -240,7 +246,7 @@ def test_builder_shortening_the_time_range_at_the_end(
|
||||
# is 250 and which comes back partial. Nothing in this window passes 300, so
|
||||
# the axis stops there rather than carrying the buckets above it
|
||||
for label, response in (("uncached", uncached), ("from cache", from_cache)):
|
||||
assert get_heatmap_buckets(response.json(), "A") == pytest.approx([100, 200, 300]), label
|
||||
assert get_buckets(response.json(), "A") == pytest.approx([100, 200, 300]), label
|
||||
columns = get_heatmap_columns(response.json(), "A")
|
||||
assert [column["values"] for column in columns] == [
|
||||
[0, 1, 0, 0],
|
||||
@@ -249,6 +255,7 @@ def test_builder_shortening_the_time_range_at_the_end(
|
||||
assert [column.get("partial", False) for column in columns] == [False, True], label
|
||||
|
||||
assert_identical_query_response(from_cache, uncached)
|
||||
assert_distribution_sums_heatmap(signoz, token, start_time_ms, end_time_ms_shortened_query, query, uncached, no_cache=False)
|
||||
|
||||
|
||||
def test_builder_shortening_the_time_range_at_the_start(
|
||||
@@ -294,13 +301,14 @@ def test_builder_shortening_the_time_range_at_the_start(
|
||||
|
||||
# 100 wide buckets, and the two maxes are 950 and 350, so the axis runs from
|
||||
# the bottom of (300, 400] to the top of (900, 1000]
|
||||
assert get_heatmap_buckets(base_query.json(), "A") == pytest.approx([300, 400, 500, 600, 700, 800, 900, 1000])
|
||||
assert get_buckets(base_query.json(), "A") == pytest.approx([300, 400, 500, 600, 700, 800, 900, 1000])
|
||||
base_columns = get_heatmap_columns(base_query.json(), "A")
|
||||
assert [column["values"] for column in base_columns] == [
|
||||
[0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0, 0, 0, 0],
|
||||
]
|
||||
assert [column.get("partial", False) for column in base_columns] == [False, False]
|
||||
assert_distribution_sums_heatmap(signoz, token, start_time_ms_base_query, end_time_ms, query, base_query, no_cache=False)
|
||||
|
||||
from_cache = make_query_request(signoz, token, start_time_ms_shortened_query, end_time_ms, query, request_type=RequestType.HEATMAP, no_cache=False)
|
||||
assert from_cache.status_code == HTTPStatus.OK, from_cache.text
|
||||
@@ -312,7 +320,7 @@ def test_builder_shortening_the_time_range_at_the_start(
|
||||
# clipping its counts, which still cover the whole step and so reach the 950
|
||||
# at minute 0, leaving the axis where the base query drew it
|
||||
for label, response in (("uncached", uncached), ("from cache", from_cache)):
|
||||
assert get_heatmap_buckets(response.json(), "A") == pytest.approx([300, 400, 500, 600, 700, 800, 900, 1000]), label
|
||||
assert get_buckets(response.json(), "A") == pytest.approx([300, 400, 500, 600, 700, 800, 900, 1000]), label
|
||||
columns = get_heatmap_columns(response.json(), "A")
|
||||
assert [column["values"] for column in columns] == [
|
||||
[0, 0, 0, 0, 0, 0, 0, 1, 0],
|
||||
@@ -321,6 +329,7 @@ def test_builder_shortening_the_time_range_at_the_start(
|
||||
assert [column.get("partial", False) for column in columns] == [True, False], label
|
||||
|
||||
assert_identical_query_response(from_cache, uncached)
|
||||
assert_distribution_sums_heatmap(signoz, token, start_time_ms_shortened_query, end_time_ms, query, uncached, no_cache=False)
|
||||
|
||||
|
||||
def test_builder_refreshing_a_sliding_time_range(
|
||||
@@ -382,7 +391,7 @@ def test_builder_refreshing_a_sliding_time_range(
|
||||
from_cache = make_query_request(signoz, token, refresh_start_ms, refresh_start_ms + 4 * MINUTE_MS, query, request_type=RequestType.HEATMAP, no_cache=False)
|
||||
assert from_cache.status_code == HTTPStatus.OK, from_cache.text
|
||||
|
||||
assert get_heatmap_buckets(from_cache.json(), "A") == pytest.approx(expected_buckets), f"refresh {refresh}"
|
||||
assert get_buckets(from_cache.json(), "A") == pytest.approx(expected_buckets), f"refresh {refresh}"
|
||||
|
||||
# a column served twice, dropped, or carried over from an earlier refresh
|
||||
# breaks the diagonal or the run of timestamps
|
||||
@@ -394,6 +403,7 @@ def test_builder_refreshing_a_sliding_time_range(
|
||||
refresh_start_ms + 3 * MINUTE_MS,
|
||||
], f"refresh {refresh}"
|
||||
assert [column["values"] for column in columns] == expected_columns, f"refresh {refresh}"
|
||||
assert_distribution_sums_heatmap(signoz, token, refresh_start_ms, refresh_start_ms + 4 * MINUTE_MS, query, from_cache, no_cache=False)
|
||||
|
||||
uncached = make_query_request(signoz, token, refresh_start_ms, refresh_start_ms + 4 * MINUTE_MS, query, request_type=RequestType.HEATMAP, no_cache=True)
|
||||
assert uncached.status_code == HTTPStatus.OK, uncached.text
|
||||
@@ -453,7 +463,7 @@ def test_promql_running_the_same_query_twice(
|
||||
# promql reports a column at the instant the range closes, and the second
|
||||
# run, answered out of what the first one cached, has to keep it
|
||||
for run, response in (("first", first), ("second", second)):
|
||||
assert get_heatmap_buckets(response.json(), "A") == [1, 2, 4], run
|
||||
assert get_buckets(response.json(), "A") == [1, 2, 4], run
|
||||
## what the query returns per `le` is cumulative across `le`, so each
|
||||
## count is its own minus the one below it, and `le=+Inf` has no finite
|
||||
## bound to sit on and lands in the trailing slot. increase over a 2m
|
||||
@@ -465,6 +475,10 @@ def test_promql_running_the_same_query_twice(
|
||||
], f"{run} run"
|
||||
|
||||
assert_identical_query_response(first, second)
|
||||
# a distribution answered out of what its own first run cached keeps that
|
||||
# closing column too
|
||||
for _ in ("first", "second"):
|
||||
assert_distribution_sums_heatmap(signoz, token, start_time_ms, end_time_ms, query, second, no_cache=False)
|
||||
|
||||
|
||||
def test_promql_shifting_the_time_range(
|
||||
@@ -510,7 +524,7 @@ def test_promql_shifting_the_time_range(
|
||||
aligned_and_cached = make_query_request(signoz, token, aligned_start_time_ms, aligned_end_time_ms, query, request_type=RequestType.HEATMAP, no_cache=False)
|
||||
assert aligned_and_cached.status_code == HTTPStatus.OK, aligned_and_cached.text
|
||||
|
||||
assert get_heatmap_buckets(aligned_and_cached.json(), "A") == [1, 2, 4]
|
||||
assert get_buckets(aligned_and_cached.json(), "A") == [1, 2, 4]
|
||||
## each column reads the counters at their latest sample at or before its
|
||||
## timestamp, and a bucket holds its own `le`'s count less the one below it.
|
||||
aligned_columns = [
|
||||
@@ -520,14 +534,16 @@ def test_promql_shifting_the_time_range(
|
||||
(aligned_end_time_ms, [1000, 2000, 3000, 4000]), # t = 3m, the tenth
|
||||
]
|
||||
assert [(column["timestamp"], column["values"]) for column in get_heatmap_columns(aligned_and_cached.json(), "A")] == aligned_columns
|
||||
assert_distribution_sums_heatmap(signoz, token, aligned_start_time_ms, aligned_end_time_ms, query, aligned_and_cached, no_cache=False)
|
||||
|
||||
# an unaligned range is rounded down to the step, so this asks for the same
|
||||
# instants as the aligned query and gets the same answer
|
||||
unaligned_and_cached = make_query_request(signoz, token, unaligned_start_time_ms, unaligned_end_time_ms, query, request_type=RequestType.HEATMAP, no_cache=False)
|
||||
assert unaligned_and_cached.status_code == HTTPStatus.OK, unaligned_and_cached.text
|
||||
assert get_heatmap_buckets(unaligned_and_cached.json(), "A") == [1, 2, 4], "unaligned query, rounded to the step"
|
||||
assert get_buckets(unaligned_and_cached.json(), "A") == [1, 2, 4], "unaligned query, rounded to the step"
|
||||
assert [(column["timestamp"], column["values"]) for column in get_heatmap_columns(unaligned_and_cached.json(), "A")] == aligned_columns, "unaligned query, rounded to the step"
|
||||
assert_identical_query_response(unaligned_and_cached, aligned_and_cached)
|
||||
assert_distribution_sums_heatmap(signoz, token, unaligned_start_time_ms, unaligned_end_time_ms, query, unaligned_and_cached, no_cache=False)
|
||||
|
||||
# skipping the alignment keeps the range as asked, so every column sits 30s
|
||||
# off the cached ones, on samples the aligned query never saw. Such a query
|
||||
@@ -542,5 +558,6 @@ def test_promql_shifting_the_time_range(
|
||||
for run in ("first", "second"):
|
||||
unaligned_as_asked = make_query_request(signoz, token, unaligned_start_time_ms, unaligned_end_time_ms, unaligned_query, request_type=RequestType.HEATMAP, no_cache=False)
|
||||
assert unaligned_as_asked.status_code == HTTPStatus.OK, unaligned_as_asked.text
|
||||
assert get_heatmap_buckets(unaligned_as_asked.json(), "A") == [1, 2, 4], f"unaligned query as asked, {run} run"
|
||||
assert get_buckets(unaligned_as_asked.json(), "A") == [1, 2, 4], f"unaligned query as asked, {run} run"
|
||||
assert [(column["timestamp"], column["values"]) for column in get_heatmap_columns(unaligned_as_asked.json(), "A")] == unaligned_columns, f"unaligned query as asked, {run} run"
|
||||
assert_distribution_sums_heatmap(signoz, token, unaligned_start_time_ms, unaligned_end_time_ms, unaligned_query, unaligned_as_asked, no_cache=False)
|
||||
|
||||
Reference in New Issue
Block a user