diff --git a/README.md b/README.md index c11532a51..7ce221b08 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,7 @@ Another interesting feature is the usage of additional packages located under [i 1. [Tracing an application running on Azure Container Apps](docs/azure_container_apps.md) 1. [Tracing Other Go Packages](docs/other_packages.md) 1. [Instrumenting Code Manually](docs/manual_instrumentation.md) +1. [Disabling Spans by Category](docs/disabling_spans.md) 1. [Generic Serverless Agent](/docs/generic_serverless_agent.md) diff --git a/agent.go b/agent.go index cd03686c3..5fef356b6 100644 --- a/agent.go +++ b/agent.go @@ -51,7 +51,8 @@ type agentResponse struct { } `json:"secrets"` ExtraHTTPHeaders []string `json:"extraHeaders"` Tracing struct { - ExtraHTTPHeaders []string `json:"extra-http-headers"` + ExtraHTTPHeaders []string `json:"extra-http-headers"` + Disable []map[string]bool `json:"disable"` } `json:"tracing"` } diff --git a/agent_test.go b/agent_test.go index 5ced6c4d6..d8ba58907 100644 --- a/agent_test.go +++ b/agent_test.go @@ -128,7 +128,8 @@ func Test_agentApplyHostSettings(t *testing.T) { Pid: 37892, HostID: "myhost", Tracing: struct { - ExtraHTTPHeaders []string `json:"extra-http-headers"` + ExtraHTTPHeaders []string `json:"extra-http-headers"` + Disable []map[string]bool `json:"disable"` }{ ExtraHTTPHeaders: []string{"my-unwanted-custom-headers"}, }, diff --git a/docs/disabling_spans.md b/docs/disabling_spans.md new file mode 100644 index 000000000..72f62c27c --- /dev/null +++ b/docs/disabling_spans.md @@ -0,0 +1,88 @@ +# Disabling Log Spans + +The Instana Go Tracer allows you to disable log spans to reduce the amount of data being collected and processed. This can be useful in high-volume environments or when you want to focus on specific types of traces. + +## Supported Span Categories + +Currently, only the following span category can be disabled: + +| Category | Description | Affected Instrumentations | +| --------- | ----------- | ------------------------- | +| `logging` | Log spans | `logrus` | + +## Configuration Methods + +There are four ways to disable log spans: + +### 1. Using Code + +You can disable log spans when initializing the tracer: + +```go +col := instana.InitCollector(&instana.Options{ + Service: "My Service", + Tracer: instana.TracerOptions{ + DisableSpans: map[string]bool{ + "logging": true, // Disable log spans + }, + }, +}) +``` + +### 2. Using Environment Variables + +You can disable log spans using the `INSTANA_TRACING_DISABLE` environment variable: + +```bash +# Disable log spans +export INSTANA_TRACING_DISABLE="logging" + +# Disable all tracing +export INSTANA_TRACING_DISABLE=true +``` + +### 3. Using Configuration File + +You can create a YAML configuration file and specify its path using the `INSTANA_CONFIG_PATH` environment variable: + +```yaml +# config.yaml +tracing: + disable: + - logging +``` + +```bash +export INSTANA_CONFIG_PATH=/path/to/config.yaml +``` + +**Note:** The tracer enforces a maximum config file size of 1 MB. Any file exceeding this limit will be rejected during validation. This safeguard is in place to prevent accidental or malicious use of excessively large files, since configuration data is expected to remain lightweight. + +### 4. Using Instana Agent Configuration + +You can configure the Instana agent to disable log spans for all applications monitored by this agent: + +1. Locate your Instana agent configuration file. +2. Add the following configuration to the agent's configuration file: +```yaml +com.instana.tracing: + disable: + - logging +``` +3. Restart the Instana agent: + +## Priority Order + +When multiple configuration methods are used, they are applied in the following decreasing order of precedence: + +1. Configuration file (`INSTANA_CONFIG_PATH`) +2. Environment variable (`INSTANA_TRACING_DISABLE`) +3. Code-level configuration +4. Agent configuration + +## Use Cases + +- **Performance Optimization**: In high-throughput applications, disabling log spans can reduce the overhead of tracing. +- **Cost Management**: Reduce the volume of trace data sent to Instana to manage costs. +- **Focus on Specific Areas**: Disable log spans to focus on the parts of your application that need attention. +- **Testing**: Temporarily disable log spans during testing or development. \ No newline at end of file diff --git a/docs/options.md b/docs/options.md index adcdb7e49..d43e4f933 100644 --- a/docs/options.md +++ b/docs/options.md @@ -66,7 +66,13 @@ IncludeProfilerFrames is whether to include profiler calls into the profile or n **Type:** [TracerOptions](https://pkg.go.dev/github.com/instana/go-sensor#TracerOptions) -Tracer contains tracer-specific configuration used by all tracers +Tracer contains tracer-specific configuration used by all tracers. Key options include: + +- **Disable**: A map of span categories to disable. Currently, only the "logging" category is supported. See [Disabling Log Spans](disabling_spans.md) for details. +- **DropAllLogs**: Turns log events on all spans into no-ops when set to true. +- **MaxLogsPerSpan**: Maximum number of log records that can be attached to a span. +- **Secrets**: A secrets matcher used to filter out sensitive data from HTTP requests, database connection strings, etc. +- **CollectableHTTPHeaders**: A list of HTTP headers to be collected from requests. #### AgentClient @@ -97,3 +103,4 @@ Go Tracer only captures log spans with severity `warn` or higher. [Tracing SQL Driver Databases](sql.md) | [Tracing Other Go Packages](other_packages.md) | [Instrumenting Code Manually](manual_instrumentation.md) + diff --git a/example/disable-log-spans/README.md b/example/disable-log-spans/README.md new file mode 100644 index 000000000..d10319b2d --- /dev/null +++ b/example/disable-log-spans/README.md @@ -0,0 +1,149 @@ +# Disabling Log Spans Example + +This example demonstrates how to disable log spans in an Instana-instrumented Go application using four different methods: +1. Using Code +2. Using Environment Variables +3. Using Configuration File +4. Using Instana Agent Configuration + +The application uses logrus for logging and GORM with SQLite for database operations. It creates a simple HTTP server with two endpoints: +- `/start` - Creates a database table and inserts a record +- `/error` - Deliberately causes a database error + +Both endpoints generate log entries that would normally create log spans in Instana. + +## Prerequisites + +- Go 1.23 or later +- The Instana Agent should either be running locally or accessible via the network. If no agent is available, a serverless endpoint must be configured. + +## Setup + +1. Clone the repository: + ```bash + git clone https://github.com/instana/go-sensor.git + cd go-sensor/example/disable-log-spans + ``` + +2. Install dependencies: + ```bash + go mod tidy + ``` + +## Running the Example + +### Scenario 1: Disable Log Spans Using Code + +In this scenario, we'll disable log spans directly in the code by modifying the Instana tracer options. + +1. Edit `main.go` and update the `init()` function to include the `Disable` option: + +```go +func init() { + c = instana.InitCollector(&instana.Options{ + Service: "logrus-example", + Tracer: instana.TracerOptions{ + DisableSpans: map[string]bool{ + "logging": true, + }, + }, + }) + connectDB() +} +``` + +2. Run the application: +```bash +go run main.go +``` + +3. Expected output: + - The application will start and make requests to both endpoints + - You'll see log messages in the console + - In the Instana UI, you'll see HTTP and database spans, but no log spans + +### Scenario 2: Disable Log Spans Using Environment Variables + +In this scenario, we'll disable log spans using the `INSTANA_TRACING_DISABLE` environment variable. + +1. Revert any changes made to `main.go` for Scenario 1. + +2. Run the application with the environment variable: +```bash +INSTANA_TRACING_DISABLE=logging go run main.go +``` + +3. Expected output: + - The application will start and make requests to both endpoints + - You'll see log messages in the console + - In the Instana UI, you'll see HTTP and database spans, but no log spans + +### Scenario 3: Disable Log Spans Using Configuration File + +In this scenario, we'll disable log spans using a YAML configuration file. + +1. Ensure the `config.yaml` file contains: +```yaml +tracing: + disable: + - logging: true +``` + +2. Run the application with the configuration file path: +```bash +INSTANA_CONFIG_PATH=./config.yaml go run main.go +``` + +3. Expected output: + - The application will start and make requests to both endpoints + - You'll see log messages in the console + - In the Instana UI, you'll see HTTP and database spans, but no log spans + +### Scenario 4: Disable Log Spans Using Instana Agent Configuration + +In this scenario, we'll configure the Instana agent to disable log spans for all applications monitored by this agent. + +1. Locate your Instana agent configuration file: + +2. Add the following configuration to the agent's configuration file: +```yaml +com.instana.tracing: + disable: + - logging: true +``` +3. Restart the Instana agent. + +4. Run the application without any special configuration: +```bash +go run main.go +``` + +1. Expected output: + - The application will start and make requests to both endpoints + - You'll see log messages in the console + - In the Instana UI, you'll see HTTP and database spans, but no log spans + - All applications monitored by this agent will have log spans disabled + +## Verifying Results + +To verify that log spans are being disabled: + +1. Run the application without disabling log spans: +```bash +go run main.go +``` + +2. Check the Instana UI - you should see log spans along with HTTP and database spans. + +3. Run the application using one of the four methods to disable log spans. + +4. Recheck the Instana UI - you should see HTTP and database spans, but no log spans. + +## Priority Order + +When multiple configuration methods are used, they are applied in the following order of precedence: + +1. Configuration file (`INSTANA_CONFIG_PATH`) +2. Environment variable (`INSTANA_TRACING_DISABLE`) +3. Code-level configuration +4. Agent configuration diff --git a/example/disable-log-spans/config.yaml b/example/disable-log-spans/config.yaml new file mode 100644 index 000000000..a260cecfb --- /dev/null +++ b/example/disable-log-spans/config.yaml @@ -0,0 +1,3 @@ +tracing: + disable: + - logging: true diff --git a/example/disable-log-spans/go.mod b/example/disable-log-spans/go.mod new file mode 100644 index 000000000..f36672664 --- /dev/null +++ b/example/disable-log-spans/go.mod @@ -0,0 +1,28 @@ +module github.com/instana/go-sensor/example/disable_log_spans + +go 1.23.0 + +replace github.com/instana/go-sensor => ../../../go-sensor + +require ( + github.com/instana/go-sensor v1.70.0 + github.com/instana/go-sensor/instrumentation/instagorm v1.32.0 + github.com/instana/go-sensor/instrumentation/instalogrus v1.34.0 + github.com/sirupsen/logrus v1.9.3 + gorm.io/driver/sqlite v1.6.0 + gorm.io/gorm v1.31.0 +) + +require ( + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/looplab/fsm v1.0.3 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/stretchr/testify v1.10.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/example/disable-log-spans/go.sum b/example/disable-log-spans/go.sum new file mode 100644 index 000000000..3a4554b60 --- /dev/null +++ b/example/disable-log-spans/go.sum @@ -0,0 +1,44 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/instana/go-sensor/instrumentation/instagorm v1.32.0 h1:fjkWMVv7gAZQfQ5bq5jUNvOEGQmVw8EIExS+fYn0u0c= +github.com/instana/go-sensor/instrumentation/instagorm v1.32.0/go.mod h1:b5wq180zXUeoybZx0jm8MQEvpdIuVTng3tUfQXvAAHk= +github.com/instana/go-sensor/instrumentation/instalogrus v1.34.0 h1:0Q51nXZWeILrNKVcoUSP4IykuSW+DAQmsbRs2T/ad/M= +github.com/instana/go-sensor/instrumentation/instalogrus v1.34.0/go.mod h1:inMNLws8h6E+kHQzelvNmPr6AClopwi9mTqXJs1TOA8= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/looplab/fsm v1.0.3 h1:qtxBsa2onOs0qFOtkqwf5zE0uP0+Te+wlIvXctPKpcw= +github.com/looplab/fsm v1.0.3/go.mod h1:PmD3fFvQEIsjMEfvZdrCDZ6y8VwKTwWNjlpEr6IKPO4= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/example/disable-log-spans/main.go b/example/disable-log-spans/main.go new file mode 100644 index 000000000..03b0903bf --- /dev/null +++ b/example/disable-log-spans/main.go @@ -0,0 +1,181 @@ +// (c) Copyright IBM Corp. 2025 + +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "os/signal" + "sync" + "time" + + instana "github.com/instana/go-sensor" + "github.com/instana/go-sensor/instrumentation/instagorm" + "github.com/instana/go-sensor/instrumentation/instalogrus" + "github.com/sirupsen/logrus" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +var ( + c instana.TracerLogger + db *gorm.DB + once sync.Once +) + +func init() { + c = instana.InitCollector(&instana.Options{ + Service: "disable-log-example", + }) + connectDB() +} + +type student struct { + gorm.Model + Name string + RollNumber uint +} + +func agentReady() chan bool { + ch := make(chan bool) + + go func() { + for { + if instana.Ready() { + ch <- true + } + } + }() + + return ch +} + +func connectDB() { + var err error + dsn := "data.db" + once.Do(func() { + db, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{}) + if err != nil { + panic("failed to connect database: " + err.Error()) + } + }) + + instagorm.Instrument(db, c, dsn) +} + +// Handler for /start +func startHandler(w http.ResponseWriter, r *http.Request) { + logrus.WithContext(r.Context()).Info("Start API is called") + + db.Statement.Context = r.Context() + + if err := db.AutoMigrate(&student{}); err != nil { + logrus.WithContext(r.Context()).Error("failed to migrate the schema" + err.Error()) + } + + db.Create(&student{Name: "Alex", RollNumber: 32}) + fmt.Println("Student added to DB.") + + w.Write([]byte("Response from API 1")) +} + +// Handler for /error +func errorHandler(w http.ResponseWriter, r *http.Request) { + logrus.WithContext(r.Context()).Info("Error API is called") + + db.Statement.Context = r.Context() + + // Try inserting into a table that doesn't exist to cause an error + type unknownTable struct { + ID int + Name string + } + + err := db.Table("non_existent_table").Create(&unknownTable{ID: 1, Name: "Error"}).Error + if err != nil { + logrus.WithContext(r.Context()).Errorf("Expected DB error occurred: %v\n", err) + http.Error(w, "Database error occurred", http.StatusInternalServerError) + return + } + + w.Write([]byte("Should not reach here")) +} + +// Function that makes 2 calls to each API +func makeRequest() { + urls := []string{ + "http://localhost:8080/start", + "http://localhost:8080/start", + "http://localhost:8080/error", + "http://localhost:8080/error", + } + + var wg sync.WaitGroup + wg.Add(len(urls)) + + for i, url := range urls { + go func(i int, url string) { + defer wg.Done() + + resp, err := http.Get(url) + if err != nil { + log.Printf("Request %d to %s failed: %v\n", i+1, url, err) + return + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + log.Printf("Request %d to %s got response: %s\n", i+1, url, string(body)) + }(i, url) + + time.Sleep(time.Second) + } + + wg.Wait() +} + +func main() { + + <-agentReady() + + logrus.SetLevel(logrus.InfoLevel) + logrus.AddHook(instalogrus.NewHook(c)) + + // Register handlers + http.HandleFunc("/start", instana.TracingHandlerFunc(c, "/start", startHandler)) + http.HandleFunc("/error", instana.TracingHandlerFunc(c, "/error", errorHandler)) + + // Start server in a goroutine + go func() { + log.Println("Starting server on :8080") + log.Fatal(http.ListenAndServe(":8080", nil)) + }() + + // Wait a bit to ensure the server has started + time.Sleep(1 * time.Second) + + // Call the makeRequest function + makeRequest() + + // close db + if sqlDB, err := db.DB(); err == nil { + sqlDB.Close() + } + + err := os.Remove("data.db") + if err != nil { + fmt.Println("unable to delete the database file: ", "data.db", ": ", err.Error()) + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt) + + fmt.Println("type CTRL+C to exit") + // wait for Ctrl+C to exit + <-stop + fmt.Println("\nShutting down server...") + +} diff --git a/fsm.go b/fsm.go index 26112c80a..823b2ee46 100644 --- a/fsm.go +++ b/fsm.go @@ -234,9 +234,42 @@ func (r *fsmS) applyHostAgentSettings(resp agentResponse) { if len(sensor.options.Tracer.CollectableHTTPHeaders) == 0 { sensor.options.Tracer.CollectableHTTPHeaders = resp.getExtraHTTPHeaders() } + + r.applyDisableTracingConfig(resp) + r.logger.Debug("CollectableHTTPHeaders used: ", sensor.options.Tracer.CollectableHTTPHeaders) } +func (r *fsmS) applyDisableTracingConfig(resp agentResponse) { + // Do nothing if we have no configuration from the agent + if len(resp.Tracing.Disable) == 0 { + r.logger.Debug("No tracing disable configuration received from agent") + return + } + + // Check if INSTANA_TRACING_DISABLE environment variable or in-code configuration is set + // If it is, it takes precedence over agent configuration + isConfigSet := len(sensor.options.Tracer.DisableSpans) != 0 + if isConfigSet { + r.logger.Info("Disable Tracing configuration is set either through in-code or " + + "INSTANA_TRACING_DISABLE environment variable, ignoring agent disable configuration") + return + } + + r.logger.Debug("Applying tracing disable configuration from agent") + + // Apply the configuration from the agent + sensor.options.Tracer.DisableSpans = make(map[string]bool) + for _, item := range resp.Tracing.Disable { + for k, v := range item { + if v { + r.logger.Debug("Disabling tracing for: ", k) + sensor.options.Tracer.DisableSpans[k] = v + } + } + } +} + func (r *fsmS) announceSensor(_ context.Context, e *f.Event) { r.logger.Debug("announcing sensor to the agent") diff --git a/fsm_test.go b/fsm_test.go index 31698b3bd..6c8e69581 100644 --- a/fsm_test.go +++ b/fsm_test.go @@ -548,3 +548,90 @@ func Test_fsmS_applyHostAgentSettings_agent_secrets_not_valid_Error(t *testing.T assert.Equal(t, []string{"testHeader"}, sensor.options.Tracer.CollectableHTTPHeaders) } + +func TestApplyDisableTracingConfig(t *testing.T) { + tests := []struct { + name string + envVarSet bool + initialDisable map[string]bool + agentConfig []map[string]bool + expectedDisable map[string]bool + }{ + { + name: "Empty agent config", + agentConfig: []map[string]bool{}, + initialDisable: map[string]bool{}, + expectedDisable: map[string]bool{}, + }, + { + name: "in-code config exists", + agentConfig: []map[string]bool{ + {"logging": true}, + }, + initialDisable: map[string]bool{ + "logging": true, + }, + expectedDisable: map[string]bool{"logging": true}, + }, + { + name: "apply agent config", + agentConfig: []map[string]bool{ + {"logging": true}, + }, + initialDisable: map[string]bool{}, + expectedDisable: map[string]bool{"logging": true}, + }, + { + name: "env variable set", + envVarSet: true, + agentConfig: []map[string]bool{}, + initialDisable: map[string]bool{}, + expectedDisable: map[string]bool{"logging": true}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + if tt.envVarSet { + envVarValue := "logging" + os.Setenv("INSTANA_TRACING_DISABLE", envVarValue) + defer os.Unsetenv("INSTANA_TRACING_DISABLE") + } + + InitCollector(&Options{ + Tracer: TracerOptions{ + DisableSpans: tt.initialDisable, + }, + }) + defer ShutdownCollector() + + resp := agentResponse{} + resp.Tracing.Disable = tt.agentConfig + + testLogger := &testLogger{} + + fsm := &fsmS{ + logger: testLogger, + } + fsm.applyDisableTracingConfig(resp) + + // Check if the maps have the same size + assert.Equal(t, len(tt.expectedDisable), len(sensor.options.Tracer.DisableSpans), + "Expected map size %d, got %d", len(tt.expectedDisable), len(sensor.options.Tracer.DisableSpans)) + + // Check if all expected keys are present with correct values + for k, v := range tt.expectedDisable { + actualValue, exists := sensor.options.Tracer.DisableSpans[k] + assert.True(t, exists, "Expected key %s not found in result", k) + assert.Equal(t, v, actualValue, "Expected %s to be %v, got %v", k, v, actualValue) + } + + // Check if there are no unexpected keys + for k := range sensor.options.Tracer.DisableSpans { + _, exists := tt.expectedDisable[k] + assert.True(t, exists, "Unexpected key in result: %s", k) + } + }) + } +} diff --git a/go.mod b/go.mod index a10c94d0b..6109da61e 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,10 @@ require ( github.com/looplab/fsm v1.0.3 github.com/opentracing/opentracing-go v1.2.0 github.com/stretchr/testify v1.10.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/options.go b/options.go index 036074772..ee5e70f24 100644 --- a/options.go +++ b/options.go @@ -4,8 +4,13 @@ package instana import ( + "fmt" "os" + "path/filepath" "strconv" + "strings" + + "gopkg.in/yaml.v3" ) // Options allows the user to configure the to-be-initialized sensor @@ -116,5 +121,143 @@ func (opts *Options) setDefaults() { opts.Tracer.CollectableHTTPHeaders = parseInstanaExtraHTTPHeaders(collectableHeaders) } + // Check if INSTANA_CONFIG_PATH environment variable is set + if configPath, ok := os.LookupEnv("INSTANA_CONFIG_PATH"); ok { + if err := parseConfigFile(configPath, &opts.Tracer); err != nil { + defaultLogger.Warn("invalid INSTANA_CONFIG_PATH= env variable value: ", err, ", ignoring") + } + // else check if INSTANA_TRACING_DISABLE environment variable is set + } else if tracingDisable, ok := os.LookupEnv("INSTANA_TRACING_DISABLE"); ok { + parseInstanaTracingDisable(tracingDisable, &opts.Tracer) + } + opts.disableW3CTraceCorrelation = os.Getenv("INSTANA_DISABLE_W3C_TRACE_CORRELATION") != "" } + +// parseInstanaTracingDisable processes the INSTANA_TRACING_DISABLE environment variable value +// and updates the TracerOptions.Disable map accordingly. +// +// When the value is a boolean (true/false), the whole tracing feature is disabled/enabled. +// When a list of category or type names is specified, only those will be disabled. +// +// Examples: +// INSTANA_TRACING_DISABLE=true - disables all tracing +// INSTANA_TRACING_DISABLE="logging" - disables logging category +func parseInstanaTracingDisable(value string, opts *TracerOptions) { + // Initialize the Disable map if it doesn't exist + if opts.DisableSpans == nil { + opts.DisableSpans = make(map[string]bool) + } + + // Trim spaces from the value + value = strings.TrimSpace(value) + + // if it's a boolean value, disable all categories + if strings.EqualFold(value, "true") { + opts.DisableAllCategories() + return + } + + // if it's not a boolean value, process as a comma-separated list and disable each category. + items := strings.Split(value, ",") + for _, item := range items { + item = strings.TrimSpace(item) + if item != "" { + opts.DisableSpans[item] = true + } + } +} + +// parseConfigFile reads and parses the YAML configuration file at the given path +// and updates the TracerOptions accordingly. +// +// The YAML file must follow this format: +// tracing: +// disable: +// - logging: true + +func parseConfigFile(path string, opts *TracerOptions) error { + // Validate the file path and security considerations + absPath, err := validateFile(path) + if err != nil { + return fmt.Errorf("config file validation failed for %s: %w", path, err) + } + + // Read the file with proper error handling + data, err := os.ReadFile(absPath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + type Config struct { + Tracing struct { + Disable []map[string]bool `yaml:"disable"` + } `yaml:"tracing"` + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return fmt.Errorf("failed to parse YAML: %w", err) + } + + if opts.DisableSpans == nil { + opts.DisableSpans = make(map[string]bool) + } + + // Add the categories configured in the YAML file to the Disable map + for _, disableMap := range config.Tracing.Disable { + for category, enabled := range disableMap { + if enabled { + opts.DisableSpans[category] = true + } + } + + } + + return nil +} + +// validateFile ensures the given config file path is safe and usable. +// Security considerations: +// - Resolves symlinks to prevent symlink attacks +// - Ensures the path exists and is a regular file +// - Enforces a reasonable file size limit to avoid DoS +// - Warns if file permissions are too permissive (world-readable) +func validateFile(path string) (absPath string, err error) { + // Resolve symlinks to avoid symlink attacks + realPath, err := filepath.EvalSymlinks(path) + if err != nil { + return absPath, fmt.Errorf("failed to resolve config file path: %w", err) + } + + // Get absolute normalized path + absPath, err = filepath.Abs(realPath) + if err != nil { + return absPath, fmt.Errorf("failed to get absolute path: %w", err) + } + + // Check if the path exists and is a regular file + fileInfo, err := os.Stat(absPath) + if err != nil { + return absPath, fmt.Errorf("failed to access config file: %w", err) + } + + // Ensure it's a regular file, not a directory or special file + if !fileInfo.Mode().IsRegular() { + return absPath, fmt.Errorf("config path is not a regular file: %s", absPath) + } + + // Enforce a maximum file size (1MB is plenty for config files) + const maxFileSize = 1 * 1024 * 1024 // 1MB + if fileInfo.Size() > maxFileSize { + return absPath, fmt.Errorf("config file too large: %d bytes (max allowed: %d bytes)", + fileInfo.Size(), maxFileSize) + } + + // Warn if the file is world-readable (optional hardening) + if fileInfo.Mode().Perm()&0004 != 0 { + defaultLogger.Warn("config file is world-readable, consider restricting permissions: ", absPath) + } + + return absPath, nil +} diff --git a/options_test.go b/options_test.go index f374e6f87..2eda8bdb3 100644 --- a/options_test.go +++ b/options_test.go @@ -4,7 +4,10 @@ package instana import ( + "fmt" "os" + "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -123,3 +126,339 @@ func restoreEnvVarFunc(key string) func() { return func() { os.Unsetenv(key) } } + +func TestParseInstanaTracingDisable(t *testing.T) { + tests := []struct { + name string + value string + expected map[string]bool + }{ + { + name: "Empty value", + value: "", + expected: map[string]bool{}, + }, + { + name: "Boolean true", + value: "True", + expected: map[string]bool{ + "logging": true, + }, + }, + { + name: "With extra spaces", + value: " logging ", + expected: map[string]bool{ + "logging": true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := TracerOptions{} + parseInstanaTracingDisable(tt.value, &opts) + + // Check if the maps have the same size + if len(opts.DisableSpans) != len(tt.expected) { + t.Errorf("Expected map size %d, got %d", len(tt.expected), len(opts.DisableSpans)) + } + + // Check if all expected keys are present with correct values + for k, v := range tt.expected { + if opts.DisableSpans[k] != v { + t.Errorf("Expected %s to be %v, got %v", k, v, opts.DisableSpans[k]) + } + } + + // Check if there are no unexpected keys + for k := range opts.DisableSpans { + if _, exists := tt.expected[k]; !exists { + t.Errorf("Unexpected key in result: %s", k) + } + } + }) + } +} + +func TestInstanaTracingDisableEnvVar(t *testing.T) { + tests := []struct { + name string + envValue string + expected map[string]bool + }{ + { + name: "No env var", + envValue: "", + expected: map[string]bool{}, + }, + { + name: "DisableSpans all", + envValue: "True", + expected: map[string]bool{ + "logging": true, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + if tt.envValue != "" { + t.Setenv("INSTANA_TRACING_DISABLE", tt.envValue) + } + + opts := DefaultOptions() + + // Check if the maps have the expected values + for k, v := range tt.expected { + if opts.Tracer.DisableSpans[k] != v { + t.Errorf("Expected %s to be %v, got %v", k, v, opts.Tracer.DisableSpans[k]) + } + } + }) + } +} + +func TestConfigFileHandling(t *testing.T) { + tests := []struct { + name string + yamlContent string + useEnvVar bool + invalidConfigPath bool + expectedError bool + expectedDisabled []string + }{ + { + name: "Basic config file parsing", + yamlContent: `tracing: + disable: + - logging: true +`, + useEnvVar: false, + expectedError: false, + expectedDisabled: []string{"logging"}, + }, + { + name: "Config file parsing with environment variable", + yamlContent: `tracing: + disable: + - logging: true +`, + useEnvVar: true, + expectedError: false, + expectedDisabled: []string{"logging"}, + }, + { + name: "Invalid YAML handling", + yamlContent: `tracing: + disable: + - logging: true + - invalid indentation +`, + useEnvVar: false, + expectedError: true, + expectedDisabled: []string{}, + }, + { + name: "Invalid config file path", + yamlContent: `tracing: + disable: + - logging: true +`, + useEnvVar: true, + invalidConfigPath: true, + expectedError: true, + expectedDisabled: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "config.yaml") + + err := os.WriteFile(configPath, []byte(tt.yamlContent), 0644) + if err != nil { + t.Fatalf("Failed to create test config file: %v", err) + } + + if tt.useEnvVar { + if tt.invalidConfigPath { + t.Setenv("INSTANA_CONFIG_PATH", "/invalid/path") + } else { + t.Setenv("INSTANA_CONFIG_PATH", configPath) + } + + opts := &Options{ + Tracer: TracerOptions{}, + } + opts.setDefaults() + + verifyDisabledCategories(t, opts.Tracer.DisableSpans, tt.expectedDisabled) + } else { + opts := &TracerOptions{ + DisableSpans: make(map[string]bool), + } + + err = parseConfigFile(configPath, opts) + + if (err != nil) != tt.expectedError { + if tt.expectedError { + t.Error("Expected an error, but didn't get one") + } else { + t.Errorf("Got unexpected error: %v", err) + } + } + + // Only verify disabled categories if no error was expected + if !tt.expectedError { + verifyDisabledCategories(t, opts.DisableSpans, tt.expectedDisabled) + } + } + }) + } +} + +// verifyDisabledCategories checks that expected categories are disabled +func verifyDisabledCategories(t *testing.T, disableMap map[string]bool, expectedDisabled []string) { + t.Helper() + + for _, category := range expectedDisabled { + if !disableMap[category] { + t.Errorf("Expected category %q to be disabled, but it wasn't", category) + } + } +} + +func TestValidateFile(t *testing.T) { + tempDir := t.TempDir() + + validFilePath := filepath.Join(tempDir, "valid.txt") + err := os.WriteFile(validFilePath, []byte("test content"), 0644) + if err != nil { + t.Fatalf("Failed to create test file: %v", err) + } + + tests := []struct { + name string + getPathFn func() (string, error) + expectedError bool + errorContains string + }{ + { + name: "Valid file", + getPathFn: func() (string, error) { + return validFilePath, nil + }, + expectedError: false, + }, + { + name: "Non-existent file", + getPathFn: func() (string, error) { + return filepath.Join(tempDir, "nonexistent.txt"), nil + }, + expectedError: true, + errorContains: "no such file or directory", + }, + { + name: "Symlink to valid file", + getPathFn: func() (string, error) { + symlinkPath := filepath.Join(tempDir, "symlink.txt") + err = os.Symlink(validFilePath, symlinkPath) + if err != nil { + return "", fmt.Errorf("Skipping symlink test, could not create symlink: %v", err) + } + return symlinkPath, nil + }, + expectedError: false, + }, + { + name: "Directory instead of file", + getPathFn: func() (string, error) { + dirPath := filepath.Join(tempDir, "testdir") + err = os.Mkdir(dirPath, 0755) + if err != nil { + return "", fmt.Errorf("Failed to create test directory: %v", err) + } + return dirPath, nil + }, + expectedError: true, + errorContains: "not a regular file", + }, + { + name: "File too large", + getPathFn: func() (string, error) { + fpath := filepath.Join(tempDir, "big.conf") + f, err := os.Create(fpath) + if err != nil { + return "", fmt.Errorf("Failed to create test file: %v", err) + } + defer f.Close() + err = f.Truncate(1024*1024 + 1) // >1MB + if err != nil { + return "", fmt.Errorf("Failed to truncate test file: %v", err) + } + return fpath, nil + }, + expectedError: true, + errorContains: "config file too large", + }, + { + name: "World-readable file", + getPathFn: func() (string, error) { + worldReadablePath := filepath.Join(tempDir, "world-readable.txt") + err = os.WriteFile(worldReadablePath, []byte("world-readable content"), 0644) + if err != nil { + return "", fmt.Errorf("Failed to create world-readable test file: %v", err) + } + return worldReadablePath, nil + }, + expectedError: false, // This should not error, but will log a warning + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path, err := tt.getPathFn() + if err != nil { + t.Skip(err) + } + absPath, err := validateFile(path) + + if (err != nil) != tt.expectedError { + if tt.expectedError { + t.Errorf("Expected error but got none") + } else { + t.Errorf("Expected no error but got: %v", err) + } + } + + // If am error is expected, check that it contains the expected text + if tt.expectedError && err != nil && tt.errorContains != "" { + if !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Error message '%s' does not contain '%s'", err.Error(), tt.errorContains) + } + } + + // For successful cases, check that the returned path is absolute + if !tt.expectedError && err == nil { + if !filepath.IsAbs(absPath) { + t.Errorf("Expected absolute path, got: %s", absPath) + } + + // For the symlink case, verify it was resolved + if tt.name == "Symlink to valid file" { + // The resolved path should be an absolute path to the target file + // Note: On macOS, /var/folders may resolve to /private/var/folders + // so just check that the base filename matches + if filepath.Base(absPath) != filepath.Base(validFilePath) { + t.Errorf("Symlink not properly resolved. Got: %s, Expected file with basename: %s", + absPath, filepath.Base(validFilePath)) + } + } + } + }) + } +} diff --git a/span.go b/span.go index 1bbb1f375..f3faee6d5 100644 --- a/span.go +++ b/span.go @@ -19,6 +19,13 @@ const minSpanLogLevel = logger.WarnLevel var _ ot.Span = (*spanS)(nil) +type spanCategory string + +const ( + logging spanCategory = "logging" + unknown spanCategory = "unknown" +) + type spanS struct { Service string Operation string @@ -89,6 +96,11 @@ func (r *spanS) FinishWithOptions(opts ot.FinishOptions) { } func (r *spanS) sendSpanToAgent() bool { + // Span shouldn't be forwarded if the span category is configured as disabled + if r.getSpanCategory().disabled() { + return false + } + //if suppress tag is present, span shouldn't be forwarded if r.context.Suppressed { return false @@ -215,6 +227,10 @@ func (r *spanS) Tracer() ot.Tracer { // sendOpenTracingLogRecords converts OpenTracing log records that contain errors // to Instana log spans and sends them to the agent func (r *spanS) sendOpenTracingLogRecords() { + if logging.disabled() { + return + } + for _, lr := range r.Logs { r.sendOpenTracingLogRecord(lr) } @@ -275,3 +291,31 @@ func openTracingLogFieldLevel(lf otlog.Field) logger.Level { return logger.DebugLevel } } + +func (c spanCategory) string() string { + return string(c) +} + +func (r *spanS) getSpanCategory() spanCategory { + // return span category if it is a registered span type + switch RegisteredSpanType(r.Operation) { + case LogSpanType: + return logging + default: + return unknown + } +} + +func (c spanCategory) disabled() bool { + // unrecognized categories are always enabled + if c == unknown { + return false + } + + // Check if sensor or options are nil + if sensor == nil || sensor.options.Tracer.DisableSpans == nil { + return false + } + + return sensor.options.Tracer.DisableSpans[c.string()] +} diff --git a/span_test.go b/span_test.go index b087b5faa..e01e161a1 100644 --- a/span_test.go +++ b/span_test.go @@ -6,6 +6,7 @@ package instana_test import ( "errors" "os" + "path/filepath" "testing" "time" @@ -477,3 +478,177 @@ func Test_tracerS_SuppressTracing(t *testing.T) { }) } } + +// TestDisableSpanCategoryFromConfig tests that log spans are properly filtered when disabled via in-code configuration +func TestDisableSpanCategory(t *testing.T) { + tests := []struct { + name string + disabledCategory string + spanOperations []string + expectedRecorded []bool + }{ + { + name: "Disable logging category", + disabledCategory: "logging", + spanOperations: []string{string(instana.LogSpanType), string(instana.HTTPServerSpanType), string(instana.KafkaSpanType)}, + expectedRecorded: []bool{false, true, true}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + recorder := instana.NewTestRecorder() + + opts := &instana.Options{ + Tracer: instana.TracerOptions{ + DisableSpans: map[string]bool{ + tt.disabledCategory: true, + }, + }, + Recorder: recorder, + AgentClient: &alwaysReadyClient{}, + } + + c := instana.InitCollector(opts) + defer instana.ShutdownCollector() + + for _, operation := range tt.spanOperations { + span := c.StartSpan(operation) + span.Finish() + } + + spans := recorder.GetQueuedSpans() + + assert.Equal(t, 2, len(spans), + "Expected number of recorded spans doesn't match") + + // Create a map of recorded operations for easier lookup + recordedOps := make(map[string]bool) + for _, span := range spans { + recordedOps[span.Name] = true + } + + // Verify each span was recorded or not as expected + for i, operation := range tt.spanOperations { + if tt.expectedRecorded[i] { + assert.True(t, recordedOps[operation], + "Expected operation %s to be recorded but it wasn't", operation) + } else { + assert.False(t, recordedOps[operation], + "Expected operation %s to be filtered out but it was recorded", operation) + } + } + }) + } +} + +// TestDisableSpanCategoryFromConfig tests that log spans are properly filtered when disabled via configuration file +func TestDisableSpanCategoryFromConfig(t *testing.T) { + + tempDir := t.TempDir() + configPath := filepath.Join(tempDir, "config.yaml") + + yamlContent := `tracing: + disable: + - logging: true +` + err := os.WriteFile(configPath, []byte(yamlContent), 0644) + require.NoError(t, err, "Failed to create test config file") + + t.Setenv("INSTANA_CONFIG_PATH", configPath) + + recorder := instana.NewTestRecorder() + + opts := &instana.Options{ + Recorder: recorder, + AgentClient: alwaysReadyClient{}, + } + + c := instana.InitCollector(opts) + defer instana.ShutdownCollector() + + operations := []string{ + string(instana.HTTPServerSpanType), + string(instana.KafkaSpanType), + string(instana.LogSpanType), + } + + // Expected recording status for each operation above + expectedRecorded := []bool{true, true, false} + + for _, operation := range operations { + span := c.StartSpan(operation) + span.Finish() + } + + spans := recorder.GetQueuedSpans() + + // Verify that spans of the disabled categories were not recorded + assert.Equal(t, 2, len(spans), + "Expected number of recorded spans doesn't match") + + // Create a map of recorded operations for easier lookup + recordedOps := make(map[string]bool) + for _, span := range spans { + recordedOps[span.Name] = true + } + + // Verify each span was recorded or not as expected + for i, operation := range operations { + if expectedRecorded[i] { + assert.True(t, recordedOps[operation], + "Expected operation %s to be recorded but it wasn't", operation) + } else { + assert.False(t, recordedOps[operation], + "Expected operation %s to be filtered out but it was recorded", operation) + } + } +} + +// TestDisableAllCategories tests that log spans are filtered when logging category is disabled via environment variable +func TestDisableAllCategories(t *testing.T) { + t.Setenv("INSTANA_TRACING_DISABLE", "logging") + + recorder := instana.NewTestRecorder() + // Initialize tracer with all categories disabled (which now only includes logging) + opts := &instana.Options{ + Tracer: instana.TracerOptions{}, + Recorder: recorder, + AgentClient: alwaysReadyClient{}, + } + + tracer := instana.InitCollector(opts) + defer instana.ShutdownCollector() + + operations := []string{ + string(instana.HTTPServerSpanType), + string(instana.KafkaSpanType), + string(instana.LogSpanType), + } + + for _, operation := range operations { + span := tracer.StartSpan(operation) + span.Finish() + } + + spans := recorder.GetQueuedSpans() + + // Verify that only non-logging spans were recorded + assert.Equal(t, 2, len(spans), "Expected only non-logging spans to be recorded") + + // Create a map of recorded operations for easier lookup + recordedOps := make(map[string]bool) + for _, span := range spans { + recordedOps[span.Name] = true + } + + // Verify log spans were not recorded + assert.False(t, recordedOps[string(instana.LogSpanType)], + "Expected log spans to be filtered out but they were recorded") + + // Verify other spans were recorded + assert.True(t, recordedOps[string(instana.HTTPServerSpanType)], + "Expected HTTP spans to be recorded") + assert.True(t, recordedOps[string(instana.KafkaSpanType)], + "Expected Kafka spans to be recorded") +} diff --git a/tracer_options.go b/tracer_options.go index 30f00dbed..8949b49de 100644 --- a/tracer_options.go +++ b/tracer_options.go @@ -22,6 +22,11 @@ type TracerOptions struct { // See https://www.instana.com/docs/setup_and_manage/host_agent/configuration/#capture-custom-http-headers for details CollectableHTTPHeaders []string + // Disable allows the exclusion of specific traces or calls from tracing based on the category (technology) + // or type (frameworks, libraries, instrumentations) supported by the traces. + // The main benefit of disabling is reducing the overall amount of data being collected and processed. + DisableSpans map[string]bool + // tracerDefaultSecrets flag is used to identify whether tracerOptions.Secrets // contains the default secret matcher configured by the Instana SDK. // @@ -43,3 +48,9 @@ func DefaultTracerOptions() TracerOptions { Secrets: DefaultSecretsMatcher(), } } + +func (opts *TracerOptions) DisableAllCategories() { + opts.DisableSpans = map[string]bool{ + logging.string(): true, + } +}