mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-18 18:50:48 +01:00
Compare commits
3 Commits
test/semco
...
ns/githook
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1010908db | ||
|
|
b86e536432 | ||
|
|
a76a7ede70 |
@@ -1,7 +1,12 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend && pnpm run commitlint --edit $1
|
||||
set -e
|
||||
|
||||
if [ -d frontend/node_modules ] && command -v pnpm >/dev/null 2>&1; then
|
||||
(cd frontend && pnpm run commitlint --edit "$1")
|
||||
else
|
||||
echo "commit-msg: frontend dependencies not installed, skipping commitlint" >&2
|
||||
fi
|
||||
|
||||
branch="$(git rev-parse --abbrev-ref HEAD)"
|
||||
|
||||
30
.githooks/install
Executable file
30
.githooks/install
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
# Installs dispatcher stubs into the repository's hooks directory. Safe to run
|
||||
# repeatedly; invoked automatically from the Makefile and from the frontend
|
||||
# postinstall so no manual setup is required.
|
||||
|
||||
set -e
|
||||
|
||||
git rev-parse --git-dir >/dev/null 2>&1 || exit 0
|
||||
|
||||
# core.hooksPath shadows the hooks directory git would otherwise use, and an
|
||||
# absolute value leaks one worktree's path into every other worktree.
|
||||
if [ -n "$(git config --get core.hooksPath 2>/dev/null || true)" ]; then
|
||||
git config --unset-all core.hooksPath 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Resolves to the common directory for linked worktrees, which is where git
|
||||
# looks for hooks regardless of which worktree the commit happens in.
|
||||
hooks_dir="$(git rev-parse --git-path hooks)"
|
||||
mkdir -p "$hooks_dir"
|
||||
|
||||
for hook in pre-commit commit-msg; do
|
||||
cat >"$hooks_dir/$hook" <<EOF
|
||||
#!/bin/sh
|
||||
# Generated by .githooks/install.
|
||||
root="\$(git rev-parse --show-toplevel)"
|
||||
[ -x "\$root/.githooks/$hook" ] || exit 0
|
||||
exec "\$root/.githooks/$hook" "\$@"
|
||||
EOF
|
||||
chmod +x "$hooks_dir/$hook"
|
||||
done
|
||||
22
.githooks/pre-commit
Executable file
22
.githooks/pre-commit
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
staged_go_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.go')
|
||||
if [ -n "$staged_go_files" ]; then
|
||||
if command -v gofmt >/dev/null 2>&1; then
|
||||
echo "$staged_go_files" | xargs gofmt -l -w
|
||||
echo "$staged_go_files" | xargs git add
|
||||
else
|
||||
echo "pre-commit: gofmt not found, skipping go formatting" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
staged_web_files=$(git diff --cached --name-only --diff-filter=ACM -- 'frontend/*')
|
||||
if [ -n "$staged_web_files" ]; then
|
||||
if [ -d frontend/node_modules ] && command -v pnpm >/dev/null 2>&1; then
|
||||
cd frontend && pnpm lint-staged
|
||||
else
|
||||
echo "pre-commit: frontend dependencies not installed, skipping lint-staged" >&2
|
||||
fi
|
||||
fi
|
||||
7
Makefile
7
Makefile
@@ -35,6 +35,13 @@ DOCKERFILE_ENTERPRISE = $(SRC)/cmd/enterprise/Dockerfile
|
||||
DOCKER_REGISTRY_ENTERPRISE ?= docker.io/signoz/signoz
|
||||
JS_BUILD_CONTEXT = $(SRC)/frontend
|
||||
|
||||
##############################################################
|
||||
# git hooks
|
||||
##############################################################
|
||||
# Hooks live outside the working tree, so they cannot be checked in. Expanded
|
||||
# while this file is parsed, which wires them up on any make invocation.
|
||||
GITHOOKS_INSTALL := $(shell sh $(SRC)/.githooks/install >/dev/null 2>&1 || true)
|
||||
|
||||
##############################################################
|
||||
# directories
|
||||
##############################################################
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd frontend && pnpm lint-staged
|
||||
@@ -20,8 +20,7 @@
|
||||
"jest": "jest",
|
||||
"jest:coverage": "jest --coverage",
|
||||
"jest:watch": "jest --watch",
|
||||
"postinstall": "pnpm i18n:generate-hash && (is-ci || pnpm husky:configure) && node scripts/update-registry.cjs",
|
||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||
"postinstall": "pnpm i18n:generate-hash && (is-ci || sh ../.githooks/install) && node scripts/update-registry.cjs",
|
||||
"commitlint": "commitlint --edit $1",
|
||||
"test": "jest",
|
||||
"test:changedsince": "jest --changedSince=main --coverage --silent",
|
||||
@@ -186,7 +185,6 @@
|
||||
"@typescript/native-preview": "7.0.0-dev.20260430.1",
|
||||
"eslint-plugin-sonarjs": "4.0.2",
|
||||
"glob": "^13.0.6",
|
||||
"husky": "^7.0.4",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-styled-components": "^7.2.0",
|
||||
|
||||
10
frontend/pnpm-lock.yaml
generated
10
frontend/pnpm-lock.yaml
generated
@@ -440,9 +440,6 @@ importers:
|
||||
glob:
|
||||
specifier: ^13.0.6
|
||||
version: 13.0.6
|
||||
husky:
|
||||
specifier: ^7.0.4
|
||||
version: 7.0.4
|
||||
is-ci:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
@@ -5529,11 +5526,6 @@ packages:
|
||||
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
|
||||
engines: {node: '>=18.18.0'}
|
||||
|
||||
husky@7.0.4:
|
||||
resolution: {integrity: sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
hyphenate-style-name@1.0.4:
|
||||
resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==}
|
||||
|
||||
@@ -14636,8 +14628,6 @@ snapshots:
|
||||
|
||||
human-signals@8.0.1: {}
|
||||
|
||||
husky@7.0.4: {}
|
||||
|
||||
hyphenate-style-name@1.0.4: {}
|
||||
|
||||
i18next-browser-languagedetector@6.1.8:
|
||||
|
||||
@@ -2,14 +2,17 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Switch } from '@signozhq/ui/switch';
|
||||
import { Form, Select, Space } from 'antd';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { ModalFooterTitle } from 'container/PipelinePage/styles';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { ProcessorData } from 'types/api/pipeline/def';
|
||||
|
||||
import { formValidationRules } from '../config';
|
||||
import { processorFields, ProcessorFormField } from './config';
|
||||
import { ProcessorFormField } from './config';
|
||||
import CSVInput from './FormFields/CSVInput';
|
||||
import JsonFlattening from './FormFields/JsonFlattening';
|
||||
import { FormWrapper, PipelineIndexIcon, StyledSelect } from './styles';
|
||||
import { resolveProcessorFields } from './utils';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
@@ -133,16 +136,23 @@ function ProcessorForm({
|
||||
selectedProcessorData,
|
||||
isAdd,
|
||||
}: ProcessorFormProps): JSX.Element {
|
||||
const { featureFlags } = useAppContext();
|
||||
const isBodyJsonEnabled =
|
||||
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
|
||||
?.active || false;
|
||||
|
||||
return (
|
||||
<div className="processor-form-container">
|
||||
{processorFields[processorType]?.map((fieldData: ProcessorFormField) => (
|
||||
<ProcessorFieldInput
|
||||
key={fieldData.name + String(fieldData.initialValue)}
|
||||
fieldData={fieldData}
|
||||
selectedProcessorData={selectedProcessorData}
|
||||
isAdd={isAdd}
|
||||
/>
|
||||
))}
|
||||
{resolveProcessorFields(processorType, isBodyJsonEnabled).map(
|
||||
(fieldData: ProcessorFormField) => (
|
||||
<ProcessorFieldInput
|
||||
key={fieldData.name + String(fieldData.initialValue)}
|
||||
fieldData={fieldData}
|
||||
selectedProcessorData={selectedProcessorData}
|
||||
isAdd={isAdd}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { processorFields, ProcessorFormField } from './config';
|
||||
|
||||
const BODY_PARSE_FROM = 'body';
|
||||
const JSON_BODY_PARSE_FROM = 'body.message';
|
||||
|
||||
// With use_json_body the collector normalizes every body into a map before user
|
||||
// operators run, so a parser pointed at `body` gets a map it cannot read and
|
||||
// silently extracts nothing. The log text lives at body.message.
|
||||
export function resolveProcessorFields(
|
||||
processorType: string,
|
||||
isBodyJsonEnabled: boolean,
|
||||
): Array<ProcessorFormField> {
|
||||
const fields = processorFields[processorType] ?? [];
|
||||
|
||||
if (!isBodyJsonEnabled) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
return fields.map((field) =>
|
||||
field.name === 'parse_from' && field.initialValue === BODY_PARSE_FROM
|
||||
? { ...field, initialValue: JSON_BODY_PARSE_FROM }
|
||||
: field,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { processorFields } from '../PipelineListsView/AddNewProcessor/config';
|
||||
import { resolveProcessorFields } from '../PipelineListsView/AddNewProcessor/utils';
|
||||
|
||||
const parseFromDefault = (
|
||||
fields: ReturnType<typeof resolveProcessorFields>,
|
||||
): unknown => fields.find((field) => field.name === 'parse_from')?.initialValue;
|
||||
|
||||
describe('resolveProcessorFields', () => {
|
||||
it.each(['grok_parser', 'regex_parser', 'json_parser'])(
|
||||
'defaults %s parse_from to body.message when use_json_body is on',
|
||||
(processorType) => {
|
||||
expect(parseFromDefault(resolveProcessorFields(processorType, true))).toBe(
|
||||
'body.message',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['grok_parser', 'regex_parser', 'json_parser'])(
|
||||
'keeps %s parse_from as body when use_json_body is off',
|
||||
(processorType) => {
|
||||
expect(parseFromDefault(resolveProcessorFields(processorType, false))).toBe(
|
||||
'body',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('leaves parse_from defaults that do not point at the body alone', () => {
|
||||
expect(parseFromDefault(resolveProcessorFields('time_parser', true))).toBe(
|
||||
'attributes.timestamp',
|
||||
);
|
||||
expect(
|
||||
parseFromDefault(resolveProcessorFields('severity_parser', true)),
|
||||
).toBe('attributes.logLevel');
|
||||
});
|
||||
|
||||
it('does not mutate the shared config', () => {
|
||||
resolveProcessorFields('grok_parser', true);
|
||||
|
||||
expect(parseFromDefault(processorFields.grok_parser)).toBe('body');
|
||||
});
|
||||
|
||||
it('returns an empty list for an unknown processor type', () => {
|
||||
expect(resolveProcessorFields('does_not_exist', true)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -29,6 +29,7 @@ type builderQuery[T any] struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
orgID valuer.UUID
|
||||
stmtBuilder qbtypes.StatementBuilder[T]
|
||||
queryType qbtypes.QueryType
|
||||
spec qbtypes.QueryBuilderQuery[T]
|
||||
variables map[string]qbtypes.VariableItem
|
||||
|
||||
@@ -51,6 +52,7 @@ func newBuilderQuery[T any](
|
||||
telemetryStore telemetrystore.TelemetryStore,
|
||||
orgID valuer.UUID,
|
||||
stmtBuilder qbtypes.StatementBuilder[T],
|
||||
queryType qbtypes.QueryType,
|
||||
spec qbtypes.QueryBuilderQuery[T],
|
||||
tr qbtypes.TimeRange,
|
||||
kind qbtypes.RequestType,
|
||||
@@ -62,6 +64,7 @@ func newBuilderQuery[T any](
|
||||
telemetryStore: telemetryStore,
|
||||
orgID: orgID,
|
||||
stmtBuilder: stmtBuilder,
|
||||
queryType: queryType,
|
||||
spec: spec,
|
||||
variables: variables,
|
||||
fromMS: tr.From,
|
||||
@@ -81,7 +84,7 @@ func (q *builderQuery[T]) Fingerprint() string {
|
||||
|
||||
// Create a deterministic fingerprint for builder queries
|
||||
// This needs to include all fields that affect the query results
|
||||
parts := []string{"builder"}
|
||||
parts := []string{q.queryType.StringValue()}
|
||||
|
||||
// Add signal type
|
||||
parts = append(parts, fmt.Sprintf("signal=%s", q.spec.Signal.StringValue()))
|
||||
|
||||
@@ -3,6 +3,7 @@ package querier
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
@@ -20,7 +21,8 @@ func TestBuilderQueryFingerprint(t *testing.T) {
|
||||
{
|
||||
name: "fingerprint includes shiftby when ShiftBy field is set",
|
||||
query: &builderQuery[qbtypes.MetricAggregation]{
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
queryType: qbtypes.QueryTypeBuilder,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
ShiftBy: 3600,
|
||||
@@ -40,7 +42,8 @@ func TestBuilderQueryFingerprint(t *testing.T) {
|
||||
{
|
||||
name: "fingerprint includes shiftby but not other functions",
|
||||
query: &builderQuery[qbtypes.MetricAggregation]{
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
queryType: qbtypes.QueryTypeBuilder,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
ShiftBy: 3600,
|
||||
@@ -63,7 +66,8 @@ func TestBuilderQueryFingerprint(t *testing.T) {
|
||||
{
|
||||
name: "no shiftby in fingerprint when ShiftBy is zero",
|
||||
query: &builderQuery[qbtypes.MetricAggregation]{
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
queryType: qbtypes.QueryTypeBuilder,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
spec: qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]{
|
||||
Signal: telemetrytypes.SignalMetrics,
|
||||
ShiftBy: 0,
|
||||
@@ -94,6 +98,29 @@ func TestBuilderQueryFingerprint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderQueryFingerprintQueryType(t *testing.T) {
|
||||
spec := qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
|
||||
Signal: telemetrytypes.SignalTraces,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Aggregations: []qbtypes.TraceAggregation{{Expression: "count()"}},
|
||||
Filter: &qbtypes.Filter{Expression: "gen_ai.request.model EXISTS"},
|
||||
}
|
||||
regular := &builderQuery[qbtypes.TraceAggregation]{
|
||||
queryType: qbtypes.QueryTypeBuilder,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
spec: spec,
|
||||
}
|
||||
ai := &builderQuery[qbtypes.TraceAggregation]{
|
||||
queryType: qbtypes.QueryTypeBuilderAI,
|
||||
kind: qbtypes.RequestTypeTimeSeries,
|
||||
spec: spec,
|
||||
}
|
||||
|
||||
assert.True(t, strings.HasPrefix(regular.Fingerprint(), qbtypes.QueryTypeBuilder.StringValue()+"&"))
|
||||
assert.True(t, strings.HasPrefix(ai.Fingerprint(), qbtypes.QueryTypeBuilderAI.StringValue()+"&"))
|
||||
assert.NotEqual(t, regular.Fingerprint(), ai.Fingerprint())
|
||||
}
|
||||
|
||||
func TestMakeBucketsOrder(t *testing.T) {
|
||||
// Test that makeBuckets returns buckets in reverse chronological order by default
|
||||
// Using milliseconds as input - need > 1 hour range to get multiple buckets
|
||||
|
||||
@@ -305,7 +305,7 @@ func (q *querier) buildQueries(
|
||||
}
|
||||
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
|
||||
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, q.aiTraceStmtBuilder, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, q.aiTraceStmtBuilder, query.Type, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
queries[spec.Name] = bq
|
||||
steps[spec.Name] = spec.StepInterval
|
||||
case qbtypes.QueryTypeBuilder:
|
||||
@@ -313,7 +313,7 @@ func (q *querier) buildQueries(
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
|
||||
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, q.traceStmtBuilder, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, q.traceStmtBuilder, query.Type, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
queries[spec.Name] = bq
|
||||
steps[spec.Name] = spec.StepInterval
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
||||
@@ -323,7 +323,7 @@ func (q *querier) buildQueries(
|
||||
if spec.Source == telemetrytypes.SourceAudit {
|
||||
stmtBuilder = q.auditStmtBuilder
|
||||
}
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, stmtBuilder, spec, timeRange, req.RequestType, tmplVars, q.builderConfig)
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, stmtBuilder, query.Type, spec, timeRange, req.RequestType, tmplVars, q.builderConfig)
|
||||
queries[spec.Name] = bq
|
||||
steps[spec.Name] = spec.StepInterval
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||
@@ -340,9 +340,9 @@ func (q *querier) buildQueries(
|
||||
|
||||
if spec.Source == telemetrytypes.SourceMeter {
|
||||
event.Source = telemetrytypes.SourceMeter.StringValue()
|
||||
bq = newBuilderQuery(q.logger, q.telemetryStore, orgID, q.meterStmtBuilder, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
bq = newBuilderQuery(q.logger, q.telemetryStore, orgID, q.meterStmtBuilder, query.Type, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
} else {
|
||||
bq = newBuilderQuery(q.logger, q.telemetryStore, orgID, q.metricStmtBuilder, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
bq = newBuilderQuery(q.logger, q.telemetryStore, orgID, q.metricStmtBuilder, query.Type, spec, timeRange, req.RequestType, tmplVars, builderConfig{})
|
||||
}
|
||||
|
||||
queries[spec.Name] = bq
|
||||
@@ -618,7 +618,7 @@ func (q *querier) QueryRawStream(ctx context.Context, orgID valuer.UUID, req *qb
|
||||
if spec.Source == telemetrytypes.SourceAudit {
|
||||
liveTailStmtBuilder = q.auditStmtBuilder
|
||||
}
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, liveTailStmtBuilder, spec, timeRange, req.RequestType, map[string]qbtypes.VariableItem{
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, orgID, liveTailStmtBuilder, query.Type, spec, timeRange, req.RequestType, map[string]qbtypes.VariableItem{
|
||||
"id": {
|
||||
Value: updatedLogID,
|
||||
},
|
||||
@@ -941,8 +941,9 @@ func (q *querier) createRangedQuery(_ valuer.UUID, originalQuery qbtypes.Query,
|
||||
specCopy := qt.spec.Copy()
|
||||
specCopy.ShiftBy = extractShiftFromBuilderQuery(specCopy)
|
||||
adjustedTimeRange := adjustTimeRangeForShift(specCopy, timeRange, qt.kind)
|
||||
// reuse the original query's statement builder so an AI query keeps its AI builder
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, qt.stmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables, builderConfig{})
|
||||
// reuse the original query's statement builder and type so an AI query
|
||||
// keeps its AI builder and cache key
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, qt.stmtBuilder, qt.queryType, specCopy, adjustedTimeRange, qt.kind, qt.variables, qt.builderConfig)
|
||||
|
||||
case *builderQuery[qbtypes.LogAggregation]:
|
||||
specCopy := qt.spec.Copy()
|
||||
@@ -952,16 +953,16 @@ func (q *querier) createRangedQuery(_ valuer.UUID, originalQuery qbtypes.Query,
|
||||
if qt.spec.Source == telemetrytypes.SourceAudit {
|
||||
shiftStmtBuilder = q.auditStmtBuilder
|
||||
}
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, shiftStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables, q.builderConfig)
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, shiftStmtBuilder, qt.queryType, specCopy, adjustedTimeRange, qt.kind, qt.variables, q.builderConfig)
|
||||
|
||||
case *builderQuery[qbtypes.MetricAggregation]:
|
||||
specCopy := qt.spec.Copy()
|
||||
specCopy.ShiftBy = extractShiftFromBuilderQuery(specCopy)
|
||||
adjustedTimeRange := adjustTimeRangeForShift(specCopy, timeRange, qt.kind)
|
||||
if qt.spec.Source == telemetrytypes.SourceMeter {
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, q.meterStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables, builderConfig{})
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, q.meterStmtBuilder, qt.queryType, specCopy, adjustedTimeRange, qt.kind, qt.variables, builderConfig{})
|
||||
}
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, q.metricStmtBuilder, specCopy, adjustedTimeRange, qt.kind, qt.variables, builderConfig{})
|
||||
return newBuilderQuery(q.logger, q.telemetryStore, qt.orgID, q.metricStmtBuilder, qt.queryType, specCopy, adjustedTimeRange, qt.kind, qt.variables, builderConfig{})
|
||||
case *traceOperatorQuery:
|
||||
specCopy := qt.spec.Copy()
|
||||
return &traceOperatorQuery{
|
||||
|
||||
@@ -19,7 +19,6 @@ pytest_plugins = [
|
||||
"fixtures.traces",
|
||||
"fixtures.metrics",
|
||||
"fixtures.queriercommon",
|
||||
"fixtures.semconvfamilies",
|
||||
"fixtures.metadata",
|
||||
"fixtures.meter",
|
||||
"fixtures.browser",
|
||||
|
||||
75
tests/fixtures/semconvfamilies.py
vendored
75
tests/fixtures/semconvfamilies.py
vendored
@@ -1,75 +0,0 @@
|
||||
"""Seed data for the semconv family matrix tests.
|
||||
|
||||
Four identities cover every fleet state of the deployment.environment(.name)
|
||||
family. The tests assert which identities a filter returns, so BOTH (a row
|
||||
that carries the two spellings with different values) and NEITHER (a keyless
|
||||
row) are the point of most cases.
|
||||
|
||||
Each row carries its family pairs in the resource attributes and in the span
|
||||
attributes, so one fleet serves the resource-context and attribute-context
|
||||
matrices. The same rows exist as logs for the logs literalness guard.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures.logs import Logs
|
||||
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
|
||||
|
||||
PREFIX = "semconv-fam"
|
||||
CURRENT_KEY = "deployment.environment.name"
|
||||
OLD_KEY = "deployment.environment"
|
||||
|
||||
# Row identities. The span name, the log body, and service.name are the identity.
|
||||
OLD = f"{PREFIX}-old" # only the old spelling, value "production"
|
||||
NEW = f"{PREFIX}-new" # only the current spelling, value "production"
|
||||
BOTH = f"{PREFIX}-both" # current "staging" and old "production" - the conflict row
|
||||
NEITHER = f"{PREFIX}-neither" # no member at all
|
||||
|
||||
_ROWS = [
|
||||
(OLD, {OLD_KEY: "production"}, timedelta(seconds=4)),
|
||||
(NEW, {CURRENT_KEY: "production"}, timedelta(seconds=3)),
|
||||
(BOTH, {CURRENT_KEY: "staging", OLD_KEY: "production"}, timedelta(seconds=2)),
|
||||
(NEITHER, {}, timedelta(seconds=1)),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(name="family_fleet", scope="function")
|
||||
def family_fleet(
|
||||
insert_logs: Callable[[list[Logs]], None],
|
||||
insert_traces: Callable[[list[Traces]], None],
|
||||
) -> Generator[datetime]:
|
||||
"""Inserts one span and one log per identity and yields the base
|
||||
timestamp."""
|
||||
now = datetime.now(tz=UTC).replace(microsecond=0) - timedelta(minutes=1)
|
||||
|
||||
insert_traces(
|
||||
[
|
||||
Traces(
|
||||
timestamp=now - offset,
|
||||
duration=timedelta(milliseconds=10),
|
||||
trace_id=TraceIdGenerator.trace_id(),
|
||||
span_id=TraceIdGenerator.span_id(),
|
||||
name=identity,
|
||||
kind=TracesKind.SPAN_KIND_SERVER,
|
||||
status_code=TracesStatusCode.STATUS_CODE_OK,
|
||||
resources={"service.name": identity, **family},
|
||||
attributes=dict(family),
|
||||
)
|
||||
for identity, family, offset in _ROWS
|
||||
]
|
||||
)
|
||||
insert_logs(
|
||||
[
|
||||
Logs(
|
||||
timestamp=now - offset,
|
||||
body=identity,
|
||||
resources={"service.name": identity, **family},
|
||||
attributes=dict(family),
|
||||
)
|
||||
for identity, family, offset in _ROWS
|
||||
]
|
||||
)
|
||||
yield now
|
||||
@@ -1,220 +0,0 @@
|
||||
"""The phase-1 matrix for semantic-convention family resolution.
|
||||
|
||||
The package runs SigNoz with resolve_semconv_families on. The fleet in
|
||||
fixtures/semconvfamilies.py has one identity per state: OLD (old spelling
|
||||
only), NEW (current only), BOTH (current "staging" and old "production"),
|
||||
NEITHER (keyless). Each case asserts which identities a filter returns, with
|
||||
either spelling as the requested name and for both contexts.
|
||||
|
||||
The pinned facts:
|
||||
- Both spellings resolve to the same merged field; the result sets do not
|
||||
depend on the requested spelling.
|
||||
- The current spelling wins on a row that carries both (BOTH reads
|
||||
"staging", never "production").
|
||||
- Negative operators keep keyless rows (NEITHER), exactly like a single
|
||||
key; presence stays an explicit EXISTS opt-in.
|
||||
- Logs stay literal: only traces have family support today.
|
||||
- With the flag off, everything stays literal.
|
||||
"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import (
|
||||
RequestType,
|
||||
build_aggregation,
|
||||
build_group_by_field,
|
||||
build_order_by,
|
||||
build_raw_query,
|
||||
build_traces_scalar_query,
|
||||
get_column_data_from_response,
|
||||
make_query_request,
|
||||
)
|
||||
from fixtures.semconvfamilies import (
|
||||
BOTH,
|
||||
CURRENT_KEY,
|
||||
NEITHER,
|
||||
NEW,
|
||||
OLD,
|
||||
OLD_KEY,
|
||||
PREFIX,
|
||||
)
|
||||
|
||||
FILTER_MATRIX = [
|
||||
pytest.param("{key} = 'production'", {OLD, NEW}, id="eq_matches_either_spelling"),
|
||||
pytest.param("{key} = 'staging'", {BOTH}, id="eq_current_wins_on_conflict"),
|
||||
pytest.param("{key} != 'production'", {BOTH, NEITHER}, id="neq_keeps_keyless_and_conflict"),
|
||||
pytest.param("{key} IN ['production', 'staging']", {OLD, NEW, BOTH}, id="in_matches_merged_value"),
|
||||
pytest.param("{key} NOT IN ['production']", {BOTH, NEITHER}, id="not_in_keeps_keyless"),
|
||||
pytest.param("{key} LIKE '%prod%'", {OLD, NEW}, id="like_matches_merged_value"),
|
||||
pytest.param("{key} EXISTS", {OLD, NEW, BOTH}, id="exists_is_any_member"),
|
||||
pytest.param("{key} NOT EXISTS", {NEITHER}, id="not_exists_is_no_member"),
|
||||
pytest.param("{key} != 'production' AND {key} EXISTS", {BOTH}, id="neq_composed_with_exists"),
|
||||
]
|
||||
|
||||
LITERAL_MATRIX = [
|
||||
pytest.param("{key} = 'production'", {NEW}, id="literal_eq_reads_one_spelling"),
|
||||
pytest.param("{key} != 'production'", {OLD, BOTH, NEITHER}, id="literal_neq_reads_one_spelling"),
|
||||
]
|
||||
|
||||
|
||||
def _trace_identities(
|
||||
signoz: types.SigNoz,
|
||||
token: str,
|
||||
base: datetime,
|
||||
expression: str,
|
||||
signal: str = "traces",
|
||||
) -> set[str]:
|
||||
identity_field = "span.name" if signal == "traces" else "body"
|
||||
identity_column = "name" if signal == "traces" else "body"
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((base - timedelta(minutes=2)).timestamp() * 1000),
|
||||
end_ms=int((base + timedelta(minutes=1)).timestamp() * 1000),
|
||||
request_type=RequestType.RAW,
|
||||
queries=[
|
||||
build_raw_query(
|
||||
"A",
|
||||
signal,
|
||||
limit=100,
|
||||
filter_expression=expression,
|
||||
order=[build_order_by("timestamp", "asc")],
|
||||
select_fields=[{"name": identity_field}],
|
||||
)
|
||||
],
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
# Sets keep the assertion stable when the shared stack is reused and older
|
||||
# rows with the same identities remain.
|
||||
return {name for name in get_column_data_from_response(response.json(), identity_column) if name.startswith(PREFIX)}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expression_template,expected", FILTER_MATRIX)
|
||||
@pytest.mark.parametrize("requested_key", [CURRENT_KEY, OLD_KEY], ids=["current", "old"])
|
||||
@pytest.mark.parametrize("context", ["resource", "attribute"])
|
||||
def test_family_filters(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
family_fleet: datetime,
|
||||
context: str,
|
||||
requested_key: str,
|
||||
expression_template: str,
|
||||
expected: set[str],
|
||||
) -> None:
|
||||
"""One matrix cell: a filter on one spelling, in one context. The result
|
||||
set is a property of the family, not of the requested spelling."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
expression = expression_template.format(key=f"{context}.{requested_key}")
|
||||
assert _trace_identities(signoz, token, family_fleet, expression) == expected, expression
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expression_template,expected", LITERAL_MATRIX)
|
||||
def test_flag_off_stays_literal(
|
||||
signoz_families_off: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
family_fleet: datetime,
|
||||
expression_template: str,
|
||||
expected: set[str],
|
||||
) -> None:
|
||||
"""The same fleet through an instance with the flag at its default: the
|
||||
current spelling reads only rows that carry the current spelling."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
expression = expression_template.format(key=f"resource.{CURRENT_KEY}")
|
||||
assert _trace_identities(signoz_families_off, token, family_fleet, expression) == expected, expression
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expression_template,expected", LITERAL_MATRIX)
|
||||
def test_logs_stay_literal_with_flag_on(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
family_fleet: datetime,
|
||||
expression_template: str,
|
||||
expected: set[str],
|
||||
) -> None:
|
||||
"""Only traces have family support. The same filters on the logs copy of
|
||||
the fleet behave literally even with the flag on."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
expression = expression_template.format(key=f"resource.{CURRENT_KEY}")
|
||||
assert _trace_identities(signoz, token, family_fleet, expression, signal="logs") == expected, expression
|
||||
|
||||
|
||||
def test_group_by_merges_and_echoes_requested_spelling(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
family_fleet: datetime,
|
||||
) -> None:
|
||||
"""Group by the current spelling over the fleet: OLD and NEW land in one
|
||||
"production" group, BOTH lands in "staging", and the group column carries
|
||||
the requested spelling."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((family_fleet - timedelta(minutes=2)).timestamp() * 1000),
|
||||
end_ms=int((family_fleet + timedelta(minutes=1)).timestamp() * 1000),
|
||||
request_type=RequestType.SCALAR,
|
||||
queries=[
|
||||
build_traces_scalar_query(
|
||||
[build_aggregation("count()")],
|
||||
filter_expression=f"service.name LIKE '{PREFIX}%'",
|
||||
group_by=[build_group_by_field(CURRENT_KEY, "string", "resource")],
|
||||
)
|
||||
],
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
result = response.json()["data"]["data"]["results"][0]
|
||||
group_column = result["columns"][0]
|
||||
assert group_column["name"] == CURRENT_KEY, group_column
|
||||
assert group_column["columnType"] == "group", group_column
|
||||
|
||||
groups = {row[0] for row in result["data"]}
|
||||
assert {"production", "staging"}.issubset(groups), groups
|
||||
assert None in groups, groups
|
||||
|
||||
|
||||
def test_bare_name_prefers_resource_and_warns(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
family_fleet: datetime,
|
||||
) -> None:
|
||||
"""The fleet stores the family under the resource and the attribute
|
||||
contexts, so a bare name is ambiguous. Resolution warns and keeps the
|
||||
resource side; the family survives the collision as one unit."""
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((family_fleet - timedelta(minutes=2)).timestamp() * 1000),
|
||||
end_ms=int((family_fleet + timedelta(minutes=1)).timestamp() * 1000),
|
||||
request_type=RequestType.RAW,
|
||||
queries=[
|
||||
build_raw_query(
|
||||
"A",
|
||||
"traces",
|
||||
limit=100,
|
||||
filter_expression=f"{CURRENT_KEY} = 'production'",
|
||||
order=[build_order_by("timestamp", "asc")],
|
||||
select_fields=[{"name": "span.name"}],
|
||||
)
|
||||
],
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
matched = {name for name in get_column_data_from_response(response.json(), "name") if name.startswith(PREFIX)}
|
||||
assert matched == {OLD, NEW}
|
||||
|
||||
warning = response.json()["data"].get("warning") or {}
|
||||
messages = " ".join(entry.get("message", "") for entry in warning.get("warnings", []))
|
||||
assert "ambiguous" in messages.lower(), messages
|
||||
@@ -1,56 +0,0 @@
|
||||
import pytest
|
||||
from testcontainers.core.container import Network
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.signoz import create_signoz
|
||||
|
||||
|
||||
@pytest.fixture(name="signoz", scope="package")
|
||||
def signoz_semconv_families(
|
||||
network: Network,
|
||||
zeus: types.TestContainerDocker,
|
||||
gateway: types.TestContainerDocker,
|
||||
sqlstore: types.TestContainerSQL,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.SigNoz:
|
||||
"""Package-scoped SigNoz with resolve_semconv_families on."""
|
||||
return create_signoz(
|
||||
network=network,
|
||||
zeus=zeus,
|
||||
gateway=gateway,
|
||||
sqlstore=sqlstore,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="signoz-semconv-families",
|
||||
env_overrides={
|
||||
"SIGNOZ_FLAGGER_CONFIG_BOOLEAN_RESOLVE__SEMCONV__FAMILIES": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="signoz_families_off", scope="package")
|
||||
def signoz_families_off(
|
||||
network: Network,
|
||||
zeus: types.TestContainerDocker,
|
||||
gateway: types.TestContainerDocker,
|
||||
sqlstore: types.TestContainerSQL,
|
||||
clickhouse: types.TestContainerClickhouse,
|
||||
request: pytest.FixtureRequest,
|
||||
pytestconfig: pytest.Config,
|
||||
) -> types.SigNoz:
|
||||
"""A second instance with the flag at its default (off). It shares the
|
||||
sqlstore and clickhouse, so the same admin token and seeded rows work."""
|
||||
return create_signoz(
|
||||
network=network,
|
||||
zeus=zeus,
|
||||
gateway=gateway,
|
||||
sqlstore=sqlstore,
|
||||
clickhouse=clickhouse,
|
||||
request=request,
|
||||
pytestconfig=pytestconfig,
|
||||
cache_key="signoz-semconv-families-off",
|
||||
env_overrides={},
|
||||
)
|
||||
Reference in New Issue
Block a user