From 4adb1affdb93456f1d3075a4795158b8b6a37068 Mon Sep 17 00:00:00 2001 From: pulak-opti <129880418+pulak-opti@users.noreply.github.com> Date: Thu, 18 Jan 2024 23:38:41 +0600 Subject: [PATCH] [FSSDK-9862] Add OpenTelemetry Tracing support (#385) * add tracing option in go-sdk * add tracing for track event * update version * update otel tracer * update sdk implementation * add comment * add otel tracing * fix linter error * update otel dependency * fix typo & update license header * update go version * use constant for tracer name * add unit tests * change otel tracing flow * skip version update for FSC test * add WithTraceContext() method for OptimizelyUserContext struct * add unit tests * add one more unit test * add review changes * fix linter * remove WithTraceContext() for UserContext --- examples/benchmark/main.go | 7 +- go.mod | 7 +- go.sum | 17 ++- pkg/client/client.go | 133 ++++++++++++++++- pkg/client/client_test.go | 141 +++++++++++++++++- pkg/client/factory.go | 18 ++- pkg/client/factory_test.go | 27 +++- pkg/client/optimizely_user_context.go | 14 +- .../optimizely_user_context_odp_test.go | 5 +- pkg/client/optimizely_user_context_test.go | 7 +- pkg/tracing/opentelemetry.go | 94 ++++++++++++ 11 files changed, 435 insertions(+), 35 deletions(-) create mode 100644 pkg/tracing/opentelemetry.go diff --git a/examples/benchmark/main.go b/examples/benchmark/main.go index c27f1fa88..74cddfd1c 100644 --- a/examples/benchmark/main.go +++ b/examples/benchmark/main.go @@ -3,16 +3,15 @@ package main import ( - "io/ioutil" "log" "os" "path" + "github.com/pkg/profile" + "github.com/optimizely/go-sdk/pkg/client" "github.com/optimizely/go-sdk/pkg/decision" "github.com/optimizely/go-sdk/pkg/entities" - - "github.com/pkg/profile" ) func stressTest() { @@ -23,7 +22,7 @@ func stressTest() { var datafileDir = path.Join(os.Getenv("DATAFILES_DIR"), "100_entities.json") - datafile, err := ioutil.ReadFile(datafileDir) + datafile, err := os.ReadFile(datafileDir) if err != nil { log.Print(err) } diff --git a/go.mod b/go.mod index 8ceaaa5a6..3694117e6 100644 --- a/go.mod +++ b/go.mod @@ -8,19 +8,24 @@ require ( github.com/json-iterator/go v1.1.12 github.com/pkg/errors v0.9.1 github.com/pkg/profile v1.7.0 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 github.com/twmb/murmur3 v1.1.6 + go.opentelemetry.io/otel v1.21.0 + go.opentelemetry.io/otel/trace v1.21.0 golang.org/x/sync v0.1.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/fgprof v0.9.3 // indirect + github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a733a1c96..7527f8b7d 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,13 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= @@ -37,10 +44,16 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg= github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/pkg/client/client.go b/pkg/client/client.go index 9093e0e71..33631e6ed 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019-2023, Optimizely, Inc. and contributors * + * Copyright 2019-2024, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -18,6 +18,7 @@ package client import ( + "context" "encoding/json" "errors" "fmt" @@ -25,6 +26,8 @@ import ( "runtime/debug" "strconv" + "github.com/hashicorp/go-multierror" + "github.com/optimizely/go-sdk/pkg/config" "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision" @@ -37,13 +40,68 @@ import ( pkgOdpSegment "github.com/optimizely/go-sdk/pkg/odp/segment" pkgOdpUtils "github.com/optimizely/go-sdk/pkg/odp/utils" "github.com/optimizely/go-sdk/pkg/optimizelyjson" + "github.com/optimizely/go-sdk/pkg/tracing" "github.com/optimizely/go-sdk/pkg/utils" +) - "github.com/hashicorp/go-multierror" +const ( + // DefaultTracerName is the name of the tracer used by the Optimizely SDK + DefaultTracerName = "OptimizelySDK" + // SpanNameDecide is the name of the span used by the Optimizely SDK for tracing decide call + SpanNameDecide = "decide" + // SpanNameDecideForKeys is the name of the span used by the Optimizely SDK for tracing decideForKeys call + SpanNameDecideForKeys = "decideForKeys" + // SpanNameDecideAll is the name of the span used by the Optimizely SDK for tracing decideAll call + SpanNameDecideAll = "decideAll" + // SpanNameActivate is the name of the span used by the Optimizely SDK for tracing Activate call + SpanNameActivate = "Activate" + // SpanNameFetchQualifiedSegments is the name of the span used by the Optimizely SDK for tracing fetchQualifiedSegments call + SpanNameFetchQualifiedSegments = "fetchQualifiedSegments" + // SpanNameSendOdpEvent is the name of the span used by the Optimizely SDK for tracing SendOdpEvent call + SpanNameSendOdpEvent = "SendOdpEvent" + // SpanNameIsFeatureEnabled is the name of the span used by the Optimizely SDK for tracing IsFeatureEnabled call + SpanNameIsFeatureEnabled = "IsFeatureEnabled" + // SpanNameGetEnabledFeatures is the name of the span used by the Optimizely SDK for tracing GetEnabledFeatures call + SpanNameGetEnabledFeatures = "GetEnabledFeatures" + // SpanNameGetFeatureVariableBoolean is the name of the span used by the Optimizely SDK for tracing GetFeatureVariableBoolean call + SpanNameGetFeatureVariableBoolean = "GetFeatureVariableBoolean" + // SpanNameGetFeatureVariableDouble is the name of the span used by the Optimizely SDK for tracing GetFeatureVariableDouble call + SpanNameGetFeatureVariableDouble = "GetFeatureVariableDouble" + // SpanNameGetFeatureVariableInteger is the name of the span used by the Optimizely SDK for tracing GetFeatureVariableInteger call + SpanNameGetFeatureVariableInteger = "GetFeatureVariableInteger" + // SpanNameGetFeatureVariableString is the name of the span used by the Optimizely SDK for tracing GetFeatureVariableString call + SpanNameGetFeatureVariableString = "GetFeatureVariableString" + // SpanNameGetFeatureVariableJSON is the name of the span used by the Optimizely SDK for tracing GetFeatureVariableJSON call + SpanNameGetFeatureVariableJSON = "GetFeatureVariableJSON" + // SpanNameGetFeatureVariablePrivate is the name of the span used by the Optimizely SDK for tracing getFeatureVariable call + SpanNameGetFeatureVariablePrivate = "getFeatureVariable" + // SpanNameGetFeatureVariablePublic is the name of the span used by the Optimizely SDK for tracing GetFeatureVariable call + SpanNameGetFeatureVariablePublic = "GetFeatureVariable" + // SpanNameGetAllFeatureVariablesWithDecision is the name of the span used by the Optimizely SDK for tracing GetAllFeatureVariablesWithDecision call + SpanNameGetAllFeatureVariablesWithDecision = "GetAllFeatureVariablesWithDecision" + // SpanNameGetDetailedFeatureDecisionUnsafe is the name of the span used by the Optimizely SDK for tracing GetDetailedFeatureDecisionUnsafe call + SpanNameGetDetailedFeatureDecisionUnsafe = "GetDetailedFeatureDecisionUnsafe" + // SpanNameGetAllFeatureVariables is the name of the span used by the Optimizely SDK for tracing GetAllFeatureVariables call + SpanNameGetAllFeatureVariables = "GetAllFeatureVariables" + // SpanNameGetVariation is the name of the span used by the Optimizely SDK for tracing GetVariation call + SpanNameGetVariation = "GetVariation" + // SpanNameTrack is the name of the span used by the Optimizely SDK for tracing Track call + SpanNameTrack = "Track" + // SpanNameGetFeatureDecision is the name of the span used by the Optimizely SDK for tracing getFeatureDecision call + SpanNameGetFeatureDecision = "getFeatureDecision" + // SpanNameGetExperimentDecision is the name of the span used by the Optimizely SDK for tracing getExperimentDecision call + SpanNameGetExperimentDecision = "getExperimentDecision" + // SpanNameGetProjectConfig is the name of the span used by the Optimizely SDK for tracing getProjectConfig call + SpanNameGetProjectConfig = "getProjectConfig" + // SpanNameGetOptimizelyConfig is the name of the span used by the Optimizely SDK for tracing GetOptimizelyConfig call + SpanNameGetOptimizelyConfig = "GetOptimizelyConfig" + // SpanNameGetDecisionVariableMap is the name of the span used by the Optimizely SDK for tracing getDecisionVariableMap call + SpanNameGetDecisionVariableMap = "getDecisionVariableMap" ) // OptimizelyClient is the entry point to the Optimizely SDK type OptimizelyClient struct { + ctx context.Context ConfigManager config.ProjectConfigManager DecisionService decision.Service EventProcessor event.Processor @@ -52,6 +110,7 @@ type OptimizelyClient struct { execGroup *utils.ExecGroup logger logging.OptimizelyLogProducer defaultDecideOptions *decide.Options + tracer tracing.Tracer } // CreateUserContext creates a context of the user for which decision APIs will be called. @@ -65,6 +124,12 @@ func (o *OptimizelyClient) CreateUserContext(userID string, attributes map[strin return newOptimizelyUserContext(o, userID, attributes, nil, nil) } +// WithTraceContext sets the context for the OptimizelyClient which can be used to propagate trace information +func (o *OptimizelyClient) WithTraceContext(ctx context.Context) *OptimizelyClient { + o.ctx = ctx + return o +} + func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, options *decide.Options) OptimizelyDecision { var err error defer func() { @@ -83,6 +148,9 @@ func (o *OptimizelyClient) decide(userContext OptimizelyUserContext, key string, } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameDecide) + defer span.End() + decisionContext := decision.FeatureDecisionContext{ ForcedDecisionService: userContext.forcedDecisionService, } @@ -188,6 +256,9 @@ func (o *OptimizelyClient) decideForKeys(userContext OptimizelyUserContext, keys } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameDecideForKeys) + defer span.End() + decisionMap := map[string]OptimizelyDecision{} if _, err = o.getProjectConfig(); err != nil { o.logger.Error("Optimizely instance is not valid, failing decideForKeys call.", err) @@ -228,6 +299,9 @@ func (o *OptimizelyClient) decideAll(userContext OptimizelyUserContext, options } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameDecideAll) + defer span.End() + projectConfig, err := o.getProjectConfig() if err != nil { o.logger.Error("Optimizely instance is not valid, failing decideAll call.", err) @@ -261,6 +335,9 @@ func (o *OptimizelyClient) fetchQualifiedSegments(userContext *OptimizelyUserCon } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameFetchQualifiedSegments) + defer span.End() + // on failure, qualifiedSegments should be reset if a previous value exists. userContext.SetQualifiedSegments(nil) @@ -305,6 +382,9 @@ func (o *OptimizelyClient) SendOdpEvent(eventType, action string, identifiers ma } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameSendOdpEvent) + defer span.End() + if _, err = o.getProjectConfig(); err != nil { o.logger.Error("SendOdpEvent failed with error:", decide.GetDecideError(decide.SDKNotReady)) return err @@ -344,6 +424,9 @@ func (o *OptimizelyClient) Activate(experimentKey string, userContext entities.U } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameActivate) + defer span.End() + decisionContext, experimentDecision, err := o.getExperimentDecision(experimentKey, userContext) if err != nil { o.logger.Error("received an error while computing experiment decision", err) @@ -382,6 +465,9 @@ func (o *OptimizelyClient) IsFeatureEnabled(featureKey string, userContext entit } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameIsFeatureEnabled) + defer span.End() + decisionContext, featureDecision, err := o.getFeatureDecision(featureKey, "", userContext) if err != nil { o.logger.Error("received an error while computing feature decision", err) @@ -436,6 +522,9 @@ func (o *OptimizelyClient) GetEnabledFeatures(userContext entities.UserContext) } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetEnabledFeatures) + defer span.End() + projectConfig, err := o.getProjectConfig() if err != nil { o.logger.Error("Error retrieving ProjectConfig", err) @@ -453,6 +542,8 @@ func (o *OptimizelyClient) GetEnabledFeatures(userContext entities.UserContext) // GetFeatureVariableBoolean returns the feature variable value of type bool associated with the given feature and variable keys. func (o *OptimizelyClient) GetFeatureVariableBoolean(featureKey, variableKey string, userContext entities.UserContext) (convertedValue bool, err error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetFeatureVariableBoolean) + defer span.End() stringValue, variableType, featureDecision, err := o.getFeatureVariable(featureKey, variableKey, userContext) defer func() { @@ -486,6 +577,8 @@ func (o *OptimizelyClient) GetFeatureVariableBoolean(featureKey, variableKey str // GetFeatureVariableDouble returns the feature variable value of type double associated with the given feature and variable keys. func (o *OptimizelyClient) GetFeatureVariableDouble(featureKey, variableKey string, userContext entities.UserContext) (convertedValue float64, err error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetFeatureVariableDouble) + defer span.End() stringValue, variableType, featureDecision, err := o.getFeatureVariable(featureKey, variableKey, userContext) defer func() { @@ -519,6 +612,8 @@ func (o *OptimizelyClient) GetFeatureVariableDouble(featureKey, variableKey stri // GetFeatureVariableInteger returns the feature variable value of type int associated with the given feature and variable keys. func (o *OptimizelyClient) GetFeatureVariableInteger(featureKey, variableKey string, userContext entities.UserContext) (convertedValue int, err error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetFeatureVariableInteger) + defer span.End() stringValue, variableType, featureDecision, err := o.getFeatureVariable(featureKey, variableKey, userContext) defer func() { @@ -552,6 +647,8 @@ func (o *OptimizelyClient) GetFeatureVariableInteger(featureKey, variableKey str // GetFeatureVariableString returns the feature variable value of type string associated with the given feature and variable keys. func (o *OptimizelyClient) GetFeatureVariableString(featureKey, variableKey string, userContext entities.UserContext) (stringValue string, err error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetFeatureVariableString) + defer span.End() stringValue, variableType, featureDecision, err := o.getFeatureVariable(featureKey, variableKey, userContext) @@ -583,6 +680,8 @@ func (o *OptimizelyClient) GetFeatureVariableString(featureKey, variableKey stri // GetFeatureVariableJSON returns the feature variable value of type json associated with the given feature and variable keys. func (o *OptimizelyClient) GetFeatureVariableJSON(featureKey, variableKey string, userContext entities.UserContext) (optlyJSON *optimizelyjson.OptimizelyJSON, err error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetFeatureVariableJSON) + defer span.End() stringVal, variableType, featureDecision, err := o.getFeatureVariable(featureKey, variableKey, userContext) defer func() { @@ -620,6 +719,8 @@ func (o *OptimizelyClient) GetFeatureVariableJSON(featureKey, variableKey string // getFeatureVariable is a helper function, returns feature variable as a string along with it's associated type and feature decision func (o *OptimizelyClient) getFeatureVariable(featureKey, variableKey string, userContext entities.UserContext) (string, entities.VariableType, *decision.FeatureDecision, error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetFeatureVariablePrivate) + defer span.End() featureDecisionContext, featureDecision, err := o.getFeatureDecision(featureKey, variableKey, userContext) if err != nil { @@ -639,6 +740,8 @@ func (o *OptimizelyClient) getFeatureVariable(featureKey, variableKey string, us // GetFeatureVariable returns feature variable as a string along with it's associated type. func (o *OptimizelyClient) GetFeatureVariable(featureKey, variableKey string, userContext entities.UserContext) (string, entities.VariableType, error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetFeatureVariablePublic) + defer span.End() stringValue, variableType, featureDecision, err := o.getFeatureVariable(featureKey, variableKey, userContext) @@ -680,6 +783,8 @@ func (o *OptimizelyClient) GetFeatureVariable(featureKey, variableKey string, us // GetAllFeatureVariablesWithDecision returns all the variables for a given feature along with the enabled state. func (o *OptimizelyClient) GetAllFeatureVariablesWithDecision(featureKey string, userContext entities.UserContext) (enabled bool, variableMap map[string]interface{}, err error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetAllFeatureVariablesWithDecision) + defer span.End() variableMap = make(map[string]interface{}) decisionContext, featureDecision, err := o.getFeatureDecision(featureKey, "", userContext) @@ -730,6 +835,8 @@ func (o *OptimizelyClient) GetAllFeatureVariablesWithDecision(featureKey string, // for a given feature along with the experiment key, variation key and the enabled state. // Usage of this method is unsafe and not recommended since it can be removed in any of the next releases. func (o *OptimizelyClient) GetDetailedFeatureDecisionUnsafe(featureKey string, userContext entities.UserContext, disableTracking bool) (decisionInfo decision.UnsafeFeatureDecisionInfo, err error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetDetailedFeatureDecisionUnsafe) + defer span.End() decisionInfo = decision.UnsafeFeatureDecisionInfo{} decisionInfo.VariableMap = make(map[string]interface{}) @@ -797,6 +904,9 @@ func (o *OptimizelyClient) GetDetailedFeatureDecisionUnsafe(featureKey string, u // GetAllFeatureVariables returns all the variables as OptimizelyJSON object for a given feature. func (o *OptimizelyClient) GetAllFeatureVariables(featureKey string, userContext entities.UserContext) (optlyJSON *optimizelyjson.OptimizelyJSON, err error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetAllFeatureVariables) + defer span.End() + _, variableMap, err := o.GetAllFeatureVariablesWithDecision(featureKey, userContext) if err != nil { return optlyJSON, err @@ -824,6 +934,9 @@ func (o *OptimizelyClient) GetVariation(experimentKey string, userContext entiti } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetVariation) + defer span.End() + _, experimentDecision, err := o.getExperimentDecision(experimentKey, userContext) if err != nil { o.logger.Error("received an error while computing experiment decision", err) @@ -856,6 +969,9 @@ func (o *OptimizelyClient) Track(eventKey string, userContext entities.UserConte } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameTrack) + defer span.End() + projectConfig, e := o.getProjectConfig() if e != nil { o.logger.Error("Optimizely SDK tracking error", e) @@ -899,6 +1015,9 @@ func (o *OptimizelyClient) getFeatureDecision(featureKey, variableKey string, us } }() + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetFeatureDecision) + defer span.End() + userID := userContext.ID o.logger.Debug(fmt.Sprintf(`Evaluating feature %q for user %q.`, featureKey, userID)) @@ -937,6 +1056,8 @@ func (o *OptimizelyClient) getFeatureDecision(featureKey, variableKey string, us } func (o *OptimizelyClient) getExperimentDecision(experimentKey string, userContext entities.UserContext) (decisionContext decision.ExperimentDecisionContext, experimentDecision decision.ExperimentDecision, err error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetExperimentDecision) + defer span.End() userID := userContext.ID o.logger.Debug(fmt.Sprintf(`Evaluating experiment %q for user %q.`, experimentKey, userID)) @@ -1033,6 +1154,8 @@ func (o *OptimizelyClient) getTypedValue(value string, variableType entities.Var } func (o *OptimizelyClient) getProjectConfig() (projectConfig config.ProjectConfig, err error) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetProjectConfig) + defer span.End() if isNil(o.ConfigManager) { return nil, errors.New("project config manager is not initialized") @@ -1057,7 +1180,8 @@ func (o *OptimizelyClient) getAllOptions(options *decide.Options) decide.Options // GetOptimizelyConfig returns OptimizelyConfig object func (o *OptimizelyClient) GetOptimizelyConfig() (optimizelyConfig *config.OptimizelyConfig) { - + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetOptimizelyConfig) + defer span.End() return o.ConfigManager.GetOptimizelyConfig() } @@ -1072,6 +1196,9 @@ func (o *OptimizelyClient) Close() { } func (o *OptimizelyClient) getDecisionVariableMap(feature entities.Feature, variation *entities.Variation, featureEnabled bool) (map[string]interface{}, decide.DecisionReasons) { + _, span := o.tracer.StartSpan(o.ctx, DefaultTracerName, SpanNameGetDecisionVariableMap) + defer span.End() + reasons := decide.NewDecisionReasons(nil) valuesMap := map[string]interface{}{} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index adade6de9..eb9a40f51 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019-2020,2022-2023 Optimizely, Inc. and contributors * + * Copyright 2019-2020,2022-2024 Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -25,6 +25,10 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "github.com/optimizely/go-sdk/pkg/config" "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision" @@ -35,11 +39,8 @@ import ( "github.com/optimizely/go-sdk/pkg/odp" "github.com/optimizely/go-sdk/pkg/odp/segment" pkgOdpUtils "github.com/optimizely/go-sdk/pkg/odp/utils" + "github.com/optimizely/go-sdk/pkg/tracing" "github.com/optimizely/go-sdk/pkg/utils" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" ) func ValidProjectConfigManager() *MockProjectConfigManager { @@ -205,6 +206,28 @@ func (m *MockODPManager) Update(apiKey, apiHost string, segmentsToCheck []string m.Called(apiKey, apiHost, segmentsToCheck) } +type MockTracer struct { + StartSpanCalled bool + TracerName string + CalledSpans []string +} + +func (m *MockTracer) StartSpan(ctx context.Context, tracerName, spanName string) (context.Context, tracing.Span) { + m.StartSpanCalled = true + m.TracerName = tracerName + if m.CalledSpans == nil { + m.CalledSpans = make([]string, 0) + } + m.CalledSpans = append(m.CalledSpans, spanName) + return ctx, &MockSpan{} +} + +type MockSpan struct{} + +func (m *MockSpan) SetAttibutes(key string, value interface{}) {} + +func (m *MockSpan) End() {} + func TestSendODPEventWhenSDKNotReady(t *testing.T) { factory := OptimizelyFactory{SDKKey: "121"} client, _ := factory.Client() @@ -259,10 +282,14 @@ func TestSendODPEventEmptyType(t *testing.T) { optimizelyClient := OptimizelyClient{ OdpManager: mockOdpManager, ConfigManager: getMockConfigManager(), + tracer: &MockTracer{}, } err := optimizelyClient.SendOdpEvent("", action, identifiers, data) assert.NoError(t, err) mockOdpManager.AssertExpectations(t) + assert.True(t, optimizelyClient.tracer.(*MockTracer).StartSpanCalled) + assert.Equal(t, DefaultTracerName, optimizelyClient.tracer.(*MockTracer).TracerName) + assert.Contains(t, optimizelyClient.tracer.(*MockTracer).CalledSpans, SpanNameSendOdpEvent) } func TestSendODPEventEmptyIdentifiers(t *testing.T) { @@ -278,9 +305,11 @@ func TestSendODPEventEmptyIdentifiers(t *testing.T) { optimizelyClient := OptimizelyClient{ logger: logging.GetLogger("", ""), ConfigManager: getMockConfigManager(), + tracer: &MockTracer{}, } err := optimizelyClient.SendOdpEvent("", action, identifiers, data) assert.Equal(t, errors.New("ODP events must have at least one key-value pair in identifiers"), err) + assert.True(t, optimizelyClient.tracer.(*MockTracer).StartSpanCalled) } func TestSendODPEventNilIdentifiers(t *testing.T) { @@ -295,9 +324,11 @@ func TestSendODPEventNilIdentifiers(t *testing.T) { optimizelyClient := OptimizelyClient{ logger: logging.GetLogger("", ""), ConfigManager: getMockConfigManager(), + tracer: &MockTracer{}, } err := optimizelyClient.SendOdpEvent("", action, nil, data) assert.Equal(t, errors.New("ODP events must have at least one key-value pair in identifiers"), err) + assert.True(t, optimizelyClient.tracer.(*MockTracer).StartSpanCalled) } func TestSendODPEvent(t *testing.T) { @@ -306,10 +337,12 @@ func TestSendODPEvent(t *testing.T) { optimizelyClient := OptimizelyClient{ OdpManager: mockOdpManager, ConfigManager: getMockConfigManager(), + tracer: &MockTracer{}, } err := optimizelyClient.SendOdpEvent("123", "", map[string]string{"identifier": "123"}, nil) assert.NoError(t, err) mockOdpManager.AssertExpectations(t) + assert.True(t, optimizelyClient.tracer.(*MockTracer).StartSpanCalled) } func TestTrack(t *testing.T) { @@ -322,6 +355,7 @@ func TestTrack(t *testing.T) { DecisionService: mockDecisionService, EventProcessor: mockProcessor, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } err := client.Track("sample_conversion", entities.UserContext{ID: "1212121", Attributes: map[string]interface{}{}}, map[string]interface{}{}) @@ -330,7 +364,7 @@ func TestTrack(t *testing.T) { assert.True(t, len(mockProcessor.Events) == 1) assert.True(t, mockProcessor.Events[0].VisitorID == "1212121") assert.True(t, mockProcessor.Events[0].EventContext.ProjectID == "15389410617") - + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestTrackFailEventNotFound(t *testing.T) { @@ -342,13 +376,14 @@ func TestTrackFailEventNotFound(t *testing.T) { DecisionService: mockDecisionService, EventProcessor: mockProcessor, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } err := client.Track("bob", entities.UserContext{ID: "1212121", Attributes: map[string]interface{}{}}, map[string]interface{}{}) assert.NoError(t, err) assert.True(t, len(mockProcessor.Events) == 0) - + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestTrackPanics(t *testing.T) { @@ -360,13 +395,14 @@ func TestTrackPanics(t *testing.T) { DecisionService: mockDecisionService, EventProcessor: mockProcessor, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } err := client.Track("bob", entities.UserContext{ID: "1212121", Attributes: map[string]interface{}{}}, map[string]interface{}{}) assert.Error(t, err) assert.True(t, len(mockProcessor.Events) == 0) - + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetEnabledFeaturesPanic(t *testing.T) { @@ -377,12 +413,14 @@ func TestGetEnabledFeaturesPanic(t *testing.T) { ConfigManager: &PanickingConfigManager{}, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } // ensure that the client calms back down and recovers result, err := client.GetEnabledFeatures(testUserContext) assert.Empty(t, result) assert.True(t, assert.Error(t, err)) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetFeatureVariableBool(t *testing.T) { @@ -448,6 +486,7 @@ func TestGetFeatureVariableBool(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } result, err := client.GetFeatureVariableBoolean(testFeatureKey, testVariableKey, testUserContext) if ts.validBool { @@ -461,6 +500,7 @@ func TestGetFeatureVariableBool(t *testing.T) { mockConfig.AssertExpectations(t) mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } } @@ -528,6 +568,7 @@ func TestGetFeatureVariableBoolWithNotification(t *testing.T) { DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), notificationCenter: notificationCenter, + tracer: &MockTracer{}, } var numberOfCalls = 0 note := notification.DecisionNotification{} @@ -548,6 +589,7 @@ func TestGetFeatureVariableBoolWithNotification(t *testing.T) { mockConfig.AssertExpectations(t) mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } } @@ -562,12 +604,14 @@ func TestGetFeatureVariableBoolPanic(t *testing.T) { ConfigManager: &PanickingConfigManager{}, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } // ensure that the client calms back down and recovers result, err := client.GetFeatureVariableBoolean(testFeatureKey, testVariableKey, testUserContext) assert.Equal(t, false, result) assert.True(t, assert.Error(t, err)) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetFeatureVariableDouble(t *testing.T) { @@ -633,6 +677,7 @@ func TestGetFeatureVariableDouble(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } result, err := client.GetFeatureVariableDouble(testFeatureKey, testVariableKey, testUserContext) if ts.validDouble { @@ -646,6 +691,7 @@ func TestGetFeatureVariableDouble(t *testing.T) { mockConfig.AssertExpectations(t) mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } } @@ -713,6 +759,7 @@ func TestGetFeatureVariableDoubleWithNotification(t *testing.T) { DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), notificationCenter: notificationCenter, + tracer: &MockTracer{}, } var numberOfCalls = 0 note := notification.DecisionNotification{} @@ -733,6 +780,7 @@ func TestGetFeatureVariableDoubleWithNotification(t *testing.T) { mockConfig.AssertExpectations(t) mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } } @@ -747,12 +795,14 @@ func TestGetFeatureVariableDoublePanic(t *testing.T) { ConfigManager: &PanickingConfigManager{}, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } // ensure that the client calms back down and recovers result, err := client.GetFeatureVariableDouble(testFeatureKey, testVariableKey, testUserContext) assert.Equal(t, float64(0), result) assert.True(t, assert.Error(t, err)) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetFeatureVariableInteger(t *testing.T) { @@ -818,6 +868,7 @@ func TestGetFeatureVariableInteger(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } result, err := client.GetFeatureVariableInteger(testFeatureKey, testVariableKey, testUserContext) if ts.validInteger { @@ -831,6 +882,7 @@ func TestGetFeatureVariableInteger(t *testing.T) { mockConfig.AssertExpectations(t) mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } } @@ -898,6 +950,7 @@ func TestGetFeatureVariableIntegerWithNotification(t *testing.T) { DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), notificationCenter: notificationCenter, + tracer: &MockTracer{}, } var numberOfCalls = 0 note := notification.DecisionNotification{} @@ -918,6 +971,7 @@ func TestGetFeatureVariableIntegerWithNotification(t *testing.T) { mockConfig.AssertExpectations(t) mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } } @@ -932,12 +986,14 @@ func TestGetFeatureVariableIntegerPanic(t *testing.T) { ConfigManager: &PanickingConfigManager{}, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } // ensure that the client calms back down and recovers result, err := client.GetFeatureVariableInteger(testFeatureKey, testVariableKey, testUserContext) assert.Equal(t, 0, result) assert.True(t, assert.Error(t, err)) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetFeatureVariableSting(t *testing.T) { @@ -1001,6 +1057,7 @@ func TestGetFeatureVariableSting(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } result, err := client.GetFeatureVariableString(testFeatureKey, testVariableKey, testUserContext) if ts.validString { @@ -1014,6 +1071,7 @@ func TestGetFeatureVariableSting(t *testing.T) { mockConfig.AssertExpectations(t) mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } } @@ -1079,6 +1137,7 @@ func TestGetFeatureVariableStringWithNotification(t *testing.T) { DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), notificationCenter: notificationCenter, + tracer: &MockTracer{}, } var numberOfCalls = 0 note := notification.DecisionNotification{} @@ -1099,6 +1158,7 @@ func TestGetFeatureVariableStringWithNotification(t *testing.T) { mockConfig.AssertExpectations(t) mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } } func TestGetFeatureVariableStringPanic(t *testing.T) { @@ -1112,12 +1172,14 @@ func TestGetFeatureVariableStringPanic(t *testing.T) { ConfigManager: &PanickingConfigManager{}, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } // ensure that the client calms back down and recovers result, err := client.GetFeatureVariableString(testFeatureKey, testVariableKey, testUserContext) assert.Equal(t, "", result) assert.True(t, assert.Error(t, err)) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetFeatureVariableJSON(t *testing.T) { @@ -1184,6 +1246,7 @@ func TestGetFeatureVariableJSON(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } result, err := client.GetFeatureVariableJSON(testFeatureKey, testVariableKey, testUserContext) if ts.validJson { @@ -1203,6 +1266,7 @@ func TestGetFeatureVariableJSON(t *testing.T) { mockConfig.AssertExpectations(t) mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } } @@ -1270,6 +1334,7 @@ func TestGetFeatureVariableJSONWithNotification(t *testing.T) { DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), notificationCenter: notificationCenter, + tracer: &MockTracer{}, } var numberOfCalls = 0 note := notification.DecisionNotification{} @@ -1290,6 +1355,7 @@ func TestGetFeatureVariableJSONWithNotification(t *testing.T) { mockConfig.AssertExpectations(t) mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } } func TestGetFeatureVariableJSONPanic(t *testing.T) { @@ -1303,12 +1369,14 @@ func TestGetFeatureVariableJSONPanic(t *testing.T) { ConfigManager: &PanickingConfigManager{}, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } // ensure that the client calms back down and recovers result, err := client.GetFeatureVariableJSON(testFeatureKey, testVariableKey, testUserContext) assert.Nil(t, result) assert.True(t, assert.Error(t, err)) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetFeatureVariableErrorCases(t *testing.T) { @@ -1322,6 +1390,7 @@ func TestGetFeatureVariableErrorCases(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } _, err1 := client.GetFeatureVariableBoolean("test_feature_key", "test_variable_key", testUserContext) _, err2 := client.GetFeatureVariableDouble("test_feature_key", "test_variable_key", testUserContext) @@ -1336,6 +1405,7 @@ func TestGetFeatureVariableErrorCases(t *testing.T) { mockConfigManager.AssertNotCalled(t, "GetFeatureByKey") mockConfigManager.AssertNotCalled(t, "GetVariableByKey") mockDecisionService.AssertNotCalled(t, "GetFeatureDecision") + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetProjectConfigIsValid(t *testing.T) { @@ -1344,12 +1414,14 @@ func TestGetProjectConfigIsValid(t *testing.T) { client := OptimizelyClient{ ConfigManager: mockConfigManager, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } actual, err := client.getProjectConfig() assert.Nil(t, err) assert.Equal(t, mockConfigManager.projectConfig, actual) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetProjectConfigIsInValid(t *testing.T) { @@ -1357,12 +1429,14 @@ func TestGetProjectConfigIsInValid(t *testing.T) { client := OptimizelyClient{ ConfigManager: InValidProjectConfigManager(), logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } actual, err := client.getProjectConfig() assert.NotNil(t, err) assert.Nil(t, actual) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetOptimizelyConfig(t *testing.T) { @@ -1371,17 +1445,20 @@ func TestGetOptimizelyConfig(t *testing.T) { client := OptimizelyClient{ ConfigManager: mockConfigManager, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } optimizelyConfig := client.GetOptimizelyConfig() assert.Equal(t, &config.OptimizelyConfig{Revision: "232"}, optimizelyConfig) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetNotificationCenter(t *testing.T) { nc := &MockNotificationCenter{} client := OptimizelyClient{ notificationCenter: nc, + tracer: &MockTracer{}, } assert.Equal(t, client.GetNotificationCenter(), nc) @@ -1426,11 +1503,13 @@ func TestGetFeatureDecisionValid(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } _, featureDecision, err := client.getFeatureDecision(testFeatureKey, testVariableKey, testUserContext) assert.Nil(t, err) assert.Equal(t, expectedFeatureDecision, featureDecision) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetFeatureDecisionErrProjectConfig(t *testing.T) { @@ -1472,10 +1551,12 @@ func TestGetFeatureDecisionErrProjectConfig(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } _, _, err := client.getFeatureDecision(testFeatureKey, testVariableKey, testUserContext) assert.Error(t, err) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetFeatureDecisionPanicProjectConfig(t *testing.T) { @@ -1516,10 +1597,12 @@ func TestGetFeatureDecisionPanicProjectConfig(t *testing.T) { ConfigManager: &PanickingConfigManager{}, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } _, _, err := client.getFeatureDecision(testFeatureKey, testVariableKey, testUserContext) assert.Error(t, err) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetFeatureDecisionPanicDecisionService(t *testing.T) { @@ -1551,11 +1634,13 @@ func TestGetFeatureDecisionPanicDecisionService(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: &PanickingDecisionService{}, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } _, _, err := client.getFeatureDecision(testFeatureKey, testVariableKey, testUserContext) assert.Error(t, err) assert.EqualError(t, err, "I'm panicking") + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetFeatureDecisionErrFeatureDecision(t *testing.T) { @@ -1597,11 +1682,13 @@ func TestGetFeatureDecisionErrFeatureDecision(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } _, decision, err := client.getFeatureDecision(testFeatureKey, testVariableKey, testUserContext) assert.Equal(t, expectedFeatureDecision, decision) assert.NoError(t, err) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetAllFeatureVariablesWithDecision(t *testing.T) { @@ -1652,6 +1739,7 @@ func TestGetAllFeatureVariablesWithDecision(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } enabled, variationMap, err := client.GetAllFeatureVariablesWithDecision(testFeatureKey, testUserContext) @@ -1661,6 +1749,7 @@ func TestGetAllFeatureVariablesWithDecision(t *testing.T) { for _, v := range variables { assert.Equal(t, v.expected, variationMap[v.key]) } + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetAllFeatureVariablesWithDecisionWithNotification(t *testing.T) { @@ -1711,6 +1800,7 @@ func TestGetAllFeatureVariablesWithDecisionWithNotification(t *testing.T) { DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), notificationCenter: notificationCenter, + tracer: &MockTracer{}, } var numberOfCalls = 0 note := notification.DecisionNotification{} @@ -1730,6 +1820,7 @@ func TestGetAllFeatureVariablesWithDecisionWithNotification(t *testing.T) { "var_json": map[string]interface{}{"field1": 12.0, "field2": "some_value"}, "var_str": "var"}}} assert.Equal(t, numberOfCalls, 1) assert.Equal(t, decisionInfo, note.DecisionInfo) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetAllFeatureVariablesWithDecisionWithError(t *testing.T) { @@ -1771,6 +1862,7 @@ func TestGetAllFeatureVariablesWithDecisionWithError(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } enabled, variationMap, err := client.GetAllFeatureVariablesWithDecision(testFeatureKey, testUserContext) @@ -1779,6 +1871,7 @@ func TestGetAllFeatureVariablesWithDecisionWithError(t *testing.T) { assert.True(t, enabled) assert.Equal(t, testVariableValue, variationMap[testVariableKey]) assert.NoError(t, err) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetAllFeatureVariablesWithDecisionWithoutFeature(t *testing.T) { @@ -1795,6 +1888,7 @@ func TestGetAllFeatureVariablesWithDecisionWithoutFeature(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } enabled, variationMap, err := client.GetAllFeatureVariablesWithDecision(invalidFeatureKey, testUserContext) @@ -1803,6 +1897,7 @@ func TestGetAllFeatureVariablesWithDecisionWithoutFeature(t *testing.T) { assert.False(t, enabled) assert.Equal(t, 0, len(variationMap)) assert.NoError(t, err) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetDetailedFeatureDecisionUnsafeWithNotification(t *testing.T) { @@ -1853,6 +1948,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithNotification(t *testing.T) { DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), notificationCenter: notificationCenter, + tracer: &MockTracer{}, } var numberOfCalls = 0 note := notification.DecisionNotification{} @@ -1872,6 +1968,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithNotification(t *testing.T) { "var_json": map[string]interface{}{"field1": 12.0, "field2": "some_value"}, "var_str": "var"}}} assert.Equal(t, numberOfCalls, 1) assert.Equal(t, decisionInfo, note.DecisionInfo) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetDetailedFeatureDecisionUnsafeWithTrackingDisabled(t *testing.T) { @@ -1922,6 +2019,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithTrackingDisabled(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } decision, err := client.GetDetailedFeatureDecisionUnsafe(testFeatureKey, testUserContext, true) @@ -1933,6 +2031,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithTrackingDisabled(t *testing.T) { } assert.Equal(t, decision.ExperimentKey, "") assert.Equal(t, decision.VariationKey, "") + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetDetailedFeatureDecisionUnsafeWithoutFeature(t *testing.T) { @@ -1949,6 +2048,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithoutFeature(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } decision, err := client.GetDetailedFeatureDecisionUnsafe(invalidFeatureKey, testUserContext, true) @@ -1957,6 +2057,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithoutFeature(t *testing.T) { assert.False(t, decision.Enabled) assert.Equal(t, 0, len(decision.VariableMap)) assert.NoError(t, err) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetDetailedFeatureDecisionUnsafeWithError(t *testing.T) { @@ -1982,11 +2083,13 @@ func TestGetDetailedFeatureDecisionUnsafeWithError(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } decision, err := client.GetDetailedFeatureDecisionUnsafe(testFeatureKey, testUserContext, true) assert.False(t, decision.Enabled) assert.Error(t, err) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetDetailedFeatureDecisionUnsafeWithFeatureTestAndTrackingEnabled(t *testing.T) { @@ -2023,6 +2126,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithFeatureTestAndTrackingEnabled(t *te DecisionService: mockDecisionService, EventProcessor: mockEventProcessor, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } decision, err := client.GetDetailedFeatureDecisionUnsafe(testFeature.Key, testUserContext, false) @@ -2035,6 +2139,7 @@ func TestGetDetailedFeatureDecisionUnsafeWithFeatureTestAndTrackingEnabled(t *te mockConfigManager.AssertExpectations(t) mockDecisionService.AssertExpectations(t) mockEventProcessor.AssertExpectations(t) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetAllFeatureVariables(t *testing.T) { @@ -2083,6 +2188,7 @@ func TestGetAllFeatureVariables(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } optlyJSON, err := client.GetAllFeatureVariables(testFeatureKey, testUserContext) @@ -2100,6 +2206,7 @@ func TestGetAllFeatureVariables(t *testing.T) { assert.Equal(t, 12.0, jsonVarMap["field1"]) assert.Equal(t, "some_value", jsonVarMap["field2"]) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } func TestGetAllFeatureVariablesWithoutFeature(t *testing.T) { @@ -2116,6 +2223,7 @@ func TestGetAllFeatureVariablesWithoutFeature(t *testing.T) { ConfigManager: mockConfigManager, DecisionService: mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } optlyJson, err := client.GetAllFeatureVariables(invalidFeatureKey, testUserContext) @@ -2128,6 +2236,7 @@ func TestGetAllFeatureVariablesWithoutFeature(t *testing.T) { variationString, err := optlyJson.ToString() assert.Equal(t, "{}", variationString) + assert.True(t, client.tracer.(*MockTracer).StartSpanCalled) } // Helper Methods @@ -2208,6 +2317,7 @@ func (s *ClientTestSuiteAB) TestActivate() { DecisionService: s.mockDecisionService, EventProcessor: s.mockEventProcessor, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } variationKey1, err1 := testClient.Activate("test_exp_1", testUserContext) @@ -2231,6 +2341,7 @@ func (s *ClientTestSuiteAB) TestActivatePanics() { ConfigManager: new(PanickingConfigManager), DecisionService: s.mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } variationKey, err := testClient.Activate("test_exp_1", testUserContext) @@ -2247,6 +2358,7 @@ func (s *ClientTestSuiteAB) TestActivateInvalidConfig() { testClient := OptimizelyClient{ ConfigManager: mockConfigManager, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } variationKey, err := testClient.Activate("test_exp_1", testUserContext) @@ -2275,6 +2387,7 @@ func (s *ClientTestSuiteAB) TestGetVariation() { ConfigManager: s.mockConfigManager, DecisionService: s.mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } variationKey, err := testClient.GetVariation("test_exp_1", testUserContext) @@ -2305,6 +2418,7 @@ func (s *ClientTestSuiteAB) TestGetVariationWithDecisionError() { ConfigManager: s.mockConfigManager, DecisionService: s.mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } variationKey, err := testClient.GetVariation("test_exp_1", testUserContext) @@ -2322,6 +2436,7 @@ func (s *ClientTestSuiteAB) TestGetVariationPanics() { ConfigManager: new(PanickingConfigManager), DecisionService: s.mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } variationKey, err := testClient.GetVariation("test_exp_1", testUserContext) @@ -2374,6 +2489,7 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabled() { DecisionService: s.mockDecisionService, EventProcessor: s.mockEventProcessor, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } result, _ := client.IsFeatureEnabled(testFeature.Key, testUserContext) s.True(result) @@ -2412,6 +2528,7 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabledWithNotification() { DecisionService: s.mockDecisionService, logger: logging.GetLogger("", ""), notificationCenter: notificationCenter, + tracer: &MockTracer{}, } var numberOfCalls = 0 note := notification.DecisionNotification{} @@ -2466,6 +2583,7 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabledWithDecisionError() { DecisionService: s.mockDecisionService, EventProcessor: s.mockEventProcessor, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } // should still return the decision because the error is non-fatal @@ -2488,6 +2606,7 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabledErrorConfig() { ConfigManager: s.mockConfigManager, DecisionService: s.mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } result, _ := client.IsFeatureEnabled(testFeatureKey, testUserContext) s.False(result) @@ -2511,6 +2630,7 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabledErrorFeatureKey() { ConfigManager: s.mockConfigManager, DecisionService: s.mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } result, err := client.IsFeatureEnabled(testFeatureKey, testUserContext) s.NoError(err) @@ -2526,6 +2646,7 @@ func (s *ClientTestSuiteFM) TestIsFeatureEnabledPanic() { client := OptimizelyClient{ ConfigManager: &PanickingConfigManager{}, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } // ensure that the client calms back down and recovers @@ -2574,6 +2695,7 @@ func (s *ClientTestSuiteFM) TestGetEnabledFeatures() { ConfigManager: s.mockConfigManager, DecisionService: s.mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } result, err := client.GetEnabledFeatures(testUserContext) s.NoError(err) @@ -2595,6 +2717,7 @@ func (s *ClientTestSuiteFM) TestGetEnabledFeaturesErrorCases() { ConfigManager: mockConfigManager, DecisionService: s.mockDecisionService, logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } result, err := client.GetEnabledFeatures(testUserContext) s.Error(err) @@ -2730,6 +2853,7 @@ func (s *ClientTestSuiteTrackEvent) SetupTest() { EventProcessor: s.mockProcessor, notificationCenter: notification.NewNotificationCenter(), logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } } @@ -2909,6 +3033,7 @@ func (s *ClientTestSuiteTrackNotification) SetupTest() { EventProcessor: s.mockProcessor, notificationCenter: notification.NewNotificationCenter(), logger: logging.GetLogger("", ""), + tracer: &MockTracer{}, } } diff --git a/pkg/client/factory.go b/pkg/client/factory.go index b6c7aef13..1f28e10d9 100644 --- a/pkg/client/factory.go +++ b/pkg/client/factory.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019-2020,2022-2023 Optimizely, Inc. and contributors * + * Copyright 2019-2020,2022-2024 Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -32,6 +32,7 @@ import ( "github.com/optimizely/go-sdk/pkg/odp" pkgUtils "github.com/optimizely/go-sdk/pkg/odp/utils" "github.com/optimizely/go-sdk/pkg/registry" + "github.com/optimizely/go-sdk/pkg/tracing" "github.com/optimizely/go-sdk/pkg/utils" ) @@ -48,6 +49,7 @@ type OptimizelyFactory struct { eventDispatcher event.Dispatcher eventProcessor event.Processor metricsRegistry metrics.Registry + tracer tracing.Tracer overrideStore decision.ExperimentOverrideStore userProfileService decision.UserProfileService notificationCenter notification.Center @@ -105,6 +107,7 @@ func (f *OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClie defaultDecideOptions: decideOptions, execGroup: eg, logger: logging.GetLogger(f.SDKKey, "OptimizelyClient"), + ctx: ctx, } if f.notificationCenter != nil { @@ -113,6 +116,12 @@ func (f *OptimizelyFactory) Client(clientOptions ...OptionFunc) (*OptimizelyClie appClient.notificationCenter = registry.GetNotificationCenter(f.SDKKey) } + if f.tracer != nil { + appClient.tracer = f.tracer + } else { + appClient.tracer = &tracing.NoopTracer{} + } + if f.configManager != nil { appClient.ConfigManager = f.configManager } else { @@ -300,6 +309,13 @@ func WithNotificationCenter(nc notification.Center) OptionFunc { } } +// WithTracer allows user to pass in their own implementation of the Tracer interface +func WithTracer(tracer tracing.Tracer) OptionFunc { + return func(f *OptimizelyFactory) { + f.tracer = tracer + } +} + // StaticClient returns a client initialized with a static project config. func (f *OptimizelyFactory) StaticClient() (optlyClient *OptimizelyClient, err error) { diff --git a/pkg/client/factory_test.go b/pkg/client/factory_test.go index 97b770d87..dd6765519 100644 --- a/pkg/client/factory_test.go +++ b/pkg/client/factory_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2019-2020,2022 Optimizely, Inc. and contributors * + * Copyright 2019-2020,2022,2024 Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -24,6 +24,9 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/optimizely/go-sdk/pkg/config" "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision" @@ -36,10 +39,8 @@ import ( pkgOdpSegment "github.com/optimizely/go-sdk/pkg/odp/segment" pkgOdpUtils "github.com/optimizely/go-sdk/pkg/odp/utils" "github.com/optimizely/go-sdk/pkg/registry" + "github.com/optimizely/go-sdk/pkg/tracing" "github.com/optimizely/go-sdk/pkg/utils" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ) type MockRequester struct { @@ -366,3 +367,21 @@ func TestClientWithDefaultDecideOptions(t *testing.T) { assert.NoError(t, err) assert.Equal(t, &decide.Options{}, optimizelyClient.defaultDecideOptions) } + +func TestOptimizelyClientWithTracer(t *testing.T) { + factory := OptimizelyFactory{SDKKey: "1212"} + optimizelyClient, err := factory.Client(WithTracer(&MockTracer{})) + assert.NoError(t, err) + assert.NotNil(t, optimizelyClient.tracer) + tracer := optimizelyClient.tracer.(*MockTracer) + assert.NotNil(t, tracer) +} + +func TestOptimizelyClientWithNoTracer(t *testing.T) { + factory := OptimizelyFactory{SDKKey: "1212"} + optimizelyClient, err := factory.Client() + assert.NoError(t, err) + assert.NotNil(t, optimizelyClient.tracer) + tracer := optimizelyClient.tracer.(*tracing.NoopTracer) + assert.NotNil(t, tracer) +} diff --git a/pkg/client/optimizely_user_context.go b/pkg/client/optimizely_user_context.go index 297ef6383..05cedbce1 100644 --- a/pkg/client/optimizely_user_context.go +++ b/pkg/client/optimizely_user_context.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2020-2022, Optimizely, Inc. and contributors * + * Copyright 2020-2022, 2024 Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -159,27 +159,27 @@ func (o *OptimizelyUserContext) TrackEvent(eventKey string, eventTags map[string // SetForcedDecision sets the forced decision (variation key) for a given decision context (flag key and optional rule key). // returns true if the forced decision has been set successfully. -func (o *OptimizelyUserContext) SetForcedDecision(context pkgDecision.OptimizelyDecisionContext, decision pkgDecision.OptimizelyForcedDecision) bool { +func (o *OptimizelyUserContext) SetForcedDecision(ctx pkgDecision.OptimizelyDecisionContext, decision pkgDecision.OptimizelyForcedDecision) bool { if o.forcedDecisionService == nil { o.forcedDecisionService = pkgDecision.NewForcedDecisionService(o.GetUserID()) } - return o.forcedDecisionService.SetForcedDecision(context, decision) + return o.forcedDecisionService.SetForcedDecision(ctx, decision) } // GetForcedDecision returns the forced decision for a given flag and an optional rule -func (o *OptimizelyUserContext) GetForcedDecision(context pkgDecision.OptimizelyDecisionContext) (pkgDecision.OptimizelyForcedDecision, error) { +func (o *OptimizelyUserContext) GetForcedDecision(ctx pkgDecision.OptimizelyDecisionContext) (pkgDecision.OptimizelyForcedDecision, error) { if o.forcedDecisionService == nil { return pkgDecision.OptimizelyForcedDecision{}, errors.New("decision not found") } - return o.forcedDecisionService.GetForcedDecision(context) + return o.forcedDecisionService.GetForcedDecision(ctx) } // RemoveForcedDecision removes the forced decision for a given flag and an optional rule. -func (o *OptimizelyUserContext) RemoveForcedDecision(context pkgDecision.OptimizelyDecisionContext) bool { +func (o *OptimizelyUserContext) RemoveForcedDecision(ctx pkgDecision.OptimizelyDecisionContext) bool { if o.forcedDecisionService == nil { return false } - return o.forcedDecisionService.RemoveForcedDecision(context) + return o.forcedDecisionService.RemoveForcedDecision(ctx) } // RemoveAllForcedDecisions removes all forced decisions bound to this user context. diff --git a/pkg/client/optimizely_user_context_odp_test.go b/pkg/client/optimizely_user_context_odp_test.go index f3b811932..522831183 100644 --- a/pkg/client/optimizely_user_context_odp_test.go +++ b/pkg/client/optimizely_user_context_odp_test.go @@ -23,13 +23,14 @@ import ( "sync" "testing" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "github.com/optimizely/go-sdk/pkg/config/datafileprojectconfig" "github.com/optimizely/go-sdk/pkg/logging" "github.com/optimizely/go-sdk/pkg/odp" "github.com/optimizely/go-sdk/pkg/odp/event" "github.com/optimizely/go-sdk/pkg/odp/segment" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" ) type OptimizelyUserContextODPTestSuite struct { diff --git a/pkg/client/optimizely_user_context_test.go b/pkg/client/optimizely_user_context_test.go index 7c8d7961c..ed76c4fc0 100644 --- a/pkg/client/optimizely_user_context_test.go +++ b/pkg/client/optimizely_user_context_test.go @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2020-2022, Optimizely, Inc. and contributors * + * Copyright 2020-2022, 2024 Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -22,14 +22,15 @@ import ( "sync" "testing" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" + "github.com/optimizely/go-sdk/pkg/decide" "github.com/optimizely/go-sdk/pkg/decision" "github.com/optimizely/go-sdk/pkg/entities" "github.com/optimizely/go-sdk/pkg/event" "github.com/optimizely/go-sdk/pkg/notification" "github.com/optimizely/go-sdk/pkg/optimizelyjson" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" ) var doOnce sync.Once // required since we only need to read datafile once diff --git a/pkg/tracing/opentelemetry.go b/pkg/tracing/opentelemetry.go new file mode 100644 index 000000000..0e988448c --- /dev/null +++ b/pkg/tracing/opentelemetry.go @@ -0,0 +1,94 @@ +/**************************************************************************** + * Copyright 2024 Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +// Package tracing // +package tracing + +import ( + "context" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// Tracer provides the necessary method to collect telemetry trace data. +// Tracer should not depend on any specific tool. To make it possible it returns a Span interface. +type Tracer interface { + // StartSpan starts a trace span. Span can be a parent or child span based on the passed context. + StartSpan(ctx context.Context, tracerName, spanName string) (context.Context, Span) +} + +// Span interface implements the trace span returned by Tracer. +type Span interface { + End() + SetAttibutes(key string, value interface{}) +} + +// otelTracer is an OpenTelemetry implementation of Tracer +type otelTracer struct { + enabled bool +} + +// NewOtelTracer returns a new instance of Tracer +func NewOtelTracer(t trace.Tracer) Tracer { + return &otelTracer{ + enabled: true, + } +} + +// StartSpan starts a trace span. Span can be a parent or child span based on the passed context. +func (t *otelTracer) StartSpan(pctx context.Context, tracerName, spanName string) (context.Context, Span) { + ctx, span := otel.Tracer(tracerName).Start(pctx, spanName) + return ctx, &otelSpan{ + span: span, + } +} + +// otelSpan is an OpenTelemetry Span implementation of Span +type otelSpan struct { + span trace.Span +} + +// SetAttibutes sets the attributes for the span +func (s *otelSpan) SetAttibutes(key string, value interface{}) { + s.span.SetAttributes(attribute.KeyValue{ + Key: attribute.Key(key), + Value: attribute.StringValue(value.(string)), + }) +} + +// End ends the span +func (s *otelSpan) End() { + s.span.End() +} + +// NoopTracer is a no-op implementation of Tracer +type NoopTracer struct{} + +// StartSpan returns a new instance of NoopTracer +func (t *NoopTracer) StartSpan(ctx context.Context, tracerName, spanName string) (context.Context, Span) { + return ctx, &NoopSpan{} +} + +// NoopSpan is a no-op implementation of Span +type NoopSpan struct{} + +// SetAttibutes sets the attributes for the noop-span +func (s *NoopSpan) SetAttibutes(key string, value interface{}) {} + +// End ends the noop-span +func (s *NoopSpan) End() {}