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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/api/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8558,6 +8558,7 @@ components:
- resource
- attribute
- body
- ""
type: string
TelemetrytypesFieldDataType:
enum:
Expand All @@ -8566,6 +8567,7 @@ components:
- float64
- int64
- number
- ""
type: string
TelemetrytypesGettableFieldKeys:
properties:
Expand Down Expand Up @@ -8597,6 +8599,7 @@ components:
- traces
- logs
- metrics
- ""
type: string
TelemetrytypesSource:
enum:
Expand Down
6 changes: 3 additions & 3 deletions ee/query-service/rules/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,17 +240,17 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
mock := mockStore.Mock()

// Mock the fingerprint query (for Prometheus label matching)
// args: $1=metric_name (the __name__ matcher maps onto the column)
mock.ExpectQuery("SELECT fingerprint, any").
WithArgs("test_metric", "__name__", "test_metric").
WithArgs("test_metric").
WillReturnRows(fingerprintRows)

// Mock the samples query (for Prometheus metric data)
// args: metric_name IN (discovered names), subquery metric_name, start, end
mock.ExpectQuery("SELECT metric_name, fingerprint, unix_milli").
WithArgs(
"test_metric",
"test_metric",
"__name__",
"test_metric",
queryStart,
queryEnd,
).
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/api/generated/services/sigNoz.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3479,18 +3479,21 @@ export enum TelemetrytypesFieldContextDTO {
resource = 'resource',
attribute = 'attribute',
body = 'body',
'' = '',
}
export enum TelemetrytypesFieldDataTypeDTO {
string = 'string',
bool = 'bool',
float64 = 'float64',
int64 = 'int64',
number = 'number',
'' = '',
}
export enum TelemetrytypesSignalDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
'' = '',
}
export interface Querybuildertypesv5GroupByKeyDTO {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import { sortBy } from 'lodash-es';
export type VariableType = 'QUERY' | 'CUSTOM' | 'TEXT' | 'DYNAMIC';

/** Telemetry signal — the generated enum (traces / logs / metrics). */
export type TelemetrySignal = TelemetrytypesSignalDTO;
// A query/variable signal is only logs/traces/metrics. TelemetrytypesSignalDTO
// also carries the empty "any" value used on field keys, which is not a valid
// query/variable signal, so exclude it here.
export type TelemetrySignal =
| TelemetrytypesSignalDTO.logs
| TelemetrytypesSignalDTO.traces
| TelemetrytypesSignalDTO.metrics;

/**
* Signal selected in the dynamic-variable editor. `'all'` is UI-only (the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ const SIGNAL_LABEL: Record<TelemetrytypesSignalDTO, string> = {
[TelemetrytypesSignalDTO.logs]: 'logs',
[TelemetrytypesSignalDTO.traces]: 'traces',
[TelemetrytypesSignalDTO.metrics]: 'metrics',
// The empty "any" signal only appears on field keys, never on a panel query;
// mapped for exhaustiveness.
[TelemetrytypesSignalDTO['']]: '',
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ export function useDrilldownDashboardVariables({
type: 'DYNAMIC',
multiSelect: true,
dynamicAttribute: fieldName,
dynamicSignal: signal ?? DYNAMIC_SIGNAL_ALL,
// `||` (not `??`): an empty "any" signal maps to All, same as unset.
dynamicSignal: signal || DYNAMIC_SIGNAL_ALL,
};
try {
await patchAsync(
Expand Down
12 changes: 7 additions & 5 deletions pkg/prometheus/clickhouseprometheus/capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,21 @@ func (c *captureClient) Read(ctx context.Context, query *prompb.Query, _ bool) (
}
}

var metricName string
// Without executing the series lookup, only an exact-name selector's
// metric name is known.
var metricNames []string
for _, matcher := range query.Matchers {
if matcher.Name == "__name__" {
metricName = matcher.Value
if matcher.Name == "__name__" && matcher.Type == prompb.LabelMatcher_EQ {
metricNames = []string{matcher.Value}
}
}

// Build the executing path's queries, but only record them.
subQuery, args, err := c.queryToClickhouseQuery(ctx, query, metricName, true)
sub, err := seriesLookupQuery(query, true)
if err != nil {
return nil, err
}
samplesQuery, samplesArgs := buildSamplesQuery(int64(query.StartTimestampMs), int64(query.EndTimestampMs), metricName, subQuery, args)
samplesQuery, samplesArgs := buildSamplesQuery(int64(query.StartTimestampMs), int64(query.EndTimestampMs), metricNames, sub)
c.recorder.record(samplesQuery, samplesArgs)

return storage.EmptySeriesSet(), nil
Expand Down
153 changes: 90 additions & 63 deletions pkg/prometheus/clickhouseprometheus/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import (
"context"
"fmt"
"math"
"strconv"
"strings"
"sort"
"sync"

"github.com/SigNoz/signoz/pkg/errors"
Expand All @@ -15,6 +14,7 @@ import (
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/cespare/xxhash/v2"
"github.com/huandu/go-sqlbuilder"
promValue "github.com/prometheus/prometheus/model/value"
"github.com/prometheus/prometheus/prompb"
"github.com/prometheus/prometheus/storage"
Expand Down Expand Up @@ -56,33 +56,28 @@ func (client *client) Read(ctx context.Context, query *prompb.Query, sortSeries
}
}

var metricName string
for _, matcher := range query.Matchers {
if matcher.Name == "__name__" {
metricName = matcher.Value
}
}

clickhouseQuery, args, err := client.queryToClickhouseQuery(ctx, query, metricName, false)
lookup, err := seriesLookupQuery(query, false)
if err != nil {
return nil, err
}
lookupSQL, lookupArgs := lookup.BuildWithFlavor(sqlbuilder.ClickHouse)

fingerprints, err := client.getFingerprintsFromClickhouseQuery(ctx, clickhouseQuery, args)
fingerprints, metricNames, err := client.getFingerprintsFromClickhouseQuery(ctx, lookupSQL, lookupArgs)
if err != nil {
return nil, err
}
if len(fingerprints) == 0 {
return remote.FromQueryResult(sortSeries, new(prompb.QueryResult)), nil
}

clickhouseSubQuery, args, err := client.queryToClickhouseQuery(ctx, query, metricName, true)
sub, err := seriesLookupQuery(query, true)
if err != nil {
return nil, err
}
samplesSQL, samplesArgs := buildSamplesQuery(int64(query.StartTimestampMs), int64(query.EndTimestampMs), metricNames, sub)

res := new(prompb.QueryResult)
timeseries, err := client.querySamples(ctx, int64(query.StartTimestampMs), int64(query.EndTimestampMs), fingerprints, metricName, clickhouseSubQuery, args)
timeseries, err := client.querySamples(ctx, samplesSQL, samplesArgs, fingerprints)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -126,86 +121,115 @@ func (c *client) ReadMultiple(ctx context.Context, queries []*prompb.Query, sort
return storage.NewMergeSeriesSet(sets, 0, storage.ChainedSeriesMerge), nil
}

func (client *client) queryToClickhouseQuery(_ context.Context, query *prompb.Query, metricName string, subQuery bool) (string, []any, error) {
var clickHouseQuery string
var conditions []string
var argCount = 0
var selectString = "fingerprint, any(labels)"
// anchorRegex makes a pattern fully anchored, the way Prometheus compiles
// matcher regexes; ClickHouse's match() would otherwise substring-match.
func anchorRegex(pattern string) string {
return "^(?:" + pattern + ")$"
}

// seriesLookupQuery builds the time-series lookup. It returns a builder so
// the samples query can embed it as a subquery with the args merged in
// render order by the builder instead of hand-numbered placeholders.
func seriesLookupQuery(query *prompb.Query, subQuery bool) (*sqlbuilder.SelectBuilder, error) {
sb := sqlbuilder.NewSelectBuilder()
if subQuery {
argCount = 1
selectString = "fingerprint"
sb.Select("fingerprint")
} else {
sb.Select("fingerprint", "any(labels)")
}

start, end, tableName := getStartAndEndAndTableName(query.StartTimestampMs, query.EndTimestampMs)
sb.From(databaseName + "." + tableName)

var args []any
conditions = append(conditions, fmt.Sprintf("metric_name = $%d", argCount+1))
conditions = append(conditions, "temporality IN ['Cumulative', 'Unspecified']")
sb.Where("temporality IN ['Cumulative', 'Unspecified']")
// Inclusive upper bound: registration rows are hour-floored by the
// exporter, so a series first registered in the hour starting exactly at
// `end` would otherwise be invisible while its samples (<= end) are in
// range.
conditions = append(conditions, fmt.Sprintf("unix_milli >= %d AND unix_milli <= %d", start, end))
sb.Where(fmt.Sprintf("unix_milli >= %d AND unix_milli <= %d", start, end))

args = append(args, metricName)
for _, m := range query.Matchers {
if m.Name == "__name__" {
// __name__ maps onto the metric_name column per matcher type;
// reducing regex/negated/absent name matchers to one equality
// made such selectors silently return empty.
switch m.Type {
case prompb.LabelMatcher_EQ:
sb.Where(sb.E("metric_name", m.Value))
case prompb.LabelMatcher_NEQ:
sb.Where(sb.NE("metric_name", m.Value))
case prompb.LabelMatcher_RE:
sb.Where(fmt.Sprintf("match(metric_name, %s)", sb.Var(anchorRegex(m.Value))))
case prompb.LabelMatcher_NRE:
sb.Where(fmt.Sprintf("not match(metric_name, %s)", sb.Var(anchorRegex(m.Value))))
default:
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported or invalid matcher type: %s", m.Type.String())
}
continue
}
switch m.Type {
case prompb.LabelMatcher_EQ:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, $%d) = $%d", argCount+2, argCount+3))
sb.Where(fmt.Sprintf("JSONExtractString(labels, %s) = %s", sb.Var(m.Name), sb.Var(m.Value)))
case prompb.LabelMatcher_NEQ:
conditions = append(conditions, fmt.Sprintf("JSONExtractString(labels, $%d) != $%d", argCount+2, argCount+3))
sb.Where(fmt.Sprintf("JSONExtractString(labels, %s) != %s", sb.Var(m.Name), sb.Var(m.Value)))
case prompb.LabelMatcher_RE:
conditions = append(conditions, fmt.Sprintf("match(JSONExtractString(labels, $%d), $%d)", argCount+2, argCount+3))
sb.Where(fmt.Sprintf("match(JSONExtractString(labels, %s), %s)", sb.Var(m.Name), sb.Var(anchorRegex(m.Value))))
case prompb.LabelMatcher_NRE:
conditions = append(conditions, fmt.Sprintf("not match(JSONExtractString(labels, $%d), $%d)", argCount+2, argCount+3))
sb.Where(fmt.Sprintf("not match(JSONExtractString(labels, %s), %s)", sb.Var(m.Name), sb.Var(anchorRegex(m.Value))))
default:
return "", nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported or invalid matcher type: %s", m.Type.String())
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported or invalid matcher type: %s", m.Type.String())
}
args = append(args, m.Name, m.Value)
argCount += 2
}

whereClause := strings.Join(conditions, " AND ")

clickHouseQuery = fmt.Sprintf(`SELECT %s FROM %s.%s WHERE %s GROUP BY fingerprint`, selectString, databaseName, tableName, whereClause)

return clickHouseQuery, args, nil
sb.GroupBy("fingerprint")
return sb, nil
}

func (client *client) getFingerprintsFromClickhouseQuery(ctx context.Context, query string, args []any) (map[uint64][]prompb.Label, error) {
func (client *client) getFingerprintsFromClickhouseQuery(ctx context.Context, query string, args []any) (map[uint64][]prompb.Label, []string, error) {
ctx = client.withClickhousePrometheusContext(ctx, "getFingerprintsFromClickhouseQuery")
rows, err := client.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, err
return nil, nil, err
}
defer rows.Close()

fingerprints := make(map[uint64][]prompb.Label)
nameSet := make(map[string]struct{})

var fingerprint uint64
var labelString string
for rows.Next() {
if err = rows.Scan(&fingerprint, &labelString); err != nil {
return nil, err
return nil, nil, err
}

labels, _, err := unmarshalLabels(labelString)
labels, metricName, err := unmarshalLabels(labelString)
if err != nil {
return nil, err
return nil, nil, err
}

fingerprints[fingerprint] = labels
if metricName != "" {
nameSet[metricName] = struct{}{}
}
}

if err := rows.Err(); err != nil {
return nil, err
return nil, nil, err
}

metricNames := make([]string, 0, len(nameSet))
for name := range nameSet {
metricNames = append(metricNames, name)
}
sort.Strings(metricNames)

return fingerprints, nil
return fingerprints, metricNames, nil
}

// buildSamplesQuery renders the samples SQL (and args) that fetches data
// points for the series selected by subQuery.
// buildSamplesQuery renders the samples SQL for the series selected by
// subQuery. The metric_name condition exists only for primary-key pruning;
// the fingerprint filter already selects the right rows.
//
// Time bounds are inclusive on both ends because that is Prometheus's
// storage contract: Select(mint, maxt) returns [start, end] and the engine
Expand All @@ -215,34 +239,37 @@ func (client *client) getFingerprintsFromClickhouseQuery(ctx context.Context, qu
// its own model — toStartOfInterval buckets covering [t, t+step), where a
// sample at `end` falls in an unrendered bucket and end-exclusive ranges
// tile exactly across cached time slices.
func buildSamplesQuery(start int64, end int64, metricName string, subQuery string, args []any) (string, []any) {
argCount := len(args)

query := fmt.Sprintf(`
SELECT metric_name, fingerprint, unix_milli, value, flags
FROM %s.%s
WHERE metric_name = $1 AND fingerprint GLOBAL IN (%s) AND unix_milli >= $%s AND unix_milli <= $%s ORDER BY fingerprint, unix_milli;`,
databaseName, distributedSamplesV4, subQuery, strconv.Itoa(argCount+2), strconv.Itoa(argCount+3))
query = strings.TrimSpace(query)

allArgs := append([]any{metricName}, args...)
allArgs = append(allArgs, start, end)
return query, allArgs
func buildSamplesQuery(start int64, end int64, metricNames []string, sub *sqlbuilder.SelectBuilder) (string, []any) {
sb := sqlbuilder.NewSelectBuilder()
sb.Select("metric_name", "fingerprint", "unix_milli", "value", "flags")
sb.From(databaseName + "." + distributedSamplesV4)

if len(metricNames) > 0 {
names := make([]any, len(metricNames))
for i, name := range metricNames {
names[i] = name
}
sb.Where(sb.In("metric_name", names...))
}
sb.Where(fmt.Sprintf("fingerprint GLOBAL IN (%s)", sb.Var(sub)))
sb.Where(sb.GTE("unix_milli", start), sb.LTE("unix_milli", end))
sb.OrderBy("fingerprint", "unix_milli")

return sb.BuildWithFlavor(sqlbuilder.ClickHouse)
}

func (client *client) querySamples(ctx context.Context, start int64, end int64, fingerprints map[uint64][]prompb.Label, metricName string, subQuery string, args []any) ([]*prompb.TimeSeries, error) {
func (client *client) querySamples(ctx context.Context, query string, args []any, fingerprints map[uint64][]prompb.Label) ([]*prompb.TimeSeries, error) {
ctx = client.withClickhousePrometheusContext(ctx, "querySamples")

query, allArgs := buildSamplesQuery(start, end, metricName, subQuery, args)

rows, err := client.telemetryStore.ClickhouseDB().Query(ctx, query, allArgs...)
rows, err := client.telemetryStore.ClickhouseDB().Query(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()

var res []*prompb.TimeSeries
var ts *prompb.TimeSeries
var metricName string
var fingerprint, prevFingerprint uint64
var timestampMs, prevTimestamp int64
var value float64
Expand Down
Loading
Loading