From f73592b4357c05eee9c8dbe2ae9329faef313698 Mon Sep 17 00:00:00 2001 From: Kyle Nusbaum Date: Wed, 8 Jul 2020 09:25:08 -0500 Subject: [PATCH] ddtrace/tracer: add startup logging (#685) This commit adds a json-formatted log line and other diagnostic messages to tracer startup in order make debugging the tracer easier. --- ddtrace/tracer/log.go | 107 +++++++++++++++++++++++++++++++ ddtrace/tracer/log_test.go | 96 +++++++++++++++++++++++++++ ddtrace/tracer/option.go | 16 +++++ ddtrace/tracer/osinfo_darwin.go | 24 +++++++ ddtrace/tracer/osinfo_default.go | 20 ++++++ ddtrace/tracer/osinfo_linux.go | 52 +++++++++++++++ ddtrace/tracer/osinfo_windows.go | 54 ++++++++++++++++ ddtrace/tracer/sampler.go | 103 +++++++++++++++++------------ ddtrace/tracer/sampler_test.go | 76 +++++++++++----------- ddtrace/tracer/tracer.go | 13 +++- ddtrace/tracer/tracer_test.go | 7 ++ ddtrace/tracer/transport.go | 6 ++ ddtrace/tracer/transport_test.go | 2 + internal/log/log.go | 5 ++ 14 files changed, 504 insertions(+), 77 deletions(-) create mode 100644 ddtrace/tracer/log.go create mode 100644 ddtrace/tracer/log_test.go create mode 100644 ddtrace/tracer/osinfo_darwin.go create mode 100644 ddtrace/tracer/osinfo_default.go create mode 100644 ddtrace/tracer/osinfo_linux.go create mode 100644 ddtrace/tracer/osinfo_windows.go diff --git a/ddtrace/tracer/log.go b/ddtrace/tracer/log.go new file mode 100644 index 0000000000..7e494319f0 --- /dev/null +++ b/ddtrace/tracer/log.go @@ -0,0 +1,107 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package tracer + +import ( + "encoding/json" + "fmt" + "math" + "net/http" + "runtime" + "time" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/version" +) + +const ( + unknown = "unknown" +) + +// startupInfo contains various information about the status of the tracer on startup. +type startupInfo struct { + Date string `json:"date"` // ISO 8601 date and time of start + OSName string `json:"os_name"` // Windows, Darwin, Debian, etc. + OSVersion string `json:"os_version"` // Version of the OS + Version string `json:"version"` // Tracer version + Lang string `json:"lang"` // "Go" + LangVersion string `json:"lang_version"` // Go version, e.g. go1.13 + Env string `json:"env"` // Tracer env + Service string `json:"service"` // Tracer Service + AgentURL string `json:"agent_url"` // The address of the agent + AgentError string `json:"agent_error"` // Any error that occurred trying to connect to agent + Debug bool `json:"debug"` // Whether debug mode is enabled + AnalyticsEnabled bool `json:"analytics_enabled"` // True if there is a global analytics rate set + SampleRate string `json:"sample_rate"` // The default sampling rate for the rules sampler + SamplingRules []SamplingRule `json:"sampling_rules"` // Rules used by the rules sampler + SamplingRulesError string `json:"sampling_rules_error"` // Any errors that occurred while parsing sampling rules + Tags map[string]string `json:"tags"` // Global tags + RuntimeMetricsEnabled bool `json:"runtime_metrics_enabled"` // Whether or not runtime metrics are enabled + HealthMetricsEnabled bool `json:"health_metrics_enabled"` // Whether or not health metrics are enabled + ApplicationVersion string `json:"dd_version"` // Version of the user's application + Architecture string `json:"architecture"` // Architecture of host machine + GlobalService string `json:"global_service"` // Global service string. If not-nil should be same as Service. (#614) +} + +// checkEndpoint tries to connect to the URL specified by endpoint. +// If the endpoint is not reachable, checkEndpoint returns an error +// explaining why. +func checkEndpoint(endpoint string) error { + req, err := http.NewRequest("POST", endpoint, nil) + if err != nil { + return fmt.Errorf("cannot create http request: %v", err) + } + _, err = defaultClient.Do(req) + if err != nil { + return err + } + return nil +} + +// logStartup generates a startupInfo for a tracer and writes it to the log in +// JSON format. +func logStartup(t *tracer) { + tags := make(map[string]string) + for k, v := range t.globalTags { + tags[k] = fmt.Sprintf("%v", v) + } + + info := startupInfo{ + Date: time.Now().Format(time.RFC3339), + OSName: osName(), + OSVersion: osVersion(), + Version: version.Tag, + Lang: "Go", + LangVersion: runtime.Version(), + Env: t.config.env, + Service: t.config.serviceName, + AgentURL: t.transport.endpoint(), + Debug: t.config.debug, + AnalyticsEnabled: !math.IsNaN(globalconfig.AnalyticsRate()), + SampleRate: fmt.Sprintf("%f", t.rulesSampling.globalRate), + SamplingRules: t.rulesSampling.rules, + Tags: tags, + RuntimeMetricsEnabled: t.config.runtimeMetrics, + HealthMetricsEnabled: t.config.runtimeMetrics, + ApplicationVersion: t.config.version, + Architecture: runtime.GOARCH, + GlobalService: globalconfig.ServiceName(), + } + if _, err := samplingRulesFromEnv(); err != nil { + info.SamplingRulesError = fmt.Sprintf("%s", err) + } + if err := checkEndpoint(t.transport.endpoint()); err != nil { + info.AgentError = fmt.Sprintf("%s", err) + log.Warn("DIAGNOSTICS Unable to reach agent: %s", err) + } + bs, err := json.Marshal(info) + if err != nil { + log.Warn("DIAGNOSTICS Failed to serialize json for startup log (%v) %#v\n", err, info) + return + } + log.Info("DATADOG TRACER CONFIGURATION %s\n", string(bs)) +} diff --git a/ddtrace/tracer/log_test.go b/ddtrace/tracer/log_test.go new file mode 100644 index 0000000000..dfba1b1de9 --- /dev/null +++ b/ddtrace/tracer/log_test.go @@ -0,0 +1,96 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package tracer + +import ( + "math" + "os" + "testing" + + "gopkg.in/DataDog/dd-trace-go.v1/internal/globalconfig" + + "github.com/stretchr/testify/assert" +) + +func TestStartupLog(t *testing.T) { + t.Run("basic", func(t *testing.T) { + assert := assert.New(t) + tp := new(testLogger) + tracer, _, _, stop := startTestTracer(t, WithLogger(tp)) + defer stop() + + tp.Reset() + logStartup(tracer) + assert.Len(tp.Lines(), 2) + assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sampling_rules":null,"sampling_rules_error":"","tags":{},"runtime_metrics_enabled":false,"health_metrics_enabled":false,"dd_version":"","architecture":"[^"]*","global_service":""}`, tp.Lines()[1]) + }) + + t.Run("configured", func(t *testing.T) { + assert := assert.New(t) + tp := new(testLogger) + os.Setenv("DD_TRACE_SAMPLE_RATE", "0.123") + defer os.Unsetenv("DD_TRACE_SAMPLE_RATE") + tracer, _, _, stop := startTestTracer(t, + WithLogger(tp), + WithService("configured.service"), + WithAgentAddr("test.host:1234"), + WithEnv("configuredEnv"), + WithGlobalTag("tag", "value"), + WithGlobalTag("tag2", math.NaN()), + WithRuntimeMetrics(), + WithAnalyticsRate(1.0), + WithServiceVersion("2.3.4"), + WithSamplingRules([]SamplingRule{ServiceRule("mysql", 0.75)}), + WithDebugMode(true), + ) + defer globalconfig.SetAnalyticsRate(math.NaN()) + defer globalconfig.SetServiceName("") + defer stop() + + tp.Reset() + logStartup(tracer) + assert.Len(tp.Lines(), 2) + assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"configuredEnv","service":"configured.service","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":true,"analytics_enabled":true,"sample_rate":"0\.123000","sampling_rules":\[{"service":"mysql","name":"","sample_rate":0\.75}\],"sampling_rules_error":"","tags":{"tag":"value","tag2":"NaN"},"runtime_metrics_enabled":true,"health_metrics_enabled":true,"dd_version":"2.3.4","architecture":"[^"]*","global_service":"configured.service"}`, tp.Lines()[1]) + }) + + t.Run("errors", func(t *testing.T) { + assert := assert.New(t) + tp := new(testLogger) + os.Setenv("DD_TRACE_SAMPLING_RULES", `[{"service": "some.service", "sample_rate": 0.234}, {"service": "other.service"}]`) + defer os.Unsetenv("DD_TRACE_SAMPLING_RULES") + tracer, _, _, stop := startTestTracer(t, WithLogger(tp)) + defer stop() + + tp.Reset() + logStartup(tracer) + assert.Len(tp.Lines(), 2) + assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ INFO: DATADOG TRACER CONFIGURATION {"date":"[^"]*","os_name":"[^"]*","os_version":"[^"]*","version":"[^"]*","lang":"Go","lang_version":"[^"]*","env":"","service":"tracer\.test","agent_url":"http://localhost:9/v0.4/traces","agent_error":"Post .*","debug":false,"analytics_enabled":false,"sample_rate":"NaN","sampling_rules":\[{"service":"some.service","name":"","sample_rate":0\.234}\],"sampling_rules_error":"found errors:\\n\\tat index 1: rate not provided","tags":{},"runtime_metrics_enabled":false,"health_metrics_enabled":false,"dd_version":"","architecture":"[^"]*","global_service":""}`, tp.Lines()[1]) + }) +} + +func TestLogSamplingRules(t *testing.T) { + assert := assert.New(t) + tp := new(testLogger) + os.Setenv("DD_TRACE_SAMPLING_RULES", `[{"service": "some.service", "sample_rate": 0.234}, {"service": "other.service"}, {"service": "last.service", "sample_rate": 0.56}, {"odd": "pairs"}, {"sample_rate": 9.10}]`) + defer os.Unsetenv("DD_TRACE_SAMPLING_RULES") + _, _, _, stop := startTestTracer(t, WithLogger(tp)) + defer stop() + + assert.Len(tp.Lines(), 2) + assert.Contains(tp.Lines()[0], "WARN: at index 4: ignoring rule {Service: Name: Rate:9.10}: rate is out of [0.0, 1.0] range") + assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ WARN: DIAGNOSTICS Error\(s\) parsing DD_TRACE_SAMPLING_RULES: found errors:\n\tat index 1: rate not provided\n\tat index 3: rate not provided$`, tp.Lines()[1]) +} + +func TestLogAgentReachable(t *testing.T) { + assert := assert.New(t) + tp := new(testLogger) + tracer, _, _, stop := startTestTracer(t, WithLogger(tp)) + defer stop() + tp.Reset() + logStartup(tracer) + assert.Len(tp.Lines(), 2) + assert.Regexp(`Datadog Tracer v[0-9]+\.[0-9]+\.[0-9]+ WARN: DIAGNOSTICS Unable to reach agent: Post`, tp.Lines()[0]) +} diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index b42602ff32..6ce6e91e29 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -12,6 +12,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "time" @@ -29,6 +30,10 @@ type config struct { // debug, when true, writes details to logs. debug bool + // logStartup, when true, causes various startup info to be written + // when the tracer starts. + logStartup bool + // serviceName specifies the name of this application. serviceName string @@ -135,6 +140,7 @@ func newConfig(opts ...StartOption) *config { } } } + c.logStartup = boolEnv("DD_TRACE_STARTUP_LOGS", true) for _, fn := range opts { fn(c) } @@ -484,3 +490,13 @@ func StackFrames(n, skip uint) FinishOption { cfg.SkipStackFrames = skip } } + +// boolEnv returns the parsed boolean value of an environment variable, or +// def if it fails to parse. +func boolEnv(key string, def bool) bool { + v, err := strconv.ParseBool(os.Getenv(key)) + if err != nil { + return def + } + return v +} diff --git a/ddtrace/tracer/osinfo_darwin.go b/ddtrace/tracer/osinfo_darwin.go new file mode 100644 index 0000000000..d2a4623db1 --- /dev/null +++ b/ddtrace/tracer/osinfo_darwin.go @@ -0,0 +1,24 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package tracer + +import ( + "os/exec" + "runtime" + "strings" +) + +func osName() string { + return runtime.GOOS +} + +func osVersion() string { + out, err := exec.Command("sw_vers", "-productVersion").Output() + if err != nil { + return unknown + } + return strings.Trim(string(out), "\n") +} diff --git a/ddtrace/tracer/osinfo_default.go b/ddtrace/tracer/osinfo_default.go new file mode 100644 index 0000000000..a5b8d3f14f --- /dev/null +++ b/ddtrace/tracer/osinfo_default.go @@ -0,0 +1,20 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +// +build !windows,!linux,!darwin + +package tracer + +import ( + "runtime" +) + +func osName() string { + return runtime.GOOS +} + +func osVersion() string { + return unknownVersion +} diff --git a/ddtrace/tracer/osinfo_linux.go b/ddtrace/tracer/osinfo_linux.go new file mode 100644 index 0000000000..5b062db0f6 --- /dev/null +++ b/ddtrace/tracer/osinfo_linux.go @@ -0,0 +1,52 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package tracer + +import ( + "bufio" + "os" + "strings" +) + +func osName() string { + f, err := os.Open("/etc/os-release") + if err != nil { + return "Linux (Unknown Distribution)" + } + defer f.Close() + s := bufio.NewScanner(f) + name := "Linux (Unknown Distribution)" + for s.Scan() { + parts := strings.SplitN(s.Text(), "=", 2) + switch parts[0] { + case "Name": + name = strings.Trim(parts[1], "\"") + } + } + return name +} + +func osVersion() string { + f, err := os.Open("/etc/os-release") + if err != nil { + return unknown + } + defer f.Close() + s := bufio.NewScanner(f) + version := unknown + for s.Scan() { + parts := strings.SplitN(s.Text(), "=", 2) + switch parts[0] { + case "VERSION": + version = strings.Trim(parts[1], "\"") + case "VERSION_ID": + if version == "" { + version = strings.Trim(parts[1], "\"") + } + } + } + return version +} diff --git a/ddtrace/tracer/osinfo_windows.go b/ddtrace/tracer/osinfo_windows.go new file mode 100644 index 0000000000..81a0b83c1a --- /dev/null +++ b/ddtrace/tracer/osinfo_windows.go @@ -0,0 +1,54 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-2020 Datadog, Inc. + +package tracer + +import ( + "fmt" + "runtime" + "strings" + + "golang.org/x/sys/windows/registry" +) + +func osName() string { + return runtime.GOOS +} + +func osVersion() string { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) + if err != nil { + return unknown + } + defer k.Close() + + var version strings.Builder + + maj, _, err := k.GetIntegerValue("CurrentMajorVersionNumber") + if err == nil { + version.WriteString(fmt.Sprintf("%d", maj)) + min, _, err := k.GetIntegerValue("CurrentMinorVersionNumber") + if err == nil { + version.WriteString(fmt.Sprintf(".%d", min)) + } + } else { + version.WriteString(unknown) + } + + ed, _, err := k.GetStringValue("EditionID") + if err == nil { + version.WriteString(" " + ed) + } else { + version.WriteString(" Unknown Edition") + } + + build, _, err := k.GetStringValue("CurrentBuild") + if err == nil { + version.WriteString(" Build " + build) + } else { + version.WriteString(" Unknown Build") + } + return version.String() +} diff --git a/ddtrace/tracer/sampler.go b/ddtrace/tracer/sampler.go index 53af13c4e2..f21fdb9bb0 100644 --- a/ddtrace/tracer/sampler.go +++ b/ddtrace/tracer/sampler.go @@ -7,11 +7,13 @@ package tracer import ( "encoding/json" + "fmt" "io" "math" "os" "regexp" "strconv" + "strings" "sync" "time" @@ -179,57 +181,57 @@ type rulesSampler struct { // Invalid rules or environment variable values are tolerated, by logging warnings and then ignoring them. func newRulesSampler(rules []SamplingRule) *rulesSampler { return &rulesSampler{ - rules: appliedSamplingRules(rules), + rules: rules, globalRate: globalSampleRate(), limiter: newRateLimiter(), } } -// appliedSamplingRules validates the user-provided rules and returns an internal representation. -// If the DD_TRACE_SAMPLING_RULES environment variable is set, it will replace the given rules. -func appliedSamplingRules(rules []SamplingRule) []SamplingRule { +// samplingRulesFromEnv parses sampling rules from the DD_TRACE_SAMPLING_RULES +// environment variable. +func samplingRulesFromEnv() ([]SamplingRule, error) { rulesFromEnv := os.Getenv("DD_TRACE_SAMPLING_RULES") - if rulesFromEnv != "" { - rules = rules[:0] - jsonRules := []struct { - Service string `json:"service"` - Name string `json:"name"` - Rate json.Number `json:"sample_rate"` - }{} - err := json.Unmarshal([]byte(rulesFromEnv), &jsonRules) - if err != nil { - log.Warn("error parsing DD_TRACE_SAMPLING_RULES: %v", err) - return nil + if rulesFromEnv == "" { + return nil, nil + } + jsonRules := []struct { + Service string `json:"service"` + Name string `json:"name"` + Rate json.Number `json:"sample_rate"` + }{} + err := json.Unmarshal([]byte(rulesFromEnv), &jsonRules) + if err != nil { + return nil, fmt.Errorf("error unmarshalling JSON: %v", err) + } + rules := make([]SamplingRule, 0, len(jsonRules)) + var errs []string + for i, v := range jsonRules { + if v.Rate == "" { + errs = append(errs, fmt.Sprintf("at index %d: rate not provided", i)) + continue } - for _, v := range jsonRules { - if v.Rate == "" { - log.Warn("error parsing rule: rate not provided") - continue - } - rate, err := v.Rate.Float64() - if err != nil { - log.Warn("error parsing rule: invalid rate: %v", err) - continue - } - switch { - case v.Service != "" && v.Name != "": - rules = append(rules, NameServiceRule(v.Name, v.Service, rate)) - case v.Service != "": - rules = append(rules, ServiceRule(v.Service, rate)) - case v.Name != "": - rules = append(rules, NameRule(v.Name, rate)) - } + rate, err := v.Rate.Float64() + if err != nil { + errs = append(errs, fmt.Sprintf("at index %d: %v", i, err)) + continue } - } - validRules := make([]SamplingRule, 0, len(rules)) - for _, v := range rules { - if !(v.Rate >= 0.0 && v.Rate <= 1.0) { - log.Warn("ignoring rule %+v: rate is out of range", v) + if !(rate >= 0.0 && rate <= 1.0) { + log.Warn("at index %d: ignoring rule %+v: rate is out of [0.0, 1.0] range", i, v) continue } - validRules = append(validRules, v) + switch { + case v.Service != "" && v.Name != "": + rules = append(rules, NameServiceRule(v.Name, v.Service, rate)) + case v.Service != "": + rules = append(rules, ServiceRule(v.Service, rate)) + case v.Name != "": + rules = append(rules, NameRule(v.Name, rate)) + } } - return validRules + if len(errs) != 0 { + return rules, fmt.Errorf("found errors:\n\t%s", strings.Join(errs, "\n\t")) + } + return rules, nil } // globalSampleRate returns the sampling rate found in the DD_TRACE_SAMPLE_RATE environment variable. @@ -384,6 +386,27 @@ func (sr *SamplingRule) match(s *span) bool { return true } +// MarshalJSON implements the json.Marshaler interface. +func (sr *SamplingRule) MarshalJSON() ([]byte, error) { + s := struct { + Service string `json:"service"` + Name string `json:"name"` + Rate float64 `json:"sample_rate"` + }{} + if sr.exactService != "" { + s.Service = sr.exactService + } else if sr.Service != nil { + s.Service = fmt.Sprintf("%s", sr.Service) + } + if sr.exactName != "" { + s.Name = sr.exactName + } else if sr.Name != nil { + s.Name = fmt.Sprintf("%s", sr.Name) + } + s.Rate = sr.Rate + return json.Marshal(&s) +} + // rateLimiter is a wrapper on top of golang.org/x/time/rate which implements a rate limiter but also // returns the effective rate of allowance. type rateLimiter struct { diff --git a/ddtrace/tracer/sampler_test.go b/ddtrace/tracer/sampler_test.go index 618aca4431..85827d8c3d 100644 --- a/ddtrace/tracer/sampler_test.go +++ b/ddtrace/tracer/sampler_test.go @@ -234,43 +234,47 @@ func TestRuleEnvVars(t *testing.T) { t.Run("sampling-rules", func(t *testing.T) { assert := assert.New(t) defer os.Unsetenv("DD_TRACE_SAMPLING_RULES") - // represents hard-coded rules - rules := []SamplingRule{ - RateRule(1.0), - } - // env overrides provided rules - os.Setenv("DD_TRACE_SAMPLING_RULES", "[]") - validRules := appliedSamplingRules(rules) - assert.Len(validRules, 0) - - // valid rules - os.Setenv("DD_TRACE_SAMPLING_RULES", `[{"service": "abcd", "sample_rate": 1.0}]`) - validRules = appliedSamplingRules(rules) - assert.Len(validRules, 1) - - os.Setenv("DD_TRACE_SAMPLING_RULES", `[{"service": "abcd", "sample_rate": 1.0},`+ - `{"name": "wxyz", "sample_rate": 0.9},`+ - `{"service": "efgh", "name": "lmnop", "sample_rate": 0.42}]`) - validRules = appliedSamplingRules(rules) - assert.Len(validRules, 3) - - // invalid rule ignored - os.Setenv("DD_TRACE_SAMPLING_RULES", `[{"service": "abcd", "sample_rate": 42.0}]`) - validRules = appliedSamplingRules(rules) - assert.Len(validRules, 0) - - os.Setenv("DD_TRACE_SAMPLING_RULES", `[{"service": "abcd", "sample_rate": "all of them"}]`) - validRules = appliedSamplingRules(rules) - assert.Len(validRules, 0) - - os.Setenv("DD_TRACE_SAMPLING_RULES", `[{"service": "abcd"}]`) - validRules = appliedSamplingRules(rules) - assert.Len(validRules, 0) - - os.Setenv("DD_TRACE_SAMPLING_RULES", `not JSON at all`) - validRules = appliedSamplingRules(rules) - assert.Len(validRules, 0) + for _, tt := range []struct { + value string + ruleN int + errStr string + }{ + { + value: "[]", + ruleN: 0, + }, { + value: `[{"service": "abcd", "sample_rate": 1.0}]`, + ruleN: 1, + }, { + value: `[{"service": "abcd", "sample_rate": 1.0},{"name": "wxyz", "sample_rate": 0.9},{"service": "efgh", "name": "lmnop", "sample_rate": 0.42}]`, + ruleN: 3, + }, { + // invalid rule ignored + value: `[{"service": "abcd", "sample_rate": 42.0}, {"service": "abcd", "sample_rate": 0.2}]`, + ruleN: 1, + }, { + value: `[{"service": "abcd", "sample_rate": "all of them"}]`, + errStr: "found errors:\n\tat index 0: strconv.ParseFloat: parsing \"all of them\": invalid syntax", + }, { + value: `[{"service": "abcd"}, {"service": "d", "sample_rate": "invalid"}]`, + errStr: "found errors:\n\tat index 0: rate not provided\n\tat index 1: strconv.ParseFloat: parsing \"invalid\": invalid syntax", + }, { + value: `not JSON at all`, + errStr: `error unmarshalling JSON: invalid character 'o' in literal null (expecting 'u')`, + }, + } { + t.Run("", func(t *testing.T) { + os.Setenv("DD_TRACE_SAMPLING_RULES", tt.value) + rules, err := samplingRulesFromEnv() + if tt.errStr == "" { + assert.NoError(err) + } else { + assert.Equal(err.Error(), tt.errStr) + } + assert.Len(rules, tt.ruleN) + }) + } }) } diff --git a/ddtrace/tracer/tracer.go b/ddtrace/tracer/tracer.go index 815401e17b..5a1fb88001 100644 --- a/ddtrace/tracer/tracer.go +++ b/ddtrace/tracer/tracer.go @@ -91,7 +91,11 @@ func Start(opts ...StartOption) { if internal.Testing { return // mock tracer active } - internal.SetGlobalTracer(newTracer(opts...)) + t := newTracer(opts...) + internal.SetGlobalTracer(t) + if t.config.logStartup { + logStartup(t) + } } // Stop stops the started tracer. Subsequent calls are valid but become no-op. @@ -130,6 +134,13 @@ const payloadQueueSize = 1000 func newUnstartedTracer(opts ...StartOption) *tracer { c := newConfig(opts...) + envRules, err := samplingRulesFromEnv() + if err != nil { + log.Warn("DIAGNOSTICS Error(s) parsing DD_TRACE_SAMPLING_RULES: %s", err) + } + if envRules != nil { + c.samplingRules = envRules + } return &tracer{ config: c, payload: newPayload(), diff --git a/ddtrace/tracer/tracer_test.go b/ddtrace/tracer/tracer_test.go index d7a6a4668f..04e8eab940 100644 --- a/ddtrace/tracer/tracer_test.go +++ b/ddtrace/tracer/tracer_test.go @@ -64,6 +64,9 @@ func TestTracerCleanStop(t *testing.T) { if testing.Short() { return } + os.Setenv("DD_TRACE_STARTUP_LOGS", "0") + defer os.Unsetenv("DD_TRACE_STARTUP_LOGS") + var wg sync.WaitGroup transport := newDummyTransport() @@ -1151,6 +1154,10 @@ func (t *dummyTransport) send(p *payload) (io.ReadCloser, error) { return ok, nil } +func (t *dummyTransport) endpoint() string { + return "http://localhost:9/v0.4/traces" +} + func decode(p *payload) (spanLists, error) { var traces spanLists err := msgp.Decode(p, &traces) diff --git a/ddtrace/tracer/transport.go b/ddtrace/tracer/transport.go index 52022737ec..7fa506e468 100644 --- a/ddtrace/tracer/transport.go +++ b/ddtrace/tracer/transport.go @@ -52,6 +52,8 @@ type transport interface { // send sends the payload p to the agent using the transport set up. // It returns a non-nil response body when no error occurred. send(p *payload) (body io.ReadCloser, err error) + // endpoint returns the URL to which the transport will send traces. + endpoint() string } // newTransport returns a new Transport implementation that sends traces to a @@ -132,6 +134,10 @@ func (t *httpTransport) send(p *payload) (body io.ReadCloser, err error) { return response.Body, nil } +func (t *httpTransport) endpoint() string { + return t.traceURL +} + // resolveAddr resolves the given agent address and fills in any missing host // and port using the defaults. Some environment variable settings will // take precedence over configuration. diff --git a/ddtrace/tracer/transport_test.go b/ddtrace/tracer/transport_test.go index 01166aa43e..21361d9216 100644 --- a/ddtrace/tracer/transport_test.go +++ b/ddtrace/tracer/transport_test.go @@ -245,6 +245,8 @@ func TestCustomTransport(t *testing.T) { } func TestWithHTTPClient(t *testing.T) { + os.Setenv("DD_TRACE_STARTUP_LOGS", "0") + defer os.Unsetenv("DD_TRACE_STARTUP_LOGS") assert := assert.New(t) srv := mockDatadogAPINewServer(t) defer srv.Close() diff --git a/internal/log/log.go b/internal/log/log.go index 6a92d5e77d..a0bb8ef31b 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -66,6 +66,11 @@ func Warn(fmt string, a ...interface{}) { printMsg("WARN", fmt, a...) } +// Info prints an informational message. +func Info(fmt string, a ...interface{}) { + printMsg("INFO", fmt, a...) +} + var ( errmu sync.RWMutex // guards below fields erragg = map[string]*errorReport{} // aggregated errors