mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-18 06:20:34 +01:00
Compare commits
8 Commits
feat/panel
...
issue-5378
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbe0a5f625 | ||
|
|
9a6fda4bf9 | ||
|
|
129ff8dce2 | ||
|
|
11c56d2010 | ||
|
|
7c85c63555 | ||
|
|
03796f012f | ||
|
|
a06900bbff | ||
|
|
f74ca236fa |
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Skeleton } from 'antd';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
@@ -124,24 +123,14 @@ function ServiceOverview({
|
||||
/>
|
||||
<Card data-testid="service_latency">
|
||||
<GraphContainer>
|
||||
{topLevelOperationsIsLoading && (
|
||||
<Skeleton
|
||||
style={{
|
||||
height: '100%',
|
||||
padding: '16px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!topLevelOperationsIsLoading && (
|
||||
<Graph
|
||||
onDragSelect={onDragSelect}
|
||||
widget={latencyWidget}
|
||||
onClickHandler={handleGraphClick('Service')}
|
||||
isQueryEnabled={isQueryEnabled}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
)}
|
||||
<Graph
|
||||
onDragSelect={onDragSelect}
|
||||
widget={latencyWidget}
|
||||
onClickHandler={handleGraphClick('Service')}
|
||||
isQueryEnabled={isQueryEnabled}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
</Card>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Skeleton } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import axios from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
@@ -29,24 +28,14 @@ function TopLevelOperation({
|
||||
</Typography>
|
||||
) : (
|
||||
<GraphContainer>
|
||||
{topLevelOperationsIsLoading && (
|
||||
<Skeleton
|
||||
style={{
|
||||
height: '100%',
|
||||
padding: '16px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!topLevelOperationsIsLoading && (
|
||||
<Graph
|
||||
widget={widget}
|
||||
onClickHandler={handleGraphClick(opName)}
|
||||
onDragSelect={onDragSelect}
|
||||
isQueryEnabled={!topLevelOperationsIsLoading}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
)}
|
||||
<Graph
|
||||
widget={widget}
|
||||
onClickHandler={handleGraphClick(opName)}
|
||||
onDragSelect={onDragSelect}
|
||||
isQueryEnabled={!topLevelOperationsIsLoading}
|
||||
version={ENTITY_VERSION_V4}
|
||||
enableDrillDown={SERVICE_DETAIL_DRILLDOWN_ENABLED}
|
||||
/>
|
||||
</GraphContainer>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -22,7 +22,7 @@ func newConfig() factory.Config {
|
||||
Agent: AgentConfig{
|
||||
// we will maintain the latest version of cloud integration agent from here,
|
||||
// till we automate it externally or figure out a way to validate it.
|
||||
Version: "v0.0.12",
|
||||
Version: "v0.0.13",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,11 +119,7 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
queries := make(map[string]qbtypes.Query)
|
||||
steps := make(map[string]qbtypes.Step)
|
||||
|
||||
// Resolve metric metadata once per request: patches each metric-aggregation
|
||||
// query's spec in place, returns the queries whose every aggregation was
|
||||
// missing (used for preseeded empty results), and any dormant-metric
|
||||
// warning string. NotFound errors for never-seen metrics are propagated.
|
||||
missingMetricQueries, dormantMetricsWarningMsg, err := q.resolveMetricMetadata(ctx, req.CompositeQuery.Queries, req.Start, req.End)
|
||||
missingMetricQueries, metricWarnings, err := q.resolveMetricMetadata(ctx, req.CompositeQuery.Queries, req.Start, req.End)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -240,13 +236,15 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
}
|
||||
}
|
||||
}
|
||||
if dormantMetricsWarningMsg != "" {
|
||||
if len(metricWarnings) > 0 {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{}
|
||||
}
|
||||
qbResp.Warning.Warnings = append(qbResp.Warning.Warnings, qbtypes.QueryWarnDataAdditional{
|
||||
Message: dormantMetricsWarningMsg,
|
||||
})
|
||||
for _, w := range metricWarnings {
|
||||
qbResp.Warning.Warnings = append(qbResp.Warning.Warnings, qbtypes.QueryWarnDataAdditional{
|
||||
Message: w,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return qbResp, qbErr
|
||||
@@ -302,12 +300,11 @@ func (q *querier) populateQBEvent(event *qbtypes.QBEvent, queries []qbtypes.Quer
|
||||
// - missingMetricQueries: names of queries whose every aggregation was
|
||||
// missing. Used downstream to preseed empty result placeholders so the
|
||||
// response still has an entry per requested query name.
|
||||
// - dormantWarning: a human-readable warning describing metrics that exist in
|
||||
// the store but produced no data within the query window. Empty when no
|
||||
// such metrics are present.
|
||||
// - err: NotFound when one or more referenced metrics have never been seen,
|
||||
// or Internal when a metadata fetch fails.
|
||||
func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.QueryEnvelope, start, end uint64) (missingMetricQueries []string, dormantWarning string, err error) {
|
||||
// - metricWarnings: human-readable warnings for metrics that could not be
|
||||
// resolved: never-seen metrics and dormant metrics (seen but no data in
|
||||
// the query window).
|
||||
// - err: Internal when a metadata fetch fails.
|
||||
func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.QueryEnvelope, start, end uint64) (missingMetricQueries []string, metricWarnings []string, err error) {
|
||||
metricNames := make([]string, 0)
|
||||
for idx := range queries {
|
||||
if queries[idx].Type != qbtypes.QueryTypeBuilder {
|
||||
@@ -325,13 +322,13 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.Q
|
||||
}
|
||||
|
||||
if len(metricNames) == 0 {
|
||||
return nil, "", nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
metricTemporality, metricTypes, err := q.metadataStore.FetchTemporalityAndTypeMulti(ctx, start, end, metricNames...)
|
||||
if err != nil {
|
||||
q.logger.WarnContext(ctx, "failed to fetch metric temporality", errors.Attr(err), slog.Any("metrics", metricNames))
|
||||
return nil, "", errors.NewInternalf(errors.CodeInternal, "failed to fetch metrics temporality")
|
||||
return nil, nil, errors.NewInternalf(errors.CodeInternal, "failed to fetch metrics temporality")
|
||||
}
|
||||
q.logger.DebugContext(ctx, "fetched metric temporalities and types", slog.Any("metric_temporality", metricTemporality), slog.Any("metric_types", metricTypes))
|
||||
|
||||
@@ -363,7 +360,7 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.Q
|
||||
}
|
||||
// Type is resolved now; validate aggregation compatibility against it.
|
||||
if err := spec.Aggregations[i].ValidateForType(); err != nil {
|
||||
return nil, "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
presentAggregations = append(presentAggregations, spec.Aggregations[i])
|
||||
}
|
||||
@@ -376,7 +373,7 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.Q
|
||||
}
|
||||
|
||||
if len(missingMetrics) == 0 {
|
||||
return missingMetricQueries, "", nil
|
||||
return missingMetricQueries, nil, nil
|
||||
}
|
||||
|
||||
isInternalMetric := func(n string) bool { return strings.HasPrefix(n, "signoz.") || strings.HasPrefix(n, "signoz_") }
|
||||
@@ -387,29 +384,33 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.Q
|
||||
}
|
||||
}
|
||||
if len(externalMissingMetrics) == 0 {
|
||||
// this means all missing metrics are internal, and since internal metrics
|
||||
// aren't user-controlled, skip errors/warnings for them since users can't act on them
|
||||
return missingMetricQueries, "", nil
|
||||
return missingMetricQueries, nil, nil
|
||||
}
|
||||
|
||||
// Classify each missing metric: never-seen → NotFound error; seen-but-no-
|
||||
// data-in-window → dormant warning.
|
||||
// Classify each missing metric: never-seen -> warning with empty result;
|
||||
// seen-but-no-data-in-window -> dormant warning.
|
||||
lastSeenInfo, _ := q.metadataStore.FetchLastSeenInfoMulti(ctx, externalMissingMetrics...)
|
||||
nonExistentMetrics := []string{}
|
||||
var nonExistentMetrics []string
|
||||
var dormantMetrics []string
|
||||
for _, name := range externalMissingMetrics {
|
||||
if ts, ok := lastSeenInfo[name]; ok && ts > 0 {
|
||||
dormantMetrics = append(dormantMetrics, name)
|
||||
continue
|
||||
}
|
||||
nonExistentMetrics = append(nonExistentMetrics, name)
|
||||
}
|
||||
|
||||
var warnings []string
|
||||
|
||||
// Never-seen metrics: the query already gets a preseeded empty result
|
||||
// via the aggregation-dropping path above; we just attach a warning.
|
||||
if len(nonExistentMetrics) == 1 {
|
||||
return nil, "", errors.NewNotFoundf(errors.CodeNotFound, "could not find the metric %s", nonExistentMetrics[0])
|
||||
}
|
||||
if len(nonExistentMetrics) > 1 {
|
||||
return nil, "", errors.NewNotFoundf(errors.CodeNotFound, "the following metrics were not found: %s", strings.Join(nonExistentMetrics, ", "))
|
||||
warnings = append(warnings, fmt.Sprintf("metric %s has never been received. Check the metric name and instrumentation", nonExistentMetrics[0]))
|
||||
} else if len(nonExistentMetrics) > 1 {
|
||||
warnings = append(warnings, fmt.Sprintf("the following metrics have never been received. Check the metric names and instrumentation: %s", strings.Join(nonExistentMetrics, ", ")))
|
||||
}
|
||||
|
||||
// All missing metrics are dormant — assemble the warning string.
|
||||
// Dormant metrics: seen before but no data in the query window.
|
||||
lastSeenStr := func(name string) string {
|
||||
if ts, ok := lastSeenInfo[name]; ok && ts > 0 {
|
||||
ago := humanize.RelTime(time.UnixMilli(ts), time.Now(), "ago", "from now")
|
||||
@@ -417,16 +418,16 @@ func (q *querier) resolveMetricMetadata(ctx context.Context, queries []qbtypes.Q
|
||||
}
|
||||
return name
|
||||
}
|
||||
if len(externalMissingMetrics) == 1 {
|
||||
dormantWarning = fmt.Sprintf("no data found for the metric %s in the query time range", lastSeenStr(missingMetrics[0]))
|
||||
} else {
|
||||
parts := make([]string, len(externalMissingMetrics))
|
||||
for i, m := range externalMissingMetrics {
|
||||
if len(dormantMetrics) == 1 {
|
||||
warnings = append(warnings, fmt.Sprintf("no data found for the metric %s in the query time range", lastSeenStr(dormantMetrics[0])))
|
||||
} else if len(dormantMetrics) > 1 {
|
||||
parts := make([]string, len(dormantMetrics))
|
||||
for i, m := range dormantMetrics {
|
||||
parts[i] = lastSeenStr(m)
|
||||
}
|
||||
dormantWarning = fmt.Sprintf("no data found for the following metrics in the query time range: %s", strings.Join(parts, ", "))
|
||||
warnings = append(warnings, fmt.Sprintf("no data found for the following metrics in the query time range: %s", strings.Join(parts, ", ")))
|
||||
}
|
||||
return missingMetricQueries, dormantWarning, nil
|
||||
return missingMetricQueries, warnings, nil
|
||||
}
|
||||
|
||||
func (q *querier) QueryRawStream(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, client *qbtypes.RawStream) {
|
||||
|
||||
@@ -37,7 +37,7 @@ func (m *mockMetricStmtBuilder) Build(_ context.Context, _, _ uint64, _ qbtypes.
|
||||
|
||||
func TestQueryRange_MetricTypeMissing(t *testing.T) {
|
||||
// When a metric has UnspecifiedType and is not found in the metadata store,
|
||||
// the querier should return a not-found error, even if the request provides a temporality
|
||||
// the querier should return an empty result with a warning instead of an error.
|
||||
providerSettings := instrumentationtest.New().ToProviderSettings()
|
||||
metadataStore := telemetrytypestest.NewMockMetadataStore()
|
||||
|
||||
@@ -80,9 +80,14 @@ func TestQueryRange_MetricTypeMissing(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, err := q.QueryRange(context.Background(), valuer.GenerateUUID(), req)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "could not find the metric unknown_metric")
|
||||
resp, err := q.QueryRange(context.Background(), valuer.GenerateUUID(), req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
require.NotNil(t, resp.Warning)
|
||||
|
||||
require.Len(t, resp.Warning.Warnings, 1)
|
||||
assert.Contains(t, resp.Warning.Warnings[0].Message, "unknown_metric")
|
||||
assert.Contains(t, resp.Warning.Warnings[0].Message, "has never been received")
|
||||
}
|
||||
|
||||
func TestQueryRange_MetricTypeFromStore(t *testing.T) {
|
||||
|
||||
@@ -101,9 +101,29 @@ func (b *MetricQueryStatementBuilder) Build(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pairFallbackWarnings []string
|
||||
for _, sel := range keySelectors {
|
||||
if _, ok := keys[sel.Name]; !ok {
|
||||
keys[sel.Name] = []*telemetrytypes.TelemetryFieldKey{{
|
||||
Name: sel.Name,
|
||||
FieldContext: telemetrytypes.FieldContextAttribute,
|
||||
FieldDataType: telemetrytypes.FieldDataTypeString,
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
}}
|
||||
pairFallbackWarnings = append(pairFallbackWarnings,
|
||||
fmt.Sprintf("key `%s` not found on metric %s", sel.Name, query.Aggregations[0].MetricName),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
start, end = querybuilder.AdjustedMetricTimeRange(start, end, uint64(query.StepInterval.Seconds()), query)
|
||||
|
||||
return b.buildPipelineStatement(ctx, start, end, query, keys, variables)
|
||||
stmt, err := b.buildPipelineStatement(ctx, start, end, query, keys, variables)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stmt.Warnings = append(stmt.Warnings, pairFallbackWarnings...)
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
func (b *MetricQueryStatementBuilder) buildPipelineStatement(
|
||||
|
||||
@@ -217,6 +217,39 @@ func TestStatementBuilder(t *testing.T) {
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "test_missing_key_falls_back_to_labels",
|
||||
requestType: qbtypes.RequestTypeTimeSeries,
|
||||
query: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
|
||||
Aggregations: []qbtypes.MetricAggregation{
|
||||
{
|
||||
MetricName: "signoz_calls_total",
|
||||
Type: metrictypes.SumType,
|
||||
Temporality: metrictypes.Cumulative,
|
||||
TimeAggregation: metrictypes.TimeAggregationRate,
|
||||
SpaceAggregation: metrictypes.SpaceAggregationSum,
|
||||
},
|
||||
},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "k8s.statefulset.name = 'my-statefulset'",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
|
||||
Name: "k8s.statefulset.name",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __temporal_aggregation_cte AS (SELECT ts, `k8s.statefulset.name`, multiIf(row_number() OVER rate_window = 1, nan, (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) < 0, per_series_value / (ts - lagInFrame(ts, 1) OVER rate_window), (per_series_value - lagInFrame(per_series_value, 1) OVER rate_window) / (ts - lagInFrame(ts, 1) OVER rate_window)) AS per_series_value FROM (SELECT fingerprint, toStartOfInterval(toDateTime(intDiv(unix_milli, 1000)), toIntervalSecond(30)) AS ts, `k8s.statefulset.name`, max(value) AS per_series_value FROM signoz_metrics.distributed_samples_v4 AS points INNER JOIN (SELECT fingerprint, JSONExtractString(labels, 'k8s.statefulset.name') AS `k8s.statefulset.name` FROM signoz_metrics.time_series_v4_6hrs WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli <= ? AND LOWER(temporality) LIKE LOWER(?) AND __normalized = ? AND JSONExtractString(labels, 'k8s.statefulset.name') = ? GROUP BY fingerprint, `k8s.statefulset.name`) AS filtered_time_series ON points.fingerprint = filtered_time_series.fingerprint WHERE metric_name IN (?) AND unix_milli >= ? AND unix_milli < ? GROUP BY fingerprint, ts, `k8s.statefulset.name` ORDER BY fingerprint, ts) WINDOW rate_window AS (PARTITION BY fingerprint ORDER BY fingerprint, ts)), __spatial_aggregation_cte AS (SELECT ts, `k8s.statefulset.name`, sum(per_series_value) AS value FROM __temporal_aggregation_cte WHERE isNaN(per_series_value) = ? GROUP BY ts, `k8s.statefulset.name`) SELECT * FROM __spatial_aggregation_cte ORDER BY `k8s.statefulset.name`, ts",
|
||||
Args: []any{"signoz_calls_total", uint64(1747936800000), uint64(1747983420000), "cumulative", false, "my-statefulset", "signoz_calls_total", uint64(1747947360000), uint64(1747983420000), 0},
|
||||
Warnings: []string{"key `k8s.statefulset.name` not found on metric signoz_calls_total"},
|
||||
},
|
||||
expectedErr: nil,
|
||||
},
|
||||
}
|
||||
|
||||
fm := NewFieldMapper()
|
||||
|
||||
@@ -614,7 +614,7 @@ def test_histogram_p90_returns_warning_outside_data_window(
|
||||
assert warnings[0]["message"].startswith(f"no data found for the metric {metric_name}")
|
||||
|
||||
|
||||
def test_non_existent_metrics_returns_404(
|
||||
def test_non_existent_metrics_returns_warning(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
@@ -635,9 +635,11 @@ def test_non_existent_metrics_returns_404(
|
||||
|
||||
start_2h = int((now - timedelta(hours=2)).timestamp() * 1000)
|
||||
response = make_query_request(signoz, token, start_2h, end_ms, [query])
|
||||
assert response.status_code == HTTPStatus.NOT_FOUND
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
assert get_error_message(response.json()) == "could not find the metric whatevergoennnsgoeshere"
|
||||
data = response.json()
|
||||
warnings = get_all_warnings(data)
|
||||
assert any("whatevergoennnsgoeshere" in w["message"] and "has never been received" in w["message"] for w in warnings), f"expected never-seen metric warning, got: {warnings}"
|
||||
|
||||
|
||||
def test_non_existent_internal_metrics_returns_no_warning(
|
||||
|
||||
Reference in New Issue
Block a user