mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-07 10:40:42 +01:00
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
cacheci / tests (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
#### Description Phase 2 of #6143: semantic-convention families resolve on logs and metrics, behind the `resolve_semconv_families` flag (default off), on the storage contract of #12802. - Registry: each member carries the scope of its own rename edges, so a fan-out keeps one membership per target and an ambiguous name stays literal. - Logs and metrics families need no family code of their own. The gate applies to every signal, and `LogicalRead` merges members through each storage's `Read`. - Metric-name families union the storage names in every `metric_name` filter, and the querier reads type, temporality, and the reduced flag across the family. - Span-metrics labels: the metrics the processor emits, listed by name, also read each family member with the `resource_` prefix. Requested names are never rewritten. - Values suggestions and related values cover every spelling of the family. - `deployment.environment.name` resolves on all three signals. `db.system.name` stays off until a value-mapping reader exists. #### Additional Information - `pkg/semconv.Family` fields are now unexported, and `transition.go` is removed. #12446 reads the old API and needs an update when stacked. - A target that emits both names of a metric-name family double-counts in `sum()` during the overlap window. Reading both names is the feature. Pinned by a test. - A family of metrics labels keeps the keyless contract of a single label: no guard, no NULL group.
737 lines
23 KiB
Go
737 lines
23 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestSchemaDecoderRejectsUnsupportedSection(t *testing.T) {
|
|
var schema schemaFile
|
|
err := decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
span_events:
|
|
changes:
|
|
- rename_events:
|
|
event_map:
|
|
old: current
|
|
`), &schema)
|
|
|
|
assert.ErrorContains(t, err, "field span_events not found", "unsupported schema sections must fail generation")
|
|
}
|
|
|
|
func TestBuildFamiliesRejectsMalformedSchemaVersion(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
latest:
|
|
spans:
|
|
changes: []
|
|
`), &schema), "test schema must decode")
|
|
|
|
_, err := buildFamilies([]schemaFile{schema}, overlayFile{})
|
|
assert.ErrorContains(t, err, `schema version "latest"`, "malformed versions must not be silently reordered")
|
|
}
|
|
|
|
func TestParseSchemaVersionRejectsNonNumericComponent(t *testing.T) {
|
|
_, err := parseSchemaVersion("1.2.x")
|
|
assert.ErrorContains(t, err, `invalid numeric component "x"`, "non-numeric version components must fail generation")
|
|
}
|
|
|
|
func TestBuildFamiliesResolvesRenameChain(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
4.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
a: b
|
|
3.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
b: c
|
|
x: c
|
|
2.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
a: b
|
|
- rename_attributes:
|
|
attribute_map:
|
|
a: b
|
|
`), &schema), "test schema must decode")
|
|
|
|
enabled := true
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
|
|
"c": {Enabled: &enabled},
|
|
}})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "c",
|
|
Kind: kindAttribute,
|
|
Members: []generatedMember{
|
|
{Name: "b", Contexts: []string{"attribute"}, Signals: []string{"traces"}},
|
|
{Name: "x", Contexts: []string{"attribute"}, Signals: []string{"traces"}},
|
|
{Name: "a", Contexts: []string{"attribute"}, Signals: []string{"traces"}},
|
|
},
|
|
}}, families, "predecessors should be ordered by distance and then name")
|
|
}
|
|
|
|
func TestBuildFamiliesMapsSchemaSectionsToScopes(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
all:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
all.old: all.current
|
|
resources:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
resource.old: resource.current
|
|
logs:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
log.old: log.current
|
|
metrics:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
state: cpu.mode
|
|
apply_to_metrics: [system.cpu.time]
|
|
- rename_metrics:
|
|
old.metric: current.metric
|
|
`), &schema), "test schema must decode")
|
|
|
|
enabled := true
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
|
|
"all.current": {Enabled: &enabled},
|
|
"resource.current": {Enabled: &enabled},
|
|
"log.current": {Enabled: &enabled},
|
|
"cpu.mode": {Enabled: &enabled},
|
|
"current.metric": {Enabled: &enabled, Kind: kindMetric},
|
|
}})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{
|
|
{
|
|
Current: "all.current", Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "all.old"}},
|
|
},
|
|
{
|
|
Current: "cpu.mode", Kind: kindAttribute,
|
|
Members: []generatedMember{{
|
|
Name: "state", Contexts: []string{"attribute"}, Signals: []string{"metrics"},
|
|
ApplyToMetrics: []string{"system.cpu.time"},
|
|
}},
|
|
},
|
|
{
|
|
Current: "current.metric", Kind: kindMetric,
|
|
Members: []generatedMember{{Name: "old.metric", Contexts: []string{"metric"}, Signals: []string{"metrics"}}},
|
|
},
|
|
{
|
|
Current: "log.current", Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "log.old", Contexts: []string{"attribute"}, Signals: []string{"logs"}}},
|
|
},
|
|
{
|
|
Current: "resource.current", Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "resource.old", Contexts: []string{"resource"}}},
|
|
},
|
|
}, families, "schema sections should produce their documented per-member signal and context scopes")
|
|
}
|
|
|
|
func TestBuildFamiliesKeepsApplyToMetricsThroughNonMetricsRepeat(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
2.0.0:
|
|
logs:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
old: current
|
|
1.0.0:
|
|
metrics:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
old: current
|
|
apply_to_metrics: [system.cpu.time]
|
|
`), &schema), "test schema must decode")
|
|
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{DefaultEnabled: true})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "current", Kind: kindAttribute,
|
|
Members: []generatedMember{{
|
|
Name: "old", Contexts: []string{"attribute"}, Signals: []string{"logs", "metrics"},
|
|
ApplyToMetrics: []string{"system.cpu.time"},
|
|
}},
|
|
}}, families, "a repeat under a non-metrics section widens the signals and must not erase the metric scope")
|
|
}
|
|
|
|
func TestBuildFamiliesRejectsExplicitlyEmptyOverlayApplyToMetrics(t *testing.T) {
|
|
enabled := true
|
|
_, err := buildFamilies(nil, overlayFile{Families: map[string]overlayFamily{
|
|
"current": {Enabled: &enabled, Old: []string{"old"}, ApplyToMetrics: []string{}},
|
|
}})
|
|
assert.ErrorContains(t, err, "explicitly empty apply_to_metrics", "the overlay must not silently widen an empty list to every metric")
|
|
}
|
|
|
|
func TestBuildFamiliesRejectsExplicitlyEmptyApplyToMetrics(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
metrics:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
old: current
|
|
apply_to_metrics: []
|
|
`), &schema), "test schema must decode")
|
|
|
|
_, err := buildFamilies([]schemaFile{schema}, overlayFile{})
|
|
assert.ErrorContains(t, err, "explicitly empty apply_to_metrics", "an empty list must not silently widen to every metric")
|
|
}
|
|
|
|
func TestBuildFamiliesKeepsFanOutSeparate(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
2.0.0:
|
|
metrics:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
state: cpu.mode
|
|
apply_to_metrics: [system.cpu.time]
|
|
1.0.0:
|
|
metrics:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
state: db.client.connection.state
|
|
apply_to_metrics: [db.client.connections.usage]
|
|
`), &schema), "test schema must decode")
|
|
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{DefaultEnabled: true})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{
|
|
{
|
|
Current: "cpu.mode", Kind: kindAttribute,
|
|
Members: []generatedMember{{
|
|
Name: "state", Contexts: []string{"attribute"}, Signals: []string{"metrics"},
|
|
ApplyToMetrics: []string{"system.cpu.time"},
|
|
}},
|
|
},
|
|
{
|
|
Current: "db.client.connection.state", Kind: kindAttribute,
|
|
Members: []generatedMember{{
|
|
Name: "state", Contexts: []string{"attribute"}, Signals: []string{"metrics"},
|
|
ApplyToMetrics: []string{"db.client.connections.usage"},
|
|
}},
|
|
},
|
|
}, families, "an old name with differently scoped rename targets must keep one membership per target")
|
|
}
|
|
|
|
func TestBuildFamiliesKeepsUnscopedRenameUnscoped(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
2.0.0:
|
|
metrics:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
direction: network.io.direction
|
|
apply_to_metrics: [system.disk.io, system.disk.merged]
|
|
1.0.0:
|
|
metrics:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
system.network.io.direction: network.io.direction
|
|
`), &schema), "test schema must decode")
|
|
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{DefaultEnabled: true})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "network.io.direction", Kind: kindAttribute,
|
|
Members: []generatedMember{
|
|
{Name: "direction", Contexts: []string{"attribute"}, Signals: []string{"metrics"}, ApplyToMetrics: []string{"system.disk.io", "system.disk.merged"}},
|
|
{Name: "system.network.io.direction", Contexts: []string{"attribute"}, Signals: []string{"metrics"}},
|
|
},
|
|
}}, families, "a rename without apply_to_metrics stays unbounded; a scoped sibling must not bound it")
|
|
}
|
|
|
|
func TestBuildFamiliesKeepsMemberScopesThroughChains(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
2.0.0:
|
|
metrics:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
messaging.client_id: messaging.client.id
|
|
1.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
messaging.kafka.client_id: messaging.client_id
|
|
`), &schema), "test schema must decode")
|
|
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{DefaultEnabled: true})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "messaging.client.id", Kind: kindAttribute,
|
|
Members: []generatedMember{
|
|
{Name: "messaging.client_id", Contexts: []string{"attribute"}, Signals: []string{"metrics"}},
|
|
{Name: "messaging.kafka.client_id", Contexts: []string{"attribute"}, Signals: []string{"traces"}},
|
|
},
|
|
}}, families, "a member keeps the scope of its own rename edge; later hops only carry it to the root")
|
|
}
|
|
|
|
func TestBuildFamiliesRecordsCrossContextMembersSeparately(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
http.user_agent: user_agent.original
|
|
resources:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
browser.user_agent: user_agent.original
|
|
`), &schema), "test schema must decode")
|
|
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{DefaultEnabled: true})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "user_agent.original", Kind: kindAttribute,
|
|
Members: []generatedMember{
|
|
{Name: "browser.user_agent", Contexts: []string{"resource"}},
|
|
{Name: "http.user_agent", Contexts: []string{"attribute"}, Signals: []string{"traces"}},
|
|
},
|
|
}}, families, "members renamed from different contexts must keep their own context scopes")
|
|
}
|
|
|
|
func TestBuildFamiliesMergesRepeatedEdgeScopes(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
2.0.0:
|
|
logs:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
old: current
|
|
1.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
old: current
|
|
`), &schema), "test schema must decode")
|
|
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{DefaultEnabled: true})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "current", Kind: kindAttribute,
|
|
Members: []generatedMember{
|
|
{Name: "old", Contexts: []string{"attribute"}, Signals: []string{"logs", "traces"}},
|
|
},
|
|
}}, families, "the same rename filed under several sections widens the member scope")
|
|
}
|
|
|
|
func TestOverlayAddsFamilyWithoutSchemaHistory(t *testing.T) {
|
|
enabled := true
|
|
families, err := buildFamilies(nil, overlayFile{Families: map[string]overlayFamily{
|
|
"added.current": {
|
|
Enabled: &enabled,
|
|
Old: []string{"added.old"},
|
|
Contexts: []string{"resource"},
|
|
Signals: []string{"traces"},
|
|
},
|
|
}})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "added.current",
|
|
Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "added.old"}},
|
|
Contexts: []string{"resource"},
|
|
Signals: []string{"traces"},
|
|
}}, families, "an explicit overlay family should not require schema history")
|
|
}
|
|
|
|
func TestOverlayOverridesGeneratedFamily(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
old: current
|
|
`), &schema), "test schema must decode")
|
|
|
|
enabled := true
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
|
|
"current": {
|
|
Enabled: &enabled,
|
|
AddOld: []string{"older"},
|
|
ExcludeOld: []string{"old"},
|
|
AddSignals: []string{"logs"},
|
|
ValueMap: map[string]string{"legacy": "current"},
|
|
},
|
|
}})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "current",
|
|
Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "older"}},
|
|
ValueMap: map[string]string{"legacy": "current"},
|
|
}}, families, "overlay additions and exclusions should be applied to the generated family")
|
|
}
|
|
|
|
func TestOverlayAddSignalsWidensMemberScopes(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
old: current
|
|
`), &schema), "test schema must decode")
|
|
|
|
enabled := true
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
|
|
"current": {Enabled: &enabled, AddSignals: []string{"logs"}},
|
|
}})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "current",
|
|
Kind: kindAttribute,
|
|
Members: []generatedMember{
|
|
{Name: "old", Contexts: []string{"attribute"}, Signals: []string{"logs", "traces"}},
|
|
},
|
|
}}, families, "add_signals widens the schema-derived member scopes and never narrows the family gate")
|
|
}
|
|
|
|
func TestOverlaySignalsSetTheFamilyGate(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
all:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
old: current
|
|
`), &schema), "test schema must decode")
|
|
|
|
enabled := true
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
|
|
"current": {Enabled: &enabled, Signals: []string{"logs", "traces"}},
|
|
}})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "current",
|
|
Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "old"}},
|
|
Signals: []string{"logs", "traces"},
|
|
}}, families, "the overlay signals list gates the family without touching member scopes")
|
|
}
|
|
|
|
func TestOverlayDisablesFamilyWhenDefaultIsEnabled(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
old: current
|
|
`), &schema), "test schema must decode")
|
|
|
|
disabled := false
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{
|
|
DefaultEnabled: true,
|
|
Families: map[string]overlayFamily{
|
|
"current": {Enabled: &disabled},
|
|
},
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Empty(t, families, "an explicitly disabled family must override default_enabled")
|
|
}
|
|
|
|
func TestRenderGoRejectsValueMap(t *testing.T) {
|
|
families := []generatedFamily{{
|
|
Current: "current", Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "old"}},
|
|
ValueMap: map[string]string{"a": "1"},
|
|
}}
|
|
|
|
_, err := renderGo(families)
|
|
assert.ErrorContains(t, err, "no value-map reader yet", "a value map must fail Go generation until a reader exists")
|
|
}
|
|
|
|
func TestRenderGoUsesCanonicalTelemetryTypes(t *testing.T) {
|
|
families := []generatedFamily{{
|
|
Current: "current", Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "old", Contexts: []string{"resource"}, Signals: []string{"traces"}}},
|
|
}}
|
|
|
|
output, err := renderGo(families)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(output), "telemetrytypes.FieldContextResource", "generated contexts should use telemetrytypes")
|
|
assert.Contains(t, string(output), "telemetrytypes.SignalTraces", "generated signals should use telemetrytypes")
|
|
}
|
|
|
|
func TestRenderGoRejectsUnknownContext(t *testing.T) {
|
|
families := []generatedFamily{{
|
|
Current: "current", Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "old", Contexts: []string{"bogus"}}},
|
|
}}
|
|
|
|
_, err := renderGo(families)
|
|
assert.ErrorContains(t, err, `unsupported field context "bogus"`, "a bad overlay context must fail generation, not compile")
|
|
}
|
|
|
|
func TestRenderGoRejectsUnknownSignal(t *testing.T) {
|
|
families := []generatedFamily{{
|
|
Current: "current", Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "old"}},
|
|
Signals: []string{"bogus"},
|
|
}}
|
|
|
|
_, err := renderGo(families)
|
|
assert.ErrorContains(t, err, `unsupported signal "bogus"`, "a bad overlay signal must fail generation, not compile")
|
|
}
|
|
|
|
func TestRenderGoPinsOutput(t *testing.T) {
|
|
families := []generatedFamily{{
|
|
Current: "deployment.environment.name", Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "deployment.environment"}},
|
|
Signals: []string{"logs", "traces"},
|
|
}}
|
|
|
|
output, err := renderGo(families)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, `// Code generated by scripts/semconv. DO NOT EDIT.
|
|
|
|
package semconv
|
|
|
|
import "github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
|
|
|
var families = []Family{
|
|
{
|
|
current: "deployment.environment.name",
|
|
kind: KindAttribute,
|
|
members: []Member{
|
|
{name: "deployment.environment"},
|
|
},
|
|
signals: []telemetrytypes.Signal{telemetrytypes.SignalLogs, telemetrytypes.SignalTraces},
|
|
},
|
|
}
|
|
`, string(output), "the emitted Go text is a contract; regeneration must be reviewable")
|
|
}
|
|
|
|
func TestRenderTypeScriptIsDeterministic(t *testing.T) {
|
|
families := []generatedFamily{{
|
|
Current: "current", Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "old"}},
|
|
ValueMap: map[string]string{"b": "2", "a": "1"},
|
|
}}
|
|
|
|
assert.Equal(t, renderTypeScript(families), renderTypeScript(families), "TypeScript generation must not depend on map iteration order")
|
|
}
|
|
|
|
func TestRenderTypeScriptPinsOutput(t *testing.T) {
|
|
families := []generatedFamily{{
|
|
Current: "deployment.environment.name", Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "deployment.environment"}},
|
|
Signals: []string{"logs", "traces"},
|
|
}}
|
|
|
|
assert.Equal(t, `// Code generated by scripts/semconv. DO NOT EDIT.
|
|
|
|
// An empty contexts/signals/applyToMetrics array places no constraint on
|
|
// that axis.
|
|
export type SemconvMember = {
|
|
readonly name: string;
|
|
readonly contexts: readonly string[];
|
|
readonly signals: readonly string[];
|
|
readonly applyToMetrics: readonly string[];
|
|
};
|
|
|
|
export type SemconvFamily = {
|
|
readonly current: string;
|
|
readonly kind: 'attribute' | 'metric';
|
|
readonly members: readonly SemconvMember[];
|
|
readonly contexts: readonly string[];
|
|
readonly signals: readonly string[];
|
|
readonly valueMap: Readonly<Record<string, string>>;
|
|
};
|
|
|
|
export const SEMCONV_FAMILIES: readonly SemconvFamily[] = [
|
|
{
|
|
current: 'deployment.environment.name',
|
|
kind: 'attribute',
|
|
members: [
|
|
{
|
|
name: 'deployment.environment',
|
|
contexts: [],
|
|
signals: [],
|
|
applyToMetrics: [],
|
|
},
|
|
],
|
|
contexts: [],
|
|
signals: ['logs', 'traces'],
|
|
valueMap: {},
|
|
},
|
|
] as const;
|
|
`, string(renderTypeScript(families)), "the emitted TypeScript text is a contract; regeneration must be reviewable")
|
|
}
|
|
|
|
func TestBuildFamiliesHandlesRenameRollback(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
2.0.0:
|
|
metrics:
|
|
changes:
|
|
- rename_metrics:
|
|
temporary: original
|
|
1.0.0:
|
|
metrics:
|
|
changes:
|
|
- rename_metrics:
|
|
original: temporary
|
|
`), &schema), "test schema must decode")
|
|
|
|
enabled := true
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
|
|
"original": {Enabled: &enabled, Kind: kindMetric},
|
|
}})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "original",
|
|
Kind: kindMetric,
|
|
Members: []generatedMember{{Name: "temporary", Contexts: []string{"metric"}, Signals: []string{"metrics"}}},
|
|
}}, families, "the latest rollback destination should remain the family root")
|
|
}
|
|
|
|
func TestBuildFamiliesRejectsSameVersionRenameChain(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
x: y
|
|
y: z
|
|
`), &schema), "test schema must decode")
|
|
|
|
_, err := buildFamilies([]schemaFile{schema}, overlayFile{})
|
|
assert.ErrorContains(t, err, `same-version attribute rename chain through "y"`, "order-sensitive same-version chains must be rejected")
|
|
}
|
|
|
|
func TestBuildFamiliesRejectsOverlayFamilyWithoutHistory(t *testing.T) {
|
|
enabled := true
|
|
_, err := buildFamilies(nil, overlayFile{Families: map[string]overlayFamily{
|
|
"missing": {Enabled: &enabled},
|
|
}})
|
|
|
|
assert.ErrorContains(t, err, `overlay family "missing" with kind "attribute" is absent`, "an overlay cannot invent a family without old members")
|
|
}
|
|
|
|
func TestBuildFamiliesRejectsEnabledFamilyWithoutOldMembers(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
old: current
|
|
`), &schema), "test schema must decode")
|
|
|
|
enabled := true
|
|
_, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
|
|
"current": {Enabled: &enabled, ExcludeOld: []string{"old"}},
|
|
}})
|
|
assert.ErrorContains(t, err, `enabled family "current" with kind "attribute" has no old members`, "exclude_old cannot empty an enabled family")
|
|
}
|
|
|
|
func TestOverlayKindDefaultsToAttribute(t *testing.T) {
|
|
var schema schemaFile
|
|
require.NoError(t, decodeKnownFields([]byte(`
|
|
versions:
|
|
1.0.0:
|
|
spans:
|
|
changes:
|
|
- rename_attributes:
|
|
attribute_map:
|
|
attribute.old: shared.current
|
|
metrics:
|
|
changes:
|
|
- rename_metrics:
|
|
metric.old: shared.current
|
|
`), &schema), "test schema must decode")
|
|
|
|
enabled := true
|
|
families, err := buildFamilies([]schemaFile{schema}, overlayFile{Families: map[string]overlayFamily{
|
|
"shared.current": {Enabled: &enabled},
|
|
}})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []generatedFamily{{
|
|
Current: "shared.current",
|
|
Kind: kindAttribute,
|
|
Members: []generatedMember{{Name: "attribute.old", Contexts: []string{"attribute"}, Signals: []string{"traces"}}},
|
|
}}, families, "a kind-less overlay policy should affect only the attribute family")
|
|
}
|
|
|
|
func TestCheckFileReportsStaleOutput(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "generated.go")
|
|
require.NoError(t, os.WriteFile(path, []byte("old"), 0o600), "test output must be writable")
|
|
|
|
assert.ErrorContains(t, checkFile(path, []byte("new")), "is stale", "check mode must reject stale generated output")
|
|
}
|
|
|
|
func TestTypeScriptStringEscapesControlCharacters(t *testing.T) {
|
|
assert.Equal(t, `'line\n\t\x01\'\\end'`, tsString("line\n\t\x01'\\end"), "generated TypeScript strings must remain valid literals")
|
|
}
|