github.com/waldiirawan/apm-agent-go/v2@v2.2.2/config_test.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package apm_test 19 20 import ( 21 "context" 22 "encoding/json" 23 "fmt" 24 "io/ioutil" 25 "net/http" 26 "net/http/httptest" 27 "net/url" 28 "os" 29 "path/filepath" 30 "strconv" 31 "strings" 32 "testing" 33 "time" 34 35 "github.com/pkg/errors" 36 "github.com/stretchr/testify/assert" 37 "github.com/stretchr/testify/require" 38 39 "github.com/waldiirawan/apm-agent-go/v2" 40 "github.com/waldiirawan/apm-agent-go/v2/apmconfig" 41 "github.com/waldiirawan/apm-agent-go/v2/apmtest" 42 "github.com/waldiirawan/apm-agent-go/v2/internal/apmlog" 43 "github.com/waldiirawan/apm-agent-go/v2/transport" 44 "github.com/waldiirawan/apm-agent-go/v2/transport/transporttest" 45 ) 46 47 func TestTracerCentralConfigUpdate(t *testing.T) { 48 run := func(configKey, configValue string, isRemote func(*apmtest.RecordingTracer) bool) { 49 t.Run(configKey, func(t *testing.T) { 50 response, _ := json.Marshal(map[string]string{configKey: configValue}) 51 testTracerCentralConfigUpdate(t, apmtest.NewTestLogger(t), string(response), isRemote) 52 }) 53 } 54 run("transaction_sample_rate", "0", func(tracer *apmtest.RecordingTracer) bool { 55 return !tracer.StartTransaction("name", "type").Sampled() 56 }) 57 run("transaction_max_spans", "0", func(tracer *apmtest.RecordingTracer) bool { 58 return tracer.StartTransaction("name", "type").StartSpan("name", "type", nil).Dropped() 59 }) 60 run("exit_span_min_duration", "10ms", func(tracer *apmtest.RecordingTracer) bool { 61 tracer.ResetPayloads() 62 63 tx := tracer.StartTransaction("name", "type") 64 span := tx.StartSpanOptions("name", "type", apm.SpanOptions{ExitSpan: true}) 65 span.Duration = 9 * time.Millisecond 66 span.End() 67 tx.End() 68 69 tracer.Flush(nil) 70 payloads := tracer.Payloads() 71 txs := payloads.Transactions 72 require.Len(t, txs, 1) 73 return txs[0].SpanCount.Dropped == 1 && len(payloads.Spans) == 0 && 74 len(txs[0].DroppedSpansStats) == 1 75 }) 76 run("capture_body", "all", func(tracer *apmtest.RecordingTracer) bool { 77 req, _ := http.NewRequest("POST", "/", strings.NewReader("...")) 78 capturer := tracer.CaptureHTTPRequestBody(req) 79 return capturer != nil 80 }) 81 run("recording", "false", func(tracer *apmtest.RecordingTracer) bool { 82 return !tracer.Recording() 83 }) 84 run("span_stack_trace_min_duration", "1ms", func(tracer *apmtest.RecordingTracer) bool { 85 tracer.ResetPayloads() 86 87 tx := tracer.StartTransaction("name", "type") 88 span := tx.StartSpan("name", "type", nil) 89 span.Duration = 1 * time.Millisecond 90 span.End() 91 tx.End() 92 93 tracer.Flush(nil) 94 payloads := tracer.Payloads() 95 assert.Len(t, payloads.Spans, 1) 96 return len(payloads.Spans[0].Stacktrace) > 0 97 }) 98 run("stack_trace_limit", "1", func(tracer *apmtest.RecordingTracer) bool { 99 tracer.ResetPayloads() 100 tracer.NewError(errors.New("boom")).Send() 101 tracer.Flush(nil) 102 payloads := tracer.Payloads() 103 assert.Len(t, payloads.Errors, 1) 104 return len(payloads.Errors[0].Exception.Stacktrace) == 1 105 }) 106 run("sanitize_field_names", "secret", func(tracer *apmtest.RecordingTracer) bool { 107 tracer.ResetPayloads() 108 tracer.SetSanitizedFieldNames("not_secret") 109 req, _ := http.NewRequest("GET", "http://server.testing/", nil) 110 req.AddCookie(&http.Cookie{Name: "secret", Value: "top"}) 111 tx := tracer.StartTransaction("name", "type") 112 tx.Context.SetHTTPRequest(req) 113 tx.End() 114 tracer.Flush(nil) 115 payloads := tracer.Payloads() 116 assert.Len(t, payloads.Transactions, 1) 117 assert.Len(t, payloads.Transactions[0].Context.Request.Cookies, 1) 118 return payloads.Transactions[0].Context.Request.Cookies[0].Value == "[REDACTED]" 119 }) 120 t.Run("log_level", func(t *testing.T) { 121 tempdir, err := ioutil.TempDir("", "apmtest_log_level") 122 require.NoError(t, err) 123 defer os.RemoveAll(tempdir) 124 125 logfile := filepath.Join(tempdir, "apm.log") 126 os.Setenv(apmlog.EnvLogFile, logfile) 127 os.Setenv(apmlog.EnvLogLevel, "off") 128 defer os.Unsetenv(apmlog.EnvLogFile) 129 defer os.Unsetenv(apmlog.EnvLogLevel) 130 131 response, _ := json.Marshal(map[string]string{"log_level": "debug"}) 132 testTracerCentralConfigUpdate(t, apmlog.DefaultLogger(), string(response), func(tracer *apmtest.RecordingTracer) bool { 133 require.NoError(t, os.Truncate(logfile, 0)) 134 tracer.StartTransaction("name", "type").End() 135 tracer.Flush(nil) 136 log, err := ioutil.ReadFile(logfile) 137 require.NoError(t, err) 138 return len(log) > 0 139 }) 140 }) 141 run("transaction_ignore_urls", "*", func(tracer *apmtest.RecordingTracer) bool { 142 u, err := url.Parse("http://testing.invalid/") 143 require.NoError(t, err) 144 return tracer.IgnoredTransactionURL(u) 145 }) 146 run("trace_continuation_strategy", "restart", func(tracer *apmtest.RecordingTracer) bool { 147 tracer.ResetPayloads() 148 149 traceID := apm.TraceID{1} 150 151 tx := tracer.StartTransactionOptions("name", "type", apm.TransactionOptions{TraceContext: apm.TraceContext{ 152 Trace: traceID, 153 Span: apm.SpanID{2}, 154 }}) 155 tx.End() 156 157 tracer.Flush(nil) 158 payloads := tracer.Payloads() 159 txs := payloads.Transactions 160 require.Len(t, txs, 1) 161 162 return apm.TraceID(txs[0].TraceID) != traceID && len(txs[0].Links) == 1 && apm.TraceID(txs[0].Links[0].TraceID) == traceID 163 }) 164 run("span_compression_enabled", "false", func(tracer *apmtest.RecordingTracer) bool { 165 tracer.ResetPayloads() 166 tx := tracer.StartTransaction("name", "type") 167 exitSpanOpts := apm.SpanOptions{ExitSpan: true} 168 for i := 0; i < 2; i++ { 169 span := tx.StartSpanOptions("name", "request", exitSpanOpts) 170 span.Duration = 50 * time.Millisecond 171 span.End() 172 } 173 tx.End() 174 tracer.Flush(nil) 175 return len(tracer.Payloads().Spans) == 2 176 }) 177 run("span_compression_exact_match_max_duration", "100ms", func(tracer *apmtest.RecordingTracer) bool { 178 tracer.ResetPayloads() 179 tx := tracer.StartTransaction("name", "type") 180 exitSpanOpts := apm.SpanOptions{ExitSpan: true} 181 for i := 0; i < 2; i++ { 182 span := tx.StartSpanOptions("name", "request", exitSpanOpts) 183 span.Duration = 100 * time.Millisecond 184 span.End() 185 } 186 // Third span does not get compressed since it exceeds the max duration. 187 span := tx.StartSpanOptions("name", "request", exitSpanOpts) 188 span.Duration = 101 * time.Millisecond 189 span.End() 190 tx.End() 191 tracer.Flush(nil) 192 return len(tracer.Payloads().Spans) == 2 193 }) 194 run("span_compression_same_kind_max_duration", "10ms", func(tracer *apmtest.RecordingTracer) bool { 195 tracer.ResetPayloads() 196 tx := tracer.StartTransaction("name", "type") 197 exitSpanOpts := apm.SpanOptions{ExitSpan: true} 198 for i := 0; i < 2; i++ { 199 span := tx.StartSpanOptions(fmt.Sprint(i), "request", exitSpanOpts) 200 span.Duration = 10 * time.Millisecond 201 span.End() 202 } 203 // Third span does not get compressed since it exceeds the max duration. 204 span := tx.StartSpanOptions("name", "request", exitSpanOpts) 205 span.Duration = 11 * time.Millisecond 206 span.End() 207 tx.End() 208 tracer.Flush(nil) 209 return len(tracer.Payloads().Spans) == 2 210 }) 211 } 212 213 func testTracerCentralConfigUpdate(t *testing.T, logger apm.Logger, serverResponse string, isRemote func(*apmtest.RecordingTracer) bool) { 214 type response struct { 215 etag string 216 body string 217 } 218 responses := make(chan response) 219 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 220 if req.URL.Path == "/" { 221 return 222 } 223 assert.Equal(t, "/config/v1/agents", req.URL.Path) 224 w.Header().Set("Cache-Control", "max-age=1") 225 select { 226 case response := <-responses: 227 w.Header().Set("Etag", strconv.Quote(response.etag)) 228 w.Write([]byte(response.body)) 229 case <-req.Context().Done(): 230 } 231 })) 232 defer server.Close() 233 serverURL, _ := url.Parse(server.URL) 234 235 httpTransport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{ServerURLs: []*url.URL{serverURL}}) 236 require.NoError(t, err) 237 238 tracer := &apmtest.RecordingTracer{} 239 var testTransport struct { 240 apmconfig.Watcher 241 *transporttest.RecorderTransport 242 } 243 testTransport.Watcher = httpTransport 244 testTransport.RecorderTransport = &tracer.RecorderTransport 245 246 tracer.Tracer, err = apm.NewTracerOptions(apm.TracerOptions{Transport: &testTransport}) 247 require.NoError(t, err) 248 defer tracer.Tracer.Close() 249 250 // This test can be run in parallel with others after creating the tracer, 251 // but not before, because we depend on NewTracerOptions picking up default 252 // configuration. 253 t.Parallel() 254 255 tracer.SetLogger(logger) 256 assert.False(t, isRemote(tracer)) 257 258 timeout := time.After(10 * time.Second) 259 260 // We each response payload twice, which causes us to block until 261 // the first one is fully consumed. 262 for i := 0; i < 2; i++ { 263 select { 264 case responses <- response{etag: "foo", body: serverResponse}: 265 case <-timeout: 266 t.Fatal("timed out waiting for config update") 267 } 268 } 269 assert.True(t, isRemote(tracer)) 270 271 for i := 0; i < 2; i++ { 272 select { 273 case responses <- response{etag: "bar", body: "{}"}: 274 case <-timeout: 275 t.Fatal("timed out waiting for config update") 276 } 277 } 278 assert.False(t, isRemote(tracer)) 279 } 280 281 func TestTracerCentralConfigUpdateDisabled(t *testing.T) { 282 responded := make(chan struct{}) 283 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 284 if req.URL.Path != "/config/v1/agents" { 285 return 286 } 287 select { 288 case responded <- struct{}{}: 289 case <-req.Context().Done(): 290 } 291 })) 292 defer server.Close() 293 294 os.Setenv("ELASTIC_APM_SERVER_URL", server.URL) 295 defer os.Unsetenv("ELASTIC_APM_SERVER_URL") 296 297 os.Setenv("ELASTIC_APM_CENTRAL_CONFIG", "false") 298 defer os.Unsetenv("ELASTIC_APM_CENTRAL_CONFIG") 299 300 httpTransport, err := transport.NewHTTPTransport(transport.HTTPTransportOptions{}) 301 require.NoError(t, err) 302 tracer, err := apm.NewTracerOptions(apm.TracerOptions{Transport: httpTransport}) 303 require.NoError(t, err) 304 defer tracer.Close() 305 tracer.SetLogger(apmtest.NewTestLogger(t)) 306 307 select { 308 case <-responded: 309 t.Fatal("unexpected config watcher response") 310 case <-time.After(2 * time.Second): 311 } 312 } 313 314 func TestTracerSetConfigWatcher(t *testing.T) { 315 watcherClosed := make(chan struct{}) 316 watcherFunc := apmtest.WatchConfigFunc(func(ctx context.Context, params apmconfig.WatchParams) <-chan apmconfig.Change { 317 changes := make(chan apmconfig.Change) 318 go func() { 319 <-ctx.Done() 320 close(watcherClosed) 321 }() 322 return changes 323 }) 324 325 tracer, err := apm.NewTracer("", "") 326 require.NoError(t, err) 327 defer tracer.Close() 328 329 tracer.SetLogger(apmtest.NewTestLogger(t)) 330 tracer.SetConfigWatcher(watcherFunc) 331 tracer.SetConfigWatcher(nil) 332 select { 333 case <-watcherClosed: 334 case <-time.After(10 * time.Second): 335 t.Fatal("timed out waiting for watcher context to be cancelled") 336 } 337 } 338 339 func TestTracerConfigWatcherPrecedence(t *testing.T) { 340 watcherFunc := apmtest.WatchConfigFunc(func(ctx context.Context, params apmconfig.WatchParams) <-chan apmconfig.Change { 341 changes := make(chan apmconfig.Change) 342 go func() { 343 select { 344 case changes <- apmconfig.Change{ 345 Attrs: map[string]string{"transaction_sample_rate": "0"}, 346 }: 347 case <-ctx.Done(): 348 } 349 }() 350 return changes 351 }) 352 tracer, err := apm.NewTracer("", "") 353 require.NoError(t, err) 354 defer tracer.Close() 355 356 tracer.SetLogger(apmtest.NewTestLogger(t)) 357 tracer.SetConfigWatcher(watcherFunc) 358 timeout := time.After(10 * time.Second) 359 for { 360 sampled := tracer.StartTransaction("name", "type").Sampled() 361 if !sampled { 362 // Updated 363 break 364 } 365 select { 366 case <-time.After(10 * time.Millisecond): 367 case <-timeout: 368 t.Fatal("timed out waiting for config update") 369 } 370 } 371 372 // Setting a sampler locally will have no effect while there is remote 373 // configuration in place. 374 tracer.SetSampler(apm.NewRatioSampler(1)) 375 sampled := tracer.StartTransaction("name", "type").Sampled() 376 assert.False(t, sampled) 377 378 // Disable remote config, which also reverts to local config. 379 tracer.SetConfigWatcher(nil) 380 for { 381 sampled := tracer.StartTransaction("name", "type").Sampled() 382 if sampled { 383 // Reverted 384 break 385 } 386 select { 387 case <-time.After(10 * time.Millisecond): 388 case <-timeout: 389 t.Fatal("timed out waiting for config to revert to local") 390 } 391 } 392 }