diff --git a/internal/metrics/constants.go b/internal/metrics/constants.go new file mode 100644 index 00000000..02f3efba --- /dev/null +++ b/internal/metrics/constants.go @@ -0,0 +1,29 @@ +package metrics + +import ( + "time" + + "go.opencensus.io/tag" +) + +const ( + defaultMetricsPrefix = "launchdarkly_relay" + + browserTagValue = "browser" + mobileTagValue = "mobile" + serverTagValue = "server" + + defaultFlushInterval = time.Minute +) + +var ( + relayIdTagKey, _ = tag.NewKey("relayId") + platformCategoryTagKey, _ = tag.NewKey("platformCategory") + userAgentTagKey, _ = tag.NewKey("userAgent") + routeTagKey, _ = tag.NewKey("route") + methodTagKey, _ = tag.NewKey("method") + envNameTagKey, _ = tag.NewKey("env") + + publicTags = []tag.Key{platformCategoryTagKey, userAgentTagKey, envNameTagKey} + privateTags = []tag.Key{platformCategoryTagKey, userAgentTagKey, relayIdTagKey, envNameTagKey} +) diff --git a/internal/metrics/datadog.go b/internal/metrics/datadog.go index bc8ebd8d..a3916c8c 100644 --- a/internal/metrics/datadog.go +++ b/internal/metrics/datadog.go @@ -12,7 +12,7 @@ import ( ) func init() { - defineExporter(datadogExporter, registerDatadogExporter) + defineExporter(datadogExporterType, registerDatadogExporter) } type DatadogOptions struct { @@ -23,7 +23,7 @@ type DatadogOptions struct { } func (d DatadogOptions) getType() ExporterType { - return datadogExporter + return datadogExporterType } type DatadogConfig config.DatadogConfig diff --git a/internal/metrics/datadog_go19.go b/internal/metrics/datadog_go19.go deleted file mode 100644 index c2a146f2..00000000 --- a/internal/metrics/datadog_go19.go +++ /dev/null @@ -1,15 +0,0 @@ -// +build !go1.10 386 - -package metrics - -import ( - "errors" -) - -func init() { - defineExporter(datadogExporter, registerFailDatadogExporter) -} - -func registerFailDatadogExporter(options ExporterOptions) error { - return errors.New("The Datadog metrics exporter requires Go version 1.10 or greater") -} diff --git a/internal/metrics/exporters.go b/internal/metrics/exporters.go new file mode 100644 index 00000000..7eda1d1b --- /dev/null +++ b/internal/metrics/exporters.go @@ -0,0 +1,77 @@ +package metrics + +import ( + "fmt" + + "go.opencensus.io/stats/view" + + "github.com/launchdarkly/ld-relay/v6/config" + "gopkg.in/launchdarkly/go-sdk-common.v2/ldlog" +) + +type ExporterType string + +const ( + datadogExporterType ExporterType = "Datadog" + stackdriverExporterType ExporterType = "Stackdriver" + prometheusExporterType ExporterType = "Prometheus" +) + +type ExporterOptions interface { + getType() ExporterType +} + +type ExporterRegisterer func(options ExporterOptions) error + +// ExporterConfig is used internally to hold options for metrics integrations. +type ExporterConfig interface { + toOptions() ExporterOptions + enabled() bool +} + +func ExporterOptionsFromConfig(c config.MetricsConfig) (options []ExporterOptions) { + exporterConfigs := []ExporterConfig{ + DatadogConfig(c.Datadog), + StackdriverConfig(c.Stackdriver), + PrometheusConfig(c.Prometheus)} + for _, e := range exporterConfigs { + if e.enabled() { + options = append(options, e.toOptions()) + } + } + return options +} + +func getPrefix(c config.CommonMetricsConfig) string { + if c.Prefix != "" { + return c.Prefix + } + return defaultMetricsPrefix +} + +func defineExporter(exporterType ExporterType, registerer ExporterRegisterer) { + exporters[exporterType] = registerer +} + +func RegisterExporters(options []ExporterOptions, loggers ldlog.Loggers) (registrationErr error) { + registerPublicExportersOnce.Do(func() { + for _, o := range options { + exporter := exporters[o.getType()] + if exporter == nil { + registrationErr = fmt.Errorf("Got unexpected exporter type: %s", o.getType()) + return + } else if err := exporter(o); err != nil { + registrationErr = fmt.Errorf("Could not register %s exporter: %s", o.getType(), err) + return + } else { + loggers.Infof("Successfully registered %s exporter.", o.getType()) + } + } + + err := view.Register(getPublicViews()...) + if err != nil { + registrationErr = fmt.Errorf("Error registering metrics views") + } + }) + return registrationErr +} diff --git a/internal/metrics/globals.go b/internal/metrics/globals.go new file mode 100644 index 00000000..44db847a --- /dev/null +++ b/internal/metrics/globals.go @@ -0,0 +1,67 @@ +package metrics + +import ( + "sync" + + "github.com/pborman/uuid" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" +) + +var ( + exporters = map[ExporterType]ExporterRegisterer{} + registerPublicExportersOnce sync.Once + registerPrivateViewsOnce sync.Once + metricsRelayId string + + browserTags []tag.Mutator + mobileTags []tag.Mutator + serverTags []tag.Mutator + + publicConnView *view.View + publicNewConnView *view.View + requestView *view.View + privateConnView *view.View + privateNewConnView *view.View +) + +func init() { + metricsRelayId = uuid.New() + browserTags = append(browserTags, tag.Insert(platformCategoryTagKey, browserTagValue)) + mobileTags = append(mobileTags, tag.Insert(platformCategoryTagKey, mobileTagValue)) + serverTags = append(serverTags, tag.Insert(platformCategoryTagKey, serverTagValue)) + + publicConnView = &view.View{ + Measure: connMeasure, + Aggregation: view.Sum(), + TagKeys: publicTags, + } + publicNewConnView = &view.View{ + Measure: newConnMeasure, + Aggregation: view.Sum(), + TagKeys: publicTags, + } + requestView = &view.View{ + Measure: requestMeasure, + Aggregation: view.Count(), + TagKeys: append(publicTags, routeTagKey, methodTagKey), + } + privateConnView = &view.View{ + Measure: privateConnMeasure, + Aggregation: view.Sum(), + TagKeys: privateTags, + } + privateNewConnView = &view.View{ + Measure: privateNewConnMeasure, + Aggregation: view.Sum(), + TagKeys: privateTags, + } +} + +func getPublicViews() []*view.View { + return []*view.View{publicConnView, publicNewConnView, requestView} +} + +func getPrivateViews() []*view.View { + return []*view.View{privateConnView, privateNewConnView} +} diff --git a/internal/metrics/measures.go b/internal/metrics/measures.go new file mode 100644 index 00000000..f2451785 --- /dev/null +++ b/internal/metrics/measures.go @@ -0,0 +1,79 @@ +package metrics + +import ( + "context" + + "go.opencensus.io/stats" + "go.opencensus.io/tag" + "go.opencensus.io/trace" + + "github.com/launchdarkly/ld-relay/v6/internal/logging" +) + +var ( + // For internal event exporter + privateConnMeasure = stats.Int64("internal_connections", "current number of connections", stats.UnitDimensionless) + privateNewConnMeasure = stats.Int64("internal_newconnections", "total number of connections", stats.UnitDimensionless) + + connMeasure = stats.Int64("connections", "current number of connections", stats.UnitDimensionless) + newConnMeasure = stats.Int64("newconnections", "total number of connections", stats.UnitDimensionless) + requestMeasure = stats.Int64("requests", "Number of hits to a route", stats.UnitDimensionless) + + BrowserConns = Measure{measures: []*stats.Int64Measure{connMeasure, privateConnMeasure}, tags: &browserTags} + MobileConns = Measure{measures: []*stats.Int64Measure{connMeasure, privateConnMeasure}, tags: &mobileTags} + ServerConns = Measure{measures: []*stats.Int64Measure{connMeasure, privateConnMeasure}, tags: &serverTags} + + NewBrowserConns = Measure{measures: []*stats.Int64Measure{newConnMeasure, privateNewConnMeasure}, tags: &browserTags} + NewMobileConns = Measure{measures: []*stats.Int64Measure{newConnMeasure, privateNewConnMeasure}, tags: &mobileTags} + NewServerConns = Measure{measures: []*stats.Int64Measure{newConnMeasure, privateNewConnMeasure}, tags: &serverTags} + + BrowserRequests = Measure{measures: []*stats.Int64Measure{requestMeasure}, tags: &browserTags} + MobileRequests = Measure{measures: []*stats.Int64Measure{requestMeasure}, tags: &mobileTags} + ServerRequests = Measure{measures: []*stats.Int64Measure{requestMeasure}, tags: &serverTags} +) + +type Measure struct { + measures []*stats.Int64Measure + tags *[]tag.Mutator +} + +func WithGauge(ctx context.Context, userAgent string, f func(), measure Measure) { + ctx, err := tag.New(ctx, tag.Insert(userAgentTagKey, sanitizeTagValue(userAgent))) + if err != nil { + logging.GetGlobalContextLoggers(ctx).Errorf(`Failed to create tags: %s`, err) + } else { + for _, m := range measure.measures { + ctx, _ := tag.New(ctx, *measure.tags...) + stats.Record(ctx, m.M(1)) + defer stats.Record(ctx, m.M(-1)) + } + } + f() +} + +func WithCount(ctx context.Context, userAgent string, f func(), measure Measure) { + ctx, err := tag.New(ctx, tag.Insert(userAgentTagKey, sanitizeTagValue(userAgent))) + if err != nil { + logging.GetGlobalContextLoggers(ctx).Errorf(`Failed to create tag for user agent : %s`, err) + } else { + for _, m := range measure.measures { + ctx, _ := tag.New(ctx, *measure.tags...) + stats.Record(ctx, m.M(1)) + } + } + f() +} + +// 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) { + tagCtx, err := tag.New(ctx, tag.Insert(routeTagKey, sanitizeTagValue(route)), tag.Insert(methodTagKey, sanitizeTagValue(method))) + if err != nil { + logging.GetGlobalContextLoggers(ctx).Errorf(`Failed to create tags for route "%s %s": %s`, method, route, err) + } else { + ctx = tagCtx + } + ctx, span := trace.StartSpan(ctx, route) + defer span.End() + + WithCount(ctx, userAgent, f, measure) +} diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 59a52c52..910a69af 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -7,134 +7,12 @@ import ( "sync" "time" - "go.opencensus.io/stats" "go.opencensus.io/stats/view" "go.opencensus.io/tag" - "go.opencensus.io/trace" - "gopkg.in/launchdarkly/go-sdk-common.v2/ldlog" - - "github.com/pborman/uuid" - - "github.com/launchdarkly/ld-relay/v6/config" "github.com/launchdarkly/ld-relay/v6/internal/events" - "github.com/launchdarkly/ld-relay/v6/internal/logging" ) -type ExporterType string - -const ( - datadogExporter ExporterType = "Datadog" - stackdriverExporter ExporterType = "Stackdriver" - prometheusExporter ExporterType = "Prometheus" - defaultMetricsPrefix = "launchdarkly_relay" - - browser = "browser" - mobile = "mobile" - server = "server" - - defaultFlushInterval = time.Minute -) - -type ExporterOptions interface { - getType() ExporterType -} - -type ExporterRegisterer func(options ExporterOptions) error - -type Measure struct { - measures []*stats.Int64Measure - tags *[]tag.Mutator -} - -var ( - exporters = map[ExporterType]ExporterRegisterer{} - registerPublicExportersOnce sync.Once - registerPrivateViewsOnce sync.Once - metricsRelayId string - - relayIdTagKey, _ = tag.NewKey("relayId") - platformCategoryTagKey, _ = tag.NewKey("platformCategory") - userAgentTagKey, _ = tag.NewKey("userAgent") - routeTagKey, _ = tag.NewKey("route") - methodTagKey, _ = tag.NewKey("method") - envNameTagKey, _ = tag.NewKey("env") - - // For internal event exporter - privateConnMeasure = stats.Int64("internal_connections", "current number of connections", stats.UnitDimensionless) - privateNewConnMeasure = stats.Int64("internal_newconnections", "total number of connections", stats.UnitDimensionless) - - connMeasure = stats.Int64("connections", "current number of connections", stats.UnitDimensionless) - newConnMeasure = stats.Int64("newconnections", "total number of connections", stats.UnitDimensionless) - requestMeasure = stats.Int64("requests", "Number of hits to a route", stats.UnitDimensionless) - - browserTags []tag.Mutator - mobileTags []tag.Mutator - serverTags []tag.Mutator - - publicTags = []tag.Key{platformCategoryTagKey, userAgentTagKey, envNameTagKey} - privateTags = []tag.Key{platformCategoryTagKey, userAgentTagKey, relayIdTagKey, envNameTagKey} - - publicConnView *view.View - publicNewConnView *view.View - requestView *view.View - privateConnView *view.View - privateNewConnView *view.View - - BrowserConns = Measure{measures: []*stats.Int64Measure{connMeasure, privateConnMeasure}, tags: &browserTags} - MobileConns = Measure{measures: []*stats.Int64Measure{connMeasure, privateConnMeasure}, tags: &mobileTags} - ServerConns = Measure{measures: []*stats.Int64Measure{connMeasure, privateConnMeasure}, tags: &serverTags} - - NewBrowserConns = Measure{measures: []*stats.Int64Measure{newConnMeasure, privateNewConnMeasure}, tags: &browserTags} - NewMobileConns = Measure{measures: []*stats.Int64Measure{newConnMeasure, privateNewConnMeasure}, tags: &mobileTags} - NewServerConns = Measure{measures: []*stats.Int64Measure{newConnMeasure, privateNewConnMeasure}, tags: &serverTags} - - BrowserRequests = Measure{measures: []*stats.Int64Measure{requestMeasure}, tags: &browserTags} - MobileRequests = Measure{measures: []*stats.Int64Measure{requestMeasure}, tags: &mobileTags} - ServerRequests = Measure{measures: []*stats.Int64Measure{requestMeasure}, tags: &serverTags} -) - -func init() { - metricsRelayId = uuid.New() - browserTags = append(browserTags, tag.Insert(platformCategoryTagKey, browser)) - mobileTags = append(mobileTags, tag.Insert(platformCategoryTagKey, mobile)) - serverTags = append(serverTags, tag.Insert(platformCategoryTagKey, server)) - - publicConnView = &view.View{ - Measure: connMeasure, - Aggregation: view.Sum(), - TagKeys: publicTags, - } - publicNewConnView = &view.View{ - Measure: newConnMeasure, - Aggregation: view.Sum(), - TagKeys: publicTags, - } - requestView = &view.View{ - Measure: requestMeasure, - Aggregation: view.Count(), - TagKeys: append(publicTags, routeTagKey, methodTagKey), - } - privateConnView = &view.View{ - Measure: privateConnMeasure, - Aggregation: view.Sum(), - TagKeys: privateTags, - } - privateNewConnView = &view.View{ - Measure: privateNewConnMeasure, - Aggregation: view.Sum(), - TagKeys: privateTags, - } -} - -func getPublicViews() []*view.View { - return []*view.View{publicConnView, publicNewConnView, requestView} -} - -func getPrivateViews() []*view.View { - return []*view.View{privateConnView, privateNewConnView} -} - type Processor struct { OpenCensusCtx context.Context closer chan<- struct{} @@ -160,59 +38,6 @@ func (o OptionEnvName) apply(p *Processor) error { return nil } -// ExporterConfig is used internally to hold options for metrics integrations. -type ExporterConfig interface { - toOptions() ExporterOptions - enabled() bool -} - -func ExporterOptionsFromConfig(c config.MetricsConfig) (options []ExporterOptions) { - exporterConfigs := []ExporterConfig{ - DatadogConfig(c.Datadog), - StackdriverConfig(c.Stackdriver), - PrometheusConfig(c.Prometheus)} - for _, e := range exporterConfigs { - if e.enabled() { - options = append(options, e.toOptions()) - } - } - return options -} - -func getPrefix(c config.CommonMetricsConfig) string { - if c.Prefix != "" { - return c.Prefix - } - return defaultMetricsPrefix -} - -func defineExporter(exporterType ExporterType, registerer ExporterRegisterer) { - exporters[exporterType] = registerer -} - -func RegisterExporters(options []ExporterOptions, loggers ldlog.Loggers) (registrationErr error) { - registerPublicExportersOnce.Do(func() { - for _, o := range options { - exporter := exporters[o.getType()] - if exporter == nil { - registrationErr = fmt.Errorf("Got unexpected exporter type: %s", o.getType()) - return - } else if err := exporter(o); err != nil { - registrationErr = fmt.Errorf("Could not register %s exporter: %s", o.getType(), err) - return - } else { - loggers.Infof("Successfully registered %s exporter.", o.getType()) - } - } - - err := view.Register(getPublicViews()...) - if err != nil { - registrationErr = fmt.Errorf("Error registering metrics views") - } - }) - return registrationErr -} - func registerPrivateViews() (err error) { registerPrivateViewsOnce.Do(func() { err = view.Register(getPrivateViews()...) @@ -260,47 +85,6 @@ func (p *Processor) Close() { }) } -func WithGauge(ctx context.Context, userAgent string, f func(), measure Measure) { - ctx, err := tag.New(ctx, tag.Insert(userAgentTagKey, sanitizeTagValue(userAgent))) - if err != nil { - logging.GetGlobalContextLoggers(ctx).Errorf(`Failed to create tags: %s`, err) - } else { - for _, m := range measure.measures { - ctx, _ := tag.New(ctx, *measure.tags...) - stats.Record(ctx, m.M(1)) - defer stats.Record(ctx, m.M(-1)) - } - } - f() -} - -func WithCount(ctx context.Context, userAgent string, f func(), measure Measure) { - ctx, err := tag.New(ctx, tag.Insert(userAgentTagKey, sanitizeTagValue(userAgent))) - if err != nil { - logging.GetGlobalContextLoggers(ctx).Errorf(`Failed to create tag for user agent : %s`, err) - } else { - for _, m := range measure.measures { - ctx, _ := tag.New(ctx, *measure.tags...) - stats.Record(ctx, m.M(1)) - } - } - f() -} - -// 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) { - tagCtx, err := tag.New(ctx, tag.Insert(routeTagKey, sanitizeTagValue(route)), tag.Insert(methodTagKey, sanitizeTagValue(method))) - if err != nil { - logging.GetGlobalContextLoggers(ctx).Errorf(`Failed to create tags for route "%s %s": %s`, method, route, err) - } else { - ctx = tagCtx - } - ctx, span := trace.StartSpan(ctx, route) - defer span.End() - - WithCount(ctx, userAgent, f, measure) -} - // Pad empty keys to match tag keyset cardinality since empty strings are dropped func sanitizeTagValue(v string) string { if strings.TrimSpace(v) == "" { diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go index 8a74bed3..ab79b653 100644 --- a/internal/metrics/metrics_test.go +++ b/internal/metrics/metrics_test.go @@ -77,9 +77,9 @@ func (a privateMetricsArgs) getExpectedTags() []tag.Tag { func TestConnectionMetrics(t *testing.T) { specs := []args{ - args{platform: browser, measure: BrowserConns, userAgent: userAgentValue}, - args{platform: mobile, measure: MobileConns, userAgent: userAgentValue}, - args{platform: server, measure: ServerConns, userAgent: userAgentValue}, + args{platform: browserTagValue, measure: BrowserConns, userAgent: userAgentValue}, + args{platform: mobileTagValue, measure: MobileConns, userAgent: userAgentValue}, + args{platform: serverTagValue, measure: ServerConns, userAgent: userAgentValue}, } t.Run("public", func(t *testing.T) { @@ -124,9 +124,9 @@ func TestConnectionMetrics(t *testing.T) { func TestNewConnectionMetrics(t *testing.T) { specs := []args{ - args{platform: browser, measure: NewBrowserConns, userAgent: userAgentValue}, - args{platform: mobile, measure: NewMobileConns, userAgent: userAgentValue}, - args{platform: server, measure: NewServerConns, userAgent: userAgentValue}, + args{platform: browserTagValue, measure: NewBrowserConns, userAgent: userAgentValue}, + args{platform: mobileTagValue, measure: NewMobileConns, userAgent: userAgentValue}, + args{platform: serverTagValue, measure: NewServerConns, userAgent: userAgentValue}, } t.Run("public", func(t *testing.T) { @@ -191,7 +191,7 @@ func TestWithRouteCount(t *testing.T) { defer trace.UnregisterExporter(exporter) defer view.Unregister(requestView) - expected := routeArgs{args: args{platform: server, measure: NewServerConns, userAgent: userAgentValue}, method: "GET", route: "someRoute"} + expected := routeArgs{args: args{platform: serverTagValue, measure: NewServerConns, userAgent: userAgentValue}, method: "GET", route: "someRoute"} // Context has a relay Id, but we shouldn't get it back as a tag with public metrics ctx, _ := tag.New(context.Background(), tag.Insert(relayIdTagKey, metricsRelayId)) diff --git a/internal/metrics/prometheus.go b/internal/metrics/prometheus.go index b5944f42..12b9a6f9 100644 --- a/internal/metrics/prometheus.go +++ b/internal/metrics/prometheus.go @@ -12,7 +12,7 @@ import ( ) func init() { - defineExporter(prometheusExporter, registerPrometheusExporter) + defineExporter(prometheusExporterType, registerPrometheusExporter) } type PrometheusOptions struct { @@ -21,7 +21,7 @@ type PrometheusOptions struct { } func (p PrometheusOptions) getType() ExporterType { - return prometheusExporter + return prometheusExporterType } type PrometheusConfig config.PrometheusConfig @@ -63,5 +63,6 @@ func registerPrometheusExporter(options ExporterOptions) error { } }() view.RegisterExporter(exporter) + // Note: we are not calling trace.RegisterExporter here as we do for the other exporters return nil } diff --git a/internal/metrics/stackdriver.go b/internal/metrics/stackdriver.go index d07efe1d..dc9a42a0 100644 --- a/internal/metrics/stackdriver.go +++ b/internal/metrics/stackdriver.go @@ -9,7 +9,7 @@ import ( ) func init() { - defineExporter(stackdriverExporter, registerStackdriverExporter) + defineExporter(stackdriverExporterType, registerStackdriverExporter) } type StackdriverOptions struct { @@ -18,7 +18,7 @@ type StackdriverOptions struct { } func (d StackdriverOptions) getType() ExporterType { - return stackdriverExporter + return stackdriverExporterType } type StackdriverConfig config.StackdriverConfig