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
5 changes: 3 additions & 2 deletions internal/metrics/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ var (
relayIDTagKey, _ = tag.NewKey("relayId") //nolint:gochecknoglobals
platformCategoryTagKey, _ = tag.NewKey("platformCategory") //nolint:gochecknoglobals
userAgentTagKey, _ = tag.NewKey("userAgent") //nolint:gochecknoglobals
sdkWrapperTagKey, _ = tag.NewKey("sdkWrapper") //nolint:gochecknoglobals
routeTagKey, _ = tag.NewKey("route") //nolint:gochecknoglobals
methodTagKey, _ = tag.NewKey("method") //nolint:gochecknoglobals
envNameTagKey, _ = tag.NewKey("env") //nolint:gochecknoglobals

publicTags = []tag.Key{platformCategoryTagKey, userAgentTagKey, envNameTagKey} //nolint:gochecknoglobals
privateTags = []tag.Key{platformCategoryTagKey, userAgentTagKey, relayIDTagKey, envNameTagKey} //nolint:gochecknoglobals
publicTags = []tag.Key{platformCategoryTagKey, userAgentTagKey, sdkWrapperTagKey, envNameTagKey} //nolint:gochecknoglobals
privateTags = []tag.Key{platformCategoryTagKey, userAgentTagKey, sdkWrapperTagKey, relayIDTagKey, envNameTagKey} //nolint:gochecknoglobals
)
20 changes: 15 additions & 5 deletions internal/metrics/events_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,21 @@ import (

type currentConnectionsMetric struct {
UserAgent string `json:"userAgent"`
SDKWrapper string `json:"sdkWrapper"`
PlatformCategory string `json:"platformCategory"`
Current int64 `json:"current"`
}

type newConnectionsMetric struct {
UserAgent string `json:"userAgent"`
SDKWrapper string `json:"sdkWrapper"`
PlatformCategory string `json:"platformCategory"`
Count int64 `json:"count"`
}

type pollingMetric struct {
UserAgent string `json:"userAgent"`
SDKWrapper string `json:"sdkWrapper"`
PlatformCategory string `json:"platformCategory"`
Count int64 `json:"count"`
}
Expand All @@ -44,6 +47,7 @@ type relayMetricsEvent struct {

type connectionsKeyType struct {
userAgent string
sdkWrapper string
platformCategory string
}

Expand Down Expand Up @@ -108,6 +112,7 @@ func (e *openCensusEventsExporter) ExportView(viewData *view.Data) {
for _, r := range viewData.Rows {
var platformCategory string
var userAgent string
var sdkWrapper string
relayIDFound := false
envNameFound := false
for _, t := range r.Tags {
Expand All @@ -126,6 +131,8 @@ func (e *openCensusEventsExporter) ExportView(viewData *view.Data) {
}
case userAgentTagKey:
userAgent = t.Value
case sdkWrapperTagKey:
sdkWrapper = t.Value
case platformCategoryTagKey:
platformCategory = t.Value
}
Expand All @@ -137,17 +144,17 @@ func (e *openCensusEventsExporter) ExportView(viewData *view.Data) {
if data, ok := r.Data.(*view.SumData); ok {
v = int64(data.Value)
}
e.updateValue(viewData.View.Name, platformCategory, userAgent, v)
e.updateValue(viewData.View.Name, platformCategory, userAgent, sdkWrapper, v)
}
}
}

func (e *openCensusEventsExporter) updateValue(name string, platformCategory string, userAgent string, value int64) {
func (e *openCensusEventsExporter) updateValue(name string, platformCategory string, userAgent string, sdkWrapper string, value int64) {
e.mu.Lock()
defer e.mu.Unlock()
switch name {
case privatePollingRequestsMeasureName:
key := connectionsKeyType{platformCategory: platformCategory, userAgent: userAgent}
key := connectionsKeyType{platformCategory: platformCategory, userAgent: userAgent, sdkWrapper: sdkWrapper}
if value == 0 {
delete(e.pollingCounts, key)
break
Expand All @@ -161,15 +168,15 @@ func (e *openCensusEventsExporter) updateValue(name string, platformCategory str
}

case privateConnMeasureName:
key := connectionsKeyType{platformCategory: platformCategory, userAgent: userAgent}
key := connectionsKeyType{platformCategory: platformCategory, userAgent: userAgent, sdkWrapper: sdkWrapper}
if value == 0 {
delete(e.currentConnections, key)
} else {
e.currentConnections[key] = value
}

case privateNewConnMeasureName:
key := connectionsKeyType{platformCategory: platformCategory, userAgent: userAgent}
key := connectionsKeyType{platformCategory: platformCategory, userAgent: userAgent, sdkWrapper: sdkWrapper}
if value == 0 {
delete(e.newConnections, key) // COVERAGE: won't happen in practice since this measure is only ever incremented
} else {
Expand Down Expand Up @@ -225,6 +232,7 @@ func (e *openCensusEventsExporter) flush() {
if v.running != v.lastReported {
event.PollingCounts = append(event.PollingCounts, pollingMetric{
UserAgent: k.userAgent,
SDKWrapper: k.sdkWrapper,
PlatformCategory: k.platformCategory,
Count: v.running - v.lastReported,
})
Expand All @@ -237,13 +245,15 @@ func (e *openCensusEventsExporter) flush() {
for k, v := range e.currentConnections {
event.Connections = append(event.Connections, currentConnectionsMetric{
UserAgent: k.userAgent,
SDKWrapper: k.sdkWrapper,
PlatformCategory: k.platformCategory,
Current: v,
})
}
for k, v := range e.newConnections {
event.NewConnections = append(event.NewConnections, newConnectionsMetric{
UserAgent: k.userAgent,
SDKWrapper: k.sdkWrapper,
PlatformCategory: k.platformCategory,
Count: v,
})
Expand Down
12 changes: 6 additions & 6 deletions internal/metrics/measures.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ func makeServerTags() []tag.Mutator {

// WithGauge increments the specified metric before running the function and then decrements it (for use with
// the active connection metrics).
func WithGauge(ctx context.Context, userAgent string, f func(), measure Measure) {
ctx, err := tag.New(ctx, tag.Insert(userAgentTagKey, sanitizeTagValue(userAgent)))
func WithGauge(ctx context.Context, userAgent, sdkWrapper string, f func(), measure Measure) {
ctx, err := tag.New(ctx, tag.Insert(userAgentTagKey, sanitizeTagValue(userAgent)), tag.Insert(sdkWrapperTagKey, sanitizeTagValue(sdkWrapper)))
if err != nil { // COVERAGE: can't make this happen in unit tests
logging.GetGlobalContextLoggers(ctx).Errorf(`Failed to create tags: %s`, err)
} else {
Expand All @@ -94,8 +94,8 @@ func WithGauge(ctx context.Context, userAgent string, f func(), measure Measure)
}

// WithCount runs a function and records a single-unit increment for the specified metric.
func WithCount(ctx context.Context, userAgent string, f func(), measure Measure) {
ctx, err := tag.New(ctx, tag.Insert(userAgentTagKey, sanitizeTagValue(userAgent)))
func WithCount(ctx context.Context, userAgent, sdkWrapper string, f func(), measure Measure) {
ctx, err := tag.New(ctx, tag.Insert(userAgentTagKey, sanitizeTagValue(userAgent)), tag.Insert(sdkWrapperTagKey, sanitizeTagValue(sdkWrapper)))
if err != nil { // COVERAGE: can't make this happen in unit tests
logging.GetGlobalContextLoggers(ctx).Errorf(`Failed to create tag for user agent : %s`, err)
} else {
Expand All @@ -108,7 +108,7 @@ func WithCount(ctx context.Context, userAgent string, f func(), measure Measure)
}

// WithRouteCount records a route hit and starts a trace. For stream connections, the duration of the stream connection is recorded
func WithRouteCount(ctx context.Context, userAgent, route, method string, f func(), measure Measure) {
func WithRouteCount(ctx context.Context, userAgent, sdkWrapper, route, method string, f func(), measure Measure) {
tagCtx, err := tag.New(ctx, tag.Insert(routeTagKey, sanitizeTagValue(route)), tag.Insert(methodTagKey, sanitizeTagValue(method)))
if err != nil { // COVERAGE: can't make this happen in unit tests
logging.GetGlobalContextLoggers(ctx).Errorf(`Failed to create tags for route "%s %s": %s`, method, route, err)
Expand All @@ -118,5 +118,5 @@ func WithRouteCount(ctx context.Context, userAgent, route, method string, f func
ctx, span := trace.StartSpan(ctx, route)
defer span.End()

WithCount(ctx, userAgent, f, measure)
WithCount(ctx, userAgent, sdkWrapper, f, measure)
}
6 changes: 4 additions & 2 deletions internal/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,12 @@ func (em *EnvironmentManager) close() {
})
}

// Pad empty keys to match tag keyset cardinality since empty strings are dropped
// sanitizeTagValue ensures tag values are valid for OpenCensus metrics.
// OpenCensus drops empty tag values, which causes cardinality mismatches in views.
// We use descriptive default values instead of generic placeholders.
func sanitizeTagValue(v string) string {
if strings.TrimSpace(v) == "" {
return "_"
return "not-provided"
}
return strings.ReplaceAll(v, "/", "_")
}
12 changes: 8 additions & 4 deletions internal/metrics/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func (m measureAndPlatform) getExpectedTagsMap(relayID string, envName string, u
envNameTagKey.Name(): envName,
platformCategoryTagKey.Name(): m.platform,
userAgentTagKey.Name(): userAgent,
sdkWrapperTagKey.Name(): "not-provided", // empty wrapper defaults to "not-provided"
}
if relayID != "" {
ret[relayIDTagKey.Name()] = relayID
Expand Down Expand Up @@ -111,7 +112,7 @@ func TestConnectionMetrics(t *testing.T) {
expectedTags := tt.getExpectedTagsMap("", p.envName, userAgentValue)
expectedPrivateTags := tt.getExpectedTagsMap(p.relayID, p.envName, userAgentValue)

WithGauge(p.env.GetOpenCensusContext(), userAgentValue, func() {
WithGauge(p.env.GetOpenCensusContext(), userAgentValue, "", func() {
p.exporter.AwaitData(t, time.Second, p.mockLog.Loggers, func(d st.TestMetricsData) bool {
return d.HasRow(publicConnView.Name, st.TestMetricsRow{
Tags: expectedTags,
Expand Down Expand Up @@ -150,7 +151,7 @@ func TestNewConnectionMetrics(t *testing.T) {
expectedTags := tt.getExpectedTagsMap("", p.envName, userAgentValue)
expectedPrivateTags := tt.getExpectedTagsMap(p.relayID, p.envName, userAgentValue)

WithCount(p.env.GetOpenCensusContext(), userAgentValue, func() {}, tt.measure)
WithCount(p.env.GetOpenCensusContext(), userAgentValue, "", func() {}, tt.measure)

p.exporter.AwaitData(t, time.Second, p.mockLog.Loggers, func(d st.TestMetricsData) bool {
return d.HasRow(publicNewConnView.Name, st.TestMetricsRow{
Expand All @@ -168,7 +169,7 @@ func TestNewConnectionMetrics(t *testing.T) {

func TestWithRouteCount(t *testing.T) {
testWithExporter(t, func(p testWithExporterParams) {
WithRouteCount(p.env.GetOpenCensusContext(), userAgentValue, "someRoute", "GET", func() {
WithRouteCount(p.env.GetOpenCensusContext(), userAgentValue, "", "someRoute", "GET", func() {
p.exporter.AwaitData(t, time.Second, p.mockLog.Loggers, func(d st.TestMetricsData) bool {
return d.HasRow(requestView.Name, st.TestMetricsRow{
Tags: map[string]string{
Expand All @@ -177,6 +178,7 @@ func TestWithRouteCount(t *testing.T) {
"platformCategory": "server",
"route": "someRoute",
"userAgent": userAgentValue,
"sdkWrapper": "not-provided",
},
Count: 1,
})
Expand All @@ -189,5 +191,7 @@ func TestWithRouteCount(t *testing.T) {

func TestSanitizeTagValue(t *testing.T) {
assert.Equal(t, "abc", sanitizeTagValue("abc"))
assert.Equal(t, "_", sanitizeTagValue(""))
assert.Equal(t, "not-provided", sanitizeTagValue(""))
assert.Equal(t, "not-provided", sanitizeTagValue(" "))
assert.Equal(t, "react_2.0.0", sanitizeTagValue("react/2.0.0"))
}
9 changes: 6 additions & 3 deletions internal/middleware/metrics_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ func withCount(handler http.Handler, measure metrics.Measure) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := GetEnvContextInfo(req.Context()).Env
userAgent := getUserAgent(req)
metrics.WithCount(ctx.GetMetricsContext(), userAgent, func() {
sdkWrapper := getSDKWrapper(req)
metrics.WithCount(ctx.GetMetricsContext(), userAgent, sdkWrapper, func() {
handler.ServeHTTP(w, req)
}, measure)
})
Expand All @@ -22,7 +23,8 @@ func withGauge(handler http.Handler, measure metrics.Measure) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := GetEnvContextInfo(req.Context())
userAgent := getUserAgent(req)
metrics.WithGauge(ctx.Env.GetMetricsContext(), userAgent, func() {
sdkWrapper := getSDKWrapper(req)
metrics.WithGauge(ctx.Env.GetMetricsContext(), userAgent, sdkWrapper, func() {
handler.ServeHTTP(w, req)
}, measure)
})
Expand Down Expand Up @@ -57,9 +59,10 @@ func RequestCount(measure metrics.Measure) mux.MiddlewareFunc {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := GetEnvContextInfo(req.Context())
userAgent := getUserAgent(req)
sdkWrapper := getSDKWrapper(req)
// Ignoring internal routing error that would have been ignored anyway
route, _ := mux.CurrentRoute(req).GetPathTemplate()
metrics.WithRouteCount(ctx.Env.GetMetricsContext(), userAgent, route, req.Method, func() {
metrics.WithRouteCount(ctx.Env.GetMetricsContext(), userAgent, sdkWrapper, route, req.Method, func() {
next.ServeHTTP(w, req)
}, measure)
})
Expand Down
2 changes: 2 additions & 0 deletions internal/middleware/metrics_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func testCountConnections(t *testing.T, countFn func(http.Handler) http.Handler,
"env": p.envName,
"platformCategory": category,
"userAgent": metricsTestUserAgent,
"sdkWrapper": "not-provided",
}

req, _ := http.NewRequest("GET", "", nil)
Expand Down Expand Up @@ -142,6 +143,7 @@ func testCountRequests(t *testing.T, measure metrics.Measure, category string) {
"route": "_test-route",
"platformCategory": category,
"userAgent": metricsTestUserAgent,
"sdkWrapper": "not-provided",
}

makeRequest := func() *http.Request {
Expand Down
6 changes: 6 additions & 0 deletions internal/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
userAgentHeader = "user-agent"
ldUserAgentHeader = "X-LaunchDarkly-User-Agent"
ldInstanceIDHeader = "X-LaunchDarkly-Instance-Id"
ldWrapperHeader = "X-LaunchDarkly-Wrapper"

httpStatusMessageInvalidEnvCredential = "Relay Proxy does not recognize the client credential (missing or invalid Authorization header)"
httpStatusMessageNotFullyConfigured = "Relay Proxy is not yet fully initialized, does not have list of environments yet"
Expand Down Expand Up @@ -69,6 +70,11 @@ func getInstanceID(req *http.Request) string {
return req.Header.Get(ldInstanceIDHeader)
}

// getSDKWrapper returns the X-LaunchDarkly-Wrapper if available
func getSDKWrapper(req *http.Request) string {
return req.Header.Get(ldWrapperHeader)
}

// Chain combines a series of middleware functions that will be applied in the same order.
func Chain(middlewares ...mux.MiddlewareFunc) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
Expand Down
16 changes: 16 additions & 0 deletions internal/middleware/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,22 @@ func TestGetUserAgent(t *testing.T) {
req.Header.Set(userAgentHeader, "my-agent")
assert.Equal(t, "my-agent", getUserAgent(req))
})
t.Run("returns empty string when no user-agent headers are present", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
assert.Equal(t, "", getUserAgent(req))
})
}

func TestGetSDKWrapper(t *testing.T) {
t.Run("returns X-LaunchDarkly-Wrapper header value", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set(ldWrapperHeader, "react/2.0.0")
assert.Equal(t, "react/2.0.0", getSDKWrapper(req))
})
t.Run("returns empty string when X-LaunchDarkly-Wrapper header is not present", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
assert.Equal(t, "", getSDKWrapper(req))
})
}

func TestSelectEnvironmentByAuthorizationKey(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions internal/relayenv/env_context_impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ func TestMetricsAreExportedForEnvironment(t *testing.T) {
require.NoError(t, err)
defer env.Close()
envImpl := env.(*envContextImpl)
metrics.WithCount(env.GetMetricsContext(), fakeUserAgent, func() {
metrics.WithCount(env.GetMetricsContext(), fakeUserAgent, "", func() {
require.Eventually(t, func() bool {
flushMetricsEvents(envImpl)
select {
Expand Down Expand Up @@ -406,7 +406,7 @@ func testMetricsDisabled(t *testing.T, allConfig config.Config) {
require.NoError(t, err)
defer env.Close()
envImpl := env.(*envContextImpl)
metrics.WithCount(env.GetMetricsContext(), fakeUserAgent, func() {
metrics.WithCount(env.GetMetricsContext(), fakeUserAgent, "", func() {
require.Never(t, func() bool {
flushMetricsEvents(envImpl)
select {
Expand Down
Loading