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  }