diff --git a/agent/agent.go b/agent/agent.go index ec8b522f..ae75c340 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -62,7 +62,7 @@ type ( ) var ( - version = "0.1.16-pre1" + version = "0.1.16-pre2" testingModeFrequency = time.Second nonTestingModeFrequency = time.Minute diff --git a/agent/agent_test.go b/agent/agent_test.go index 7cb2f79a..a9a420db 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -5,6 +5,7 @@ import ( "os/exec" "reflect" "strings" + "sync" "testing" "time" @@ -141,3 +142,27 @@ func TestTildeExpandRaceMetadata(t *testing.T) { <-time.After(5 * time.Second) agent.Stop() } + +var a *Agent + +func BenchmarkNewAgent(b *testing.B) { + for i := 0; i < b.N; i++ { + var err error + a, err = NewAgent(WithTestingModeEnabled(), + WithHandlePanicAsFail(), + WithRetriesOnFail(3), + WithSetGlobalTracer()) + if err != nil { + b.Fatal(err) + } + span := a.Tracer().StartSpan("Test") + span.SetTag("span.kind", "test") + span.SetTag("test.name", "BenchNewAgent") + span.SetTag("test.suite", "root") + span.SetTag("test.status", tags.TestStatus_PASS) + span.SetBaggageItem("trace.kind", "test") + span.Finish() + once = sync.Once{} + a.Stop() + } +} diff --git a/agent/dependencies_test.go b/agent/dependencies_test.go new file mode 100644 index 00000000..deaeb82d --- /dev/null +++ b/agent/dependencies_test.go @@ -0,0 +1,11 @@ +package agent + +import "testing" + +var mp map[string]string + +func BenchmarkGetDependencyMap(b *testing.B) { + for i := 0; i < b.N; i++ { + mp = getDependencyMap() + } +} diff --git a/agent/git_test.go b/agent/git_test.go index e7788ba4..a53d05fa 100644 --- a/agent/git_test.go +++ b/agent/git_test.go @@ -108,3 +108,18 @@ func TestMergeRegex(t *testing.T) { } } } + +var data *GitData +var diff *GitDiff + +func BenchmarkGetGitData(b *testing.B) { + for i := 0; i < b.N; i++ { + data = getGitData() + } +} + +func BenchmarkGetGitDiff(b *testing.B) { + for i := 0; i < b.N; i++ { + diff = getGitDiff() + } +} diff --git a/go.mod b/go.mod index 5bfa1e54..a990d6d7 100644 --- a/go.mod +++ b/go.mod @@ -15,13 +15,10 @@ require ( github.com/stretchr/testify v1.5.1 github.com/undefinedlabs/go-mpatch v0.0.0-20200326085307-1a86426f42a6 github.com/vmihailenco/msgpack v4.0.4+incompatible - golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 // indirect golang.org/x/net v0.0.0-20200301022130-244492dfa37a golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 // indirect - golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 // indirect google.golang.org/appengine v1.6.5 // indirect google.golang.org/grpc v1.27.1 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 - honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc // indirect ) diff --git a/init.go b/init.go index fb3ae83d..995a00ad 100644 --- a/init.go +++ b/init.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/signal" + "reflect" "runtime" "sync" "syscall" @@ -75,6 +76,32 @@ func GetContextFromTest(t *testing.T) context.Context { return context.TODO() } +// Sets the test code from the caller of this func +func SetTestCodeFromCaller(t *testing.T) { + SetTestCodeFromCallerSkip(t, 1) +} + +// Sets the test code from the caller of this func +func SetTestCodeFromCallerSkip(t *testing.T, skip int) { + test := GetTest(t) + if test == nil { + return + } + pc, _, _, _ := runtime.Caller(skip + 1) + test.SetTestCode(pc) +} + +// Sets the test code from a func +func SetTestCodeFromFunc(t *testing.T, fn interface{}) { + test := GetTest(t) + if test == nil { + return + } + value := reflect.ValueOf(fn) + pc := value.Pointer() + test.SetTestCode(pc) +} + // Gets the *Benchmark from a *testing.B func GetBenchmark(b *testing.B) *scopetesting.Benchmark { return scopetesting.GetBenchmark(b) diff --git a/instrumentation/logging/logger_test.go b/instrumentation/logging/logger_test.go index e831f0f3..c1a6399a 100644 --- a/instrumentation/logging/logger_test.go +++ b/instrumentation/logging/logger_test.go @@ -132,3 +132,20 @@ func checkMessage(msg string, records []opentracing.LogRecord) bool { } return false } + +func BenchmarkPatchStandardLogger(b *testing.B) { + for i := 0; i < b.N; i++ { + PatchStandardLogger() + UnpatchStandardLogger() + } +} + +var lg *stdlog.Logger + +func BenchmarkPatchLogger(b *testing.B) { + for i := 0; i < b.N; i++ { + lg = stdlog.New(os.Stdout, "", stdlog.Llongfile|stdlog.Lmicroseconds) + PatchLogger(lg) + UnpatchLogger(lg) + } +} diff --git a/instrumentation/testing/benchmark.go b/instrumentation/testing/benchmark.go index 1bd122b7..a704f580 100644 --- a/instrumentation/testing/benchmark.go +++ b/instrumentation/testing/benchmark.go @@ -111,8 +111,10 @@ func startBenchmark(b *testing.B, pc uintptr, benchFunc func(b *testing.B)) { fullTestName = strings.Join(nameSegments, "/") } packageName := reflection.GetBenchmarkSuiteName(b) + pName, _, tCode := getPackageAndNameAndBoundaries(pc) + if packageName == "" { - packageName = getPackageName(pc, fullTestName) + packageName = pName } oTags := opentracing.Tags{ @@ -124,9 +126,8 @@ func startBenchmark(b *testing.B, pc uintptr, benchFunc func(b *testing.B)) { "test.type": "benchmark", } - testCode := getTestCodeBoundaries(pc, fullTestName) - if testCode != "" { - oTags["test.code"] = testCode + if tCode != "" { + oTags["test.code"] = tCode } var startOptions []opentracing.StartSpanOption diff --git a/instrumentation/testing/logger.go b/instrumentation/testing/logger.go index 08665537..7603de81 100644 --- a/instrumentation/testing/logger.go +++ b/instrumentation/testing/logger.go @@ -52,84 +52,82 @@ func UnpatchTestingLogger() { } func patchError() { - patch("Error", func(test *Test, argsValues []reflect.Value) { + patch("Error", func(test *Test, args []interface{}) { test.t.Helper() - args := getArgs(argsValues[0]) test.Error(args...) }) } func patchErrorf() { - patch("Errorf", func(test *Test, argsValues []reflect.Value) { + patch("Errorf", func(test *Test, args []interface{}) { test.t.Helper() - format := argsValues[0].String() - args := getArgs(argsValues[1]) - test.Errorf(format, args...) + format := args[0].(string) + test.Errorf(format, args[1:]...) }) } func patchFatal() { - patch("Fatal", func(test *Test, argsValues []reflect.Value) { + patch("Fatal", func(test *Test, args []interface{}) { test.t.Helper() - args := getArgs(argsValues[0]) test.Fatal(args...) }) } func patchFatalf() { - patch("Fatalf", func(test *Test, argsValues []reflect.Value) { + patch("Fatalf", func(test *Test, args []interface{}) { test.t.Helper() - format := argsValues[0].String() - args := getArgs(argsValues[1]) - test.Fatalf(format, args...) + format := args[0].(string) + test.Fatalf(format, args[1:]...) }) } func patchLog() { - patch("Log", func(test *Test, argsValues []reflect.Value) { + patch("Log", func(test *Test, args []interface{}) { test.t.Helper() - args := getArgs(argsValues[0]) test.Log(args...) }) } func patchLogf() { - patch("Logf", func(test *Test, argsValues []reflect.Value) { + patch("Logf", func(test *Test, args []interface{}) { test.t.Helper() - format := argsValues[0].String() - args := getArgs(argsValues[1]) - test.Logf(format, args...) + format := args[0].(string) + test.Logf(format, args[1:]...) }) } func patchSkip() { - patch("Skip", func(test *Test, argsValues []reflect.Value) { + patch("Skip", func(test *Test, args []interface{}) { test.t.Helper() - args := getArgs(argsValues[0]) test.Skip(args...) }) } func patchSkipf() { - patch("Skipf", func(test *Test, argsValues []reflect.Value) { + patch("Skipf", func(test *Test, args []interface{}) { test.t.Helper() - format := argsValues[0].String() - args := getArgs(argsValues[1]) - test.Skipf(format, args...) + format := args[0].(string) + test.Skipf(format, args[1:]...) }) } -func getArgs(in reflect.Value) []interface{} { +func createArgs(in []reflect.Value) []interface{} { var args []interface{} - if in.Kind() == reflect.Slice { - for i := 0; i < in.Len(); i++ { - args = append(args, in.Index(i).Interface()) + for _, item := range in { + if item.Kind() == reflect.Slice { + var itemArg []interface{} + for i := 0; i < item.Len(); i++ { + itemArg = append(itemArg, item.Index(i).Interface()) + } + args = append(args, itemArg) + } else { + args = append(args, item.Interface()) } } return args } -func patch(methodName string, methodBody func(test *Test, argsValues []reflect.Value)) { +func patch(methodName string, methodBody func(test *Test, argsValues []interface{})) { patchesMutex.Lock() defer patchesMutex.Unlock() patchPointersMutex.Lock() @@ -144,6 +142,7 @@ func patch(methodName string, methodBody func(test *Test, argsValues []reflect.V var methodPatch *mpatch.Patch var err error methodPatch, err = mpatch.PatchMethodWithMakeFunc(method, func(in []reflect.Value) []reflect.Value { + argIn := createArgs(in[1:]) t := (*testing.T)(unsafe.Pointer(in[0].Pointer())) if t == nil { instrumentation.Logger().Println("testing.T is nil") @@ -161,7 +160,7 @@ func patch(methodName string, methodBody func(test *Test, argsValues []reflect.V instrumentation.Logger().Printf("test struct for %v doesn't exist\n", t.Name()) return nil } - methodBody(test, in[1:]) + methodBody(test, argIn) return nil }) logOnError(err) diff --git a/instrumentation/testing/testing.go b/instrumentation/testing/testing.go index 04c882fb..16a7ca30 100644 --- a/instrumentation/testing/testing.go +++ b/instrumentation/testing/testing.go @@ -19,14 +19,16 @@ import ( "go.undefinedlabs.com/scopeagent/reflection" "go.undefinedlabs.com/scopeagent/runner" "go.undefinedlabs.com/scopeagent/tags" + "go.undefinedlabs.com/scopeagent/tracer" ) type ( Test struct { testing.TB - ctx context.Context - span opentracing.Span - t *testing.T + ctx context.Context + span opentracing.Span + t *testing.T + codePC uintptr } Option func(*Test) @@ -64,6 +66,7 @@ func StartTestFromCaller(t *testing.T, pc uintptr, opts ...Option) *Test { // If there is already one we want to replace it, so we clear the context test.ctx = context.Background() } + test.codePC = pc for _, opt := range opts { opt(test) @@ -72,17 +75,16 @@ func StartTestFromCaller(t *testing.T, pc uintptr, opts ...Option) *Test { // Extracting the testing func name (by removing any possible sub-test suffix `{test_func}/{sub_test}`) // to search the func source code bounds and to calculate the package name. fullTestName := runner.GetOriginalTestName(t.Name()) - packageName := getPackageName(pc, fullTestName) + pName, _, testCode := getPackageAndNameAndBoundaries(pc) testTags := opentracing.Tags{ "span.kind": "test", "test.name": fullTestName, - "test.suite": packageName, + "test.suite": pName, "test.framework": "testing", "test.language": "go", } - testCode := getTestCodeBoundaries(pc, fullTestName) if testCode != "" { testTags["test.code"] = testCode } @@ -102,6 +104,15 @@ func StartTestFromCaller(t *testing.T, pc uintptr, opts ...Option) *Test { return test } +// Set test code +func (test *Test) SetTestCode(pc uintptr) { + pName, _, fBoundaries := getPackageAndNameAndBoundaries(pc) + test.span.SetTag("test.suite", pName) + if fBoundaries != "" { + test.span.SetTag("test.code", fBoundaries) + } +} + // Ends the current test func (test *Test) End() { autoInstrumentedTestsMutex.RLock() @@ -135,6 +146,15 @@ func (test *Test) Run(name string, f func(t *testing.T)) bool { func (test *Test) end() { finishTime := time.Now() + // If we have our own implementation of the span, we can set the exact start time from the test + if ownSpan, ok := test.span.(tracer.Span); ok { + if startTime, err := reflection.GetTestStartTime(test.t); err == nil { + ownSpan.SetStart(startTime) + } else { + instrumentation.Logger().Printf("error: %v", err) + } + } + // Remove the Test struct from the hash map, so a call to Start while we end this instance will create a new struct removeTest(test.t) // Stop and get records generated by loggers diff --git a/instrumentation/testing/testing_test.go b/instrumentation/testing/testing_test.go index 965c0d8f..d9dae4d5 100644 --- a/instrumentation/testing/testing_test.go +++ b/instrumentation/testing/testing_test.go @@ -1,7 +1,12 @@ package testing import ( + "fmt" + "sync" "testing" + "time" + + "go.undefinedlabs.com/scopeagent/reflection" ) func TestLogBufferRegex(t *testing.T) { @@ -30,3 +35,65 @@ func TestLogBufferRegex(t *testing.T) { func TestExtractSubTestLogBuffer(t *testing.T) { t.Run("SubTest", TestLogBufferRegex) } + +func BenchmarkTestInit(b *testing.B) { + for i := 0; i < b.N; i++ { + tests := append(make([]testing.InternalTest, 0), + testing.InternalTest{Name: "Test01", F: func(t *testing.T) {}}, + testing.InternalTest{Name: "Test02", F: func(t *testing.T) {}}, + testing.InternalTest{Name: "Test03", F: func(t *testing.T) {}}, + testing.InternalTest{Name: "Test04", F: func(t *testing.T) {}}, + testing.InternalTest{Name: "Test05", F: func(t *testing.T) {}}, + ) + benchmarks := append(make([]testing.InternalBenchmark, 0), + testing.InternalBenchmark{Name: "Test01", F: func(b *testing.B) {}}, + testing.InternalBenchmark{Name: "Test02", F: func(b *testing.B) {}}, + testing.InternalBenchmark{Name: "Test03", F: func(b *testing.B) {}}, + testing.InternalBenchmark{Name: "Test04", F: func(b *testing.B) {}}, + testing.InternalBenchmark{Name: "Test05", F: func(b *testing.B) {}}, + ) + Init(testing.MainStart(nil, tests, benchmarks, nil)) + } +} + +func BenchmarkLoggerPatcher(b *testing.B) { + for i := 0; i < b.N; i++ { + PatchTestingLogger() + UnpatchTestingLogger() + } +} + +func TestLoggerPatcher(t *testing.T) { + tm := time.Now() + PatchTestingLogger() + wg := sync.WaitGroup{} + for i := 0; i < 1000; i++ { + wg.Add(1) + go func(x int) { + defer wg.Done() + t.Log(fmt.Sprintf("Hello world %d", x)) + }(i) + } + wg.Wait() + UnpatchTestingLogger() + if time.Since(tm) > 2*time.Second { + t.Fatal("Test is too slow") + } +} + +func TestIsParallelByReflection(t *testing.T) { + t.Parallel() + tm := time.Now() + wg := sync.WaitGroup{} + for i := 0; i < 1000; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _ = reflection.GetIsParallel(t) + }() + } + wg.Wait() + if time.Since(tm) > time.Second { + t.Fatal("Test is too slow") + } +} diff --git a/instrumentation/testing/util.go b/instrumentation/testing/util.go index 4ea946d7..974f71bb 100644 --- a/instrumentation/testing/util.go +++ b/instrumentation/testing/util.go @@ -10,49 +10,41 @@ import ( "go.undefinedlabs.com/scopeagent/instrumentation" ) -// Gets the Test/Benchmark parent func name without sub-benchmark and sub-test segments -func getFuncName(fullName string) string { - testNameSlash := strings.IndexByte(fullName, '/') - funcName := fullName - if testNameSlash >= 0 { - funcName = fullName[:testNameSlash] - } - return funcName +func getPackageAndName(pc uintptr) (string, string) { + return splitPackageAndName(runtime.FuncForPC(pc).Name()) } -// Gets the Package name -func getPackageName(pc uintptr, fullName string) string { - // Parent test/benchmark name - funcName := getFuncName(fullName) - // Full func name (format ex: {packageName}.{test/benchmark name}.{inner function of sub benchmark/test} - funcFullName := runtime.FuncForPC(pc).Name() - - // We select the packageName as substring from start to the index of the test/benchmark name minus 1 - funcNameIndex := strings.LastIndex(funcFullName, funcName) - if funcNameIndex < 1 { - funcNameIndex = len(funcFullName) +func splitPackageAndName(funcFullName string) (string, string) { + lastSlash := strings.LastIndexByte(funcFullName, '/') + if lastSlash < 0 { + lastSlash = 0 } - packageName := funcFullName[:funcNameIndex-1] - + firstDot := strings.IndexByte(funcFullName[lastSlash:], '.') + lastSlash + packName := funcFullName[:firstDot] // If the package has the format: _/{path...} // We convert the path from absolute to relative to the source root sourceRoot := instrumentation.GetSourceRoot() - if len(packageName) > 0 && packageName[0] == '_' && strings.Index(packageName, sourceRoot) != -1 { - packageName = strings.Replace(packageName, path.Dir(sourceRoot)+"/", "", -1)[1:] + if len(packName) > 0 && packName[0] == '_' && strings.Index(packName, sourceRoot) != -1 { + packName = strings.Replace(packName, path.Dir(sourceRoot)+"/", "", -1)[1:] } - - return packageName + funcName := funcFullName[firstDot+1:] + return packName, funcName } -// Gets the source code boundaries of a test or benchmark in the format: {file}:{startLine}:{endLine} -func getTestCodeBoundaries(pc uintptr, fullName string) string { - funcName := getFuncName(fullName) - sourceBounds, err := ast.GetFuncSourceForName(pc, funcName) +func getPackageAndNameAndBoundaries(pc uintptr) (string, string, string) { + pName, fName := getPackageAndName(pc) + dotIndex := strings.IndexByte(fName, '.') + if dotIndex != -1 { + fName = fName[:dotIndex] + } + + fBoundaries := "" + sourceBounds, err := ast.GetFuncSourceForName(pc, fName) if err != nil { - instrumentation.Logger().Printf("error calculating the source boundaries for '%s [%s]': %v", funcName, fullName, err) + instrumentation.Logger().Printf("error calculating the source boundaries for '%s': %v", fName, err) } if sourceBounds != nil { - return fmt.Sprintf("%s:%d:%d", sourceBounds.File, sourceBounds.Start.Line, sourceBounds.End.Line) + fBoundaries = fmt.Sprintf("%s:%d:%d", sourceBounds.File, sourceBounds.Start.Line, sourceBounds.End.Line) } - return "" + return pName, fName, fBoundaries } diff --git a/instrumentation/testing/util_test.go b/instrumentation/testing/util_test.go index f8d7378f..27d16cfc 100644 --- a/instrumentation/testing/util_test.go +++ b/instrumentation/testing/util_test.go @@ -8,61 +8,42 @@ import ( "go.undefinedlabs.com/scopeagent/ast" ) -func TestGetFuncName(t *testing.T) { - cases := map[string]string{ - "TestBase": "TestBase", - "TestBase/Sub01": "TestBase", - "TestBase/Sub 02": "TestBase", - "TestBase/Sub/Sub02": "TestBase", - "TestBase/Sub/Sub02/Sub03": "TestBase", - "TestBase/Sub/Sub02/Sub03/S u b 0 4": "TestBase", +func TestSplitPackageAndName(t *testing.T) { + cases := map[string][]string{ + "pkg.TestBase": {"pkg", "TestBase"}, + "pkg.TestBase.func1": {"pkg", "TestBase.func1"}, + "github.com/org/proj.TestBase": {"github.com/org/proj", "TestBase"}, } for key, expected := range cases { t.Run(key, func(t *testing.T) { - value := getFuncName(key) - if value != expected { - t.Fatalf("value '%s', expected: '%s'", value, expected) + pkg, fname := splitPackageAndName(key) + if pkg != expected[0] { + t.Fatalf("value '%s', expected: '%s'", pkg, expected[0]) + } + if fname != expected[1] { + t.Fatalf("value '%s', expected: '%s'", fname, expected[1]) } }) } } -func TestGetPackageName(t *testing.T) { +func TestGetTestCodeBoundaries(t *testing.T) { var pc uintptr func() { pc, _, _, _ = runtime.Caller(1) }() testName := t.Name() - packName := getPackageName(pc, testName) - subTestName := "" - t.Run("sub-test", func(t *testing.T) { - subTestName = t.Name() - }) - packName02 := getPackageName(pc, subTestName) + pkg, fname, bound := getPackageAndNameAndBoundaries(pc) - if testName != "TestGetPackageName" { - t.Fatalf("value '%s' not expected", testName) - } - if subTestName != "TestGetPackageName/sub-test" { - t.Fatalf("value '%s' not expected", testName) + if pkg != "go.undefinedlabs.com/scopeagent/instrumentation/testing" { + t.Fatalf("value '%s' not expected", pkg) } - if packName != "go.undefinedlabs.com/scopeagent/instrumentation/testing" { - t.Fatalf("value '%s' not expected", packName) + if fname != testName { + t.Fatalf("value '%s' not expected", fname) } - if packName != packName02 { - t.Fatalf("value '%s' not expected", packName02) - } -} - -func TestGetTestCodeBoundaries(t *testing.T) { - var pc uintptr - func() { pc, _, _, _ = runtime.Caller(1) }() - testName := t.Name() - - actualBoundary := getTestCodeBoundaries(pc, testName) boundaryExpected, _ := ast.GetFuncSourceForName(pc, testName) calcExpected := fmt.Sprintf("%s:%d:%d", boundaryExpected.File, boundaryExpected.Start.Line, boundaryExpected.End.Line) - if actualBoundary != calcExpected { - t.Fatalf("value '%s' not expected", actualBoundary) + if bound != calcExpected { + t.Fatalf("value '%s' not expected", bound) } } diff --git a/reflection/reflect.go b/reflection/reflect.go index ef281283..46a01bdd 100644 --- a/reflection/reflect.go +++ b/reflection/reflect.go @@ -5,6 +5,7 @@ import ( "reflect" "sync" "testing" + "time" "unsafe" ) @@ -66,6 +67,32 @@ func GetIsParallel(t *testing.T) bool { return false } +func GetTestStartTime(t *testing.T) (time.Time, error) { + mu := GetTestMutex(t) + if mu != nil { + mu.Lock() + defer mu.Unlock() + } + if pointer, err := GetFieldPointerOf(t, "start"); err == nil { + return *(*time.Time)(pointer), nil + } else { + return time.Time{}, err + } +} + +func GetTestDuration(t *testing.T) (time.Duration, error) { + mu := GetTestMutex(t) + if mu != nil { + mu.Lock() + defer mu.Unlock() + } + if pointer, err := GetFieldPointerOf(t, "duration"); err == nil { + return *(*time.Duration)(pointer), nil + } else { + return 0, err + } +} + func GetBenchmarkMutex(b *testing.B) *sync.RWMutex { if ptr, err := GetFieldPointerOf(b, "mu"); err == nil { return (*sync.RWMutex)(ptr) diff --git a/tracer/span.go b/tracer/span.go index 5bd9c11a..1fd3879d 100644 --- a/tracer/span.go +++ b/tracer/span.go @@ -22,6 +22,9 @@ type Span interface { // Start indicates when the span began Start() time.Time + + // Sets the start time + SetStart(start time.Time) opentracing.Span } // Implements the `Span` interface. Created via tracerImpl (see @@ -72,6 +75,13 @@ func (s *spanImpl) trim() bool { return !s.raw.Context.Sampled && s.tracer.options.TrimUnsampledSpans } +func (s *spanImpl) SetStart(start time.Time) opentracing.Span { + s.Lock() + defer s.Unlock() + s.raw.Start = start + return s +} + func (s *spanImpl) SetTag(key string, value interface{}) opentracing.Span { defer s.onTag(key, value) s.Lock()