Skip to content

Commit 14d05df

Browse files
authored
Low-cardinality route from path mode (#882)
1 parent 9a77854 commit 14d05df

File tree

14 files changed

+651
-18
lines changed

14 files changed

+651
-18
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
routes:
2+
ignored_patterns:
3+
- /metrics
4+
unmatched: low-cardinality
5+
max_path_segment_cardinality: 3
6+
otel_metrics_export:
7+
endpoint: http://otelcol:4318
8+
otel_traces_export:
9+
endpoint: http://jaeger:4318
10+
attributes:
11+
select:
12+
"*":
13+
include: ["*"]

internal/test/integration/red_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,42 @@ func testREDMetricsForHTTPLibraryNoRoute(t *testing.T, url, svcName string) {
881881
require.Empty(t, results)
882882
}
883883

884+
func testREDMetricsForHTTPLibraryNoRouteLowCardinality(t *testing.T, url, svcName string) {
885+
validNames := []string{"user", "customer", "test", "option", "metric"}
886+
887+
// Call 3 times the instrumented service, forcing it to:
888+
// - take at least 30ms to respond
889+
// - returning a 404 code
890+
for i := 0; i < 3; i++ {
891+
for _, s := range validNames {
892+
ti.DoHTTPGet(t, url+"/api/"+s+"?delay=30ms&status=404", 404)
893+
}
894+
}
895+
896+
// Eventually, Prometheus would make this query visible
897+
pq := prom.Client{HostPort: prometheusHostPort}
898+
var results []prom.Result
899+
test.Eventually(t, testTimeout, func(t require.TestingT) {
900+
var err error
901+
results, err = pq.Query(`http_server_request_duration_seconds_count{` +
902+
`http_request_method="GET",` +
903+
`http_response_status_code="404",` +
904+
`service_namespace="integration-test",` +
905+
`service_name="` + svcName + `",` +
906+
`http_route="/api/*"}`)
907+
require.NoError(t, err)
908+
// check duration_count has 3 calls and all the arguments
909+
enoughPromResults(t, results)
910+
val := totalPromCount(t, results)
911+
assert.LessOrEqual(t, 3, val)
912+
if len(results) > 0 {
913+
res := results[0]
914+
addr := res.Metric["client_address"]
915+
assert.NotNil(t, addr)
916+
}
917+
})
918+
}
919+
884920
func testREDMetricsHTTPNoRoute(t *testing.T) {
885921
for _, testCaseURL := range []string{
886922
instrumentedServiceGorillaURL,
@@ -892,6 +928,17 @@ func testREDMetricsHTTPNoRoute(t *testing.T) {
892928
}
893929
}
894930

931+
func testREDMetricsHTTPNoRouteLowCardinality(t *testing.T) {
932+
for _, testCaseURL := range []string{
933+
instrumentedServiceStdURL,
934+
} {
935+
t.Run(testCaseURL, func(t *testing.T) {
936+
waitForTestComponents(t, testCaseURL)
937+
testREDMetricsForHTTPLibraryNoRouteLowCardinality(t, testCaseURL, "testserver")
938+
})
939+
}
940+
}
941+
895942
func testREDMetricsUnsupportedHTTP(t *testing.T) {
896943
for _, testCaseURL := range []string{
897944
instrumentedServiceStdURL,

internal/test/integration/suites_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,16 @@ func TestSuiteNoRoutes(t *testing.T) {
631631
require.NoError(t, compose.Close())
632632
}
633633

634+
func TestSuiteNoRoutesLowCardinality(t *testing.T) {
635+
compose, err := docker.ComposeSuite("docker-compose.yml", path.Join(pathOutput, "test-suite-no-routes-low-cardinality.log"))
636+
require.NoError(t, err)
637+
638+
compose.Env = append(compose.Env, "INSTRUMENTER_CONFIG_SUFFIX=-no-route-lc")
639+
require.NoError(t, compose.Up())
640+
t.Run("RED metrics", testREDMetricsHTTPNoRouteLowCardinality)
641+
require.NoError(t, compose.Close())
642+
}
643+
634644
func TestSuite_Elixir(t *testing.T) {
635645
compose, err := docker.ComposeSuite("docker-compose-elixir.yml", path.Join(pathOutput, "test-suite-elixir.log"))
636646
require.NoError(t, err)

pkg/appolly/app/svc/svc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"go.opentelemetry.io/obi/pkg/appolly/services"
1111
attr "go.opentelemetry.io/obi/pkg/export/attributes/names"
1212
"go.opentelemetry.io/obi/pkg/internal/transform/route"
13+
"go.opentelemetry.io/obi/pkg/internal/transform/route/clusterurl"
1314
)
1415

1516
type InstrumentableType int
@@ -117,6 +118,7 @@ type Attrs struct {
117118
CustomInRouteMatcher route.Matcher
118119
CustomOutRouteMatcher route.Matcher
119120
HarvestedRouteMatcher route.Matcher
121+
PathTrie *clusterurl.PathTrie
120122
}
121123

122124
func (i *Attrs) GetUID() UID {

pkg/appolly/discover/matcher_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"go.opentelemetry.io/obi/pkg/internal/testutil"
1515
"go.opentelemetry.io/obi/pkg/obi"
1616
"go.opentelemetry.io/obi/pkg/pipe/msg"
17+
"go.opentelemetry.io/obi/pkg/transform"
1718
)
1819

1920
func testMatch(t *testing.T, m Event[ProcessMatch], name string,
@@ -551,7 +552,7 @@ func TestCriteriaMatcher_Granular(t *testing.T) {
551552

552553
require.Len(t, planetMatch.Criteria, 2)
553554

554-
planetAttrs := makeServiceAttrs(&planetMatch)
555+
planetAttrs := makeServiceAttrs(&planetMatch, &transform.RoutesConfig{})
555556

556557
assert.True(t, planetAttrs.ExportModes.CanExportTraces())
557558
assert.False(t, planetAttrs.ExportModes.CanExportMetrics())
@@ -562,7 +563,7 @@ func TestCriteriaMatcher_Granular(t *testing.T) {
562563

563564
require.Len(t, satelliteMatch.Criteria, 2)
564565

565-
satelliteAttrs := makeServiceAttrs(&satelliteMatch)
566+
satelliteAttrs := makeServiceAttrs(&satelliteMatch, &transform.RoutesConfig{})
566567

567568
assert.False(t, satelliteAttrs.ExportModes.CanExportTraces())
568569
assert.False(t, satelliteAttrs.ExportModes.CanExportMetrics())
@@ -572,7 +573,7 @@ func TestCriteriaMatcher_Granular(t *testing.T) {
572573

573574
require.Len(t, starMatch.Criteria, 2)
574575

575-
starAttrs := makeServiceAttrs(&starMatch)
576+
starAttrs := makeServiceAttrs(&starMatch, &transform.RoutesConfig{})
576577

577578
assert.False(t, starAttrs.ExportModes.CanExportTraces())
578579
assert.True(t, starAttrs.ExportModes.CanExportMetrics())
@@ -582,7 +583,7 @@ func TestCriteriaMatcher_Granular(t *testing.T) {
582583

583584
require.Len(t, asteroidMatch.Criteria, 2)
584585

585-
asteroidAttrs := makeServiceAttrs(&asteroidMatch)
586+
asteroidAttrs := makeServiceAttrs(&asteroidMatch, &transform.RoutesConfig{})
586587

587588
assert.True(t, asteroidAttrs.ExportModes.CanExportTraces())
588589
assert.True(t, asteroidAttrs.ExportModes.CanExportMetrics())

pkg/appolly/discover/typer.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import (
2121
"go.opentelemetry.io/obi/pkg/export/imetrics"
2222
"go.opentelemetry.io/obi/pkg/internal/goexec"
2323
"go.opentelemetry.io/obi/pkg/internal/procs"
24+
"go.opentelemetry.io/obi/pkg/internal/transform/route/clusterurl"
2425
"go.opentelemetry.io/obi/pkg/kube"
2526
"go.opentelemetry.io/obi/pkg/obi"
2627
"go.opentelemetry.io/obi/pkg/pipe/msg"
2728
"go.opentelemetry.io/obi/pkg/pipe/swarm"
2829
"go.opentelemetry.io/obi/pkg/pipe/swarm/swarms"
30+
"go.opentelemetry.io/obi/pkg/transform"
2931
)
3032

3133
type instrumentedExecutable struct {
@@ -87,7 +89,7 @@ func samplerFromConfig(s *services.SamplerConfig) trace.Sampler {
8789
return nil
8890
}
8991

90-
func makeServiceAttrs(processMatch *ProcessMatch) svc.Attrs {
92+
func makeServiceAttrs(processMatch *ProcessMatch, routesCfg *transform.RoutesConfig) svc.Attrs {
9193
var name string
9294
var namespace string
9395
exportModes := services.ExportModeUnset
@@ -116,6 +118,11 @@ func makeServiceAttrs(processMatch *ProcessMatch) svc.Attrs {
116118
}
117119
}
118120

121+
wildcard := byte('*')
122+
if routesCfg.WildcardChar != "" {
123+
wildcard = routesCfg.WildcardChar[0]
124+
}
125+
119126
s := svc.Attrs{
120127
UID: svc.UID{
121128
Name: name,
@@ -124,6 +131,7 @@ func makeServiceAttrs(processMatch *ProcessMatch) svc.Attrs {
124131
ProcPID: processMatch.Process.Pid,
125132
ExportModes: exportModes,
126133
Sampler: samplerFromConfig(samplerConfig),
134+
PathTrie: clusterurl.NewPathTrie(routesCfg.MaxPathSegmentCardinality, wildcard),
127135
}
128136

129137
if routesConfig != nil {
@@ -146,7 +154,7 @@ func (t *typer) FilterClassify(evs []Event[ProcessMatch]) []Event[ebpf.Instrumen
146154
ev := &evs[i]
147155
switch evs[i].Type {
148156
case EventCreated:
149-
svcID := makeServiceAttrs(&ev.Obj)
157+
svcID := makeServiceAttrs(&ev.Obj, t.cfg.Routes)
150158

151159
if elfFile, err := findExecElf(ev.Obj.Process, svcID, t.k8sInformer.IsKubeEnabled()); err != nil {
152160
t.log.Debug("error finding process ELF. Ignoring", "error", err)

pkg/appolly/discover/typer_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/stretchr/testify/assert"
1111

1212
"go.opentelemetry.io/obi/pkg/appolly/services"
13+
"go.opentelemetry.io/obi/pkg/transform"
1314
)
1415

1516
type dummyCriterion struct {
@@ -41,7 +42,7 @@ func TestMakeServiceAttrs(t *testing.T) {
4142
dummyCriterion{name: "svc1", namespace: "ns1", export: services.ExportModeUnset},
4243
},
4344
}
44-
attrs := makeServiceAttrs(proc)
45+
attrs := makeServiceAttrs(proc, &transform.RoutesConfig{})
4546
assert.Equal(t, "svc1", attrs.UID.Name)
4647
assert.Equal(t, "ns1", attrs.UID.Namespace)
4748
assert.Equal(t, int32(1234), attrs.ProcPID)
@@ -60,7 +61,7 @@ func TestMakeServiceAttrs(t *testing.T) {
6061
dummyCriterion{sampler: sampler, routes: routes},
6162
},
6263
}
63-
attrs2 := makeServiceAttrs(proc2)
64+
attrs2 := makeServiceAttrs(proc2, &transform.RoutesConfig{})
6465
assert.NotNil(t, attrs2.Sampler)
6566
assert.NotNil(t, attrs2.CustomInRouteMatcher)
6667
assert.NotNil(t, attrs2.CustomOutRouteMatcher)

pkg/internal/transform/route/clusterurl/cluster_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ func TestClusterURL(t *testing.T) {
6666
assert.Equal(t, "/*", csf.ClusterURL("/1#"))
6767
assert.Equal(t, "a", csf.ClusterURL("a#"))
6868
assert.Equal(t, "/a/b/c/d/e/f/g/h/i", csf.ClusterURL("/a/b/c/d/e/f/g/h/i/j"))
69+
assert.Equal(t, "/api/user", csf.ClusterURL("/api/user"))
70+
assert.Equal(t, "/api/customer", csf.ClusterURL("/api/customer"))
71+
assert.Equal(t, "/api/test", csf.ClusterURL("/api/test"))
72+
assert.Equal(t, "/api/option", csf.ClusterURL("/api/option"))
73+
assert.Equal(t, "/api/metric", csf.ClusterURL("/api/metric"))
6974
}
7075

7176
func BenchmarkClusterURLWithCache(b *testing.B) {

0 commit comments

Comments
 (0)