├── .github └── workflows │ └── build.yml ├── .gitignore ├── CODEOWNERS ├── LICENSE ├── README.md ├── agent ├── agent.go ├── agent_test.go ├── reporter.go ├── reporter_test.go ├── warmup.go ├── warmup_test.go ├── wrapper.go └── wrapper_test.go ├── application ├── application_support.go ├── application_test.go ├── constants.go └── utils.go ├── config ├── config.go └── config_test.go ├── constants ├── integrations.go ├── lambda.go └── tconfig.go ├── docker-compose.yml ├── ext ├── constants.go └── span_options.go ├── go.mod ├── invocation ├── constants.go ├── data.go ├── invocation.go ├── invocation_support.go ├── invocation_test.go ├── invocation_trace_support.go ├── invocation_trace_support_test.go ├── testdata │ ├── alb-lambda-target-request-headers-only.json │ ├── api-gw-missing-fields.json │ ├── api-gw.json │ ├── apigw-request-missing-fields.json │ ├── apigw-request.json │ ├── cf-event.json │ ├── cloudwatch-logs-event.json │ ├── dynamodb-event-malformed.json │ ├── dynamodb-event-wrong-arn.json │ ├── dynamodb-event.json │ ├── kinesis-event.json │ ├── kinesis-firehose-event.json │ ├── s3-event.json │ ├── schedule-event.json │ ├── sns-event.json │ └── sqs-event.json ├── trigger_events.go └── trigger_events_test.go ├── log ├── constants.go ├── data.go ├── log.go ├── log_support.go ├── tlogger.go └── tlogger_test.go ├── metric ├── constants.go ├── cpu.go ├── cpu_test.go ├── cpu_times.go ├── data.go ├── disk.go ├── gc.go ├── gc_test.go ├── goroutine.go ├── goroutine_test.go ├── heap.go ├── heap_test.go ├── memory.go ├── memory_test.go ├── metric.go ├── metric_support.go ├── metric_test.go └── net.go ├── plugin ├── data.go └── plugin.go ├── samplers ├── composite_sampler.go ├── composite_sampler_test.go ├── count_aware_sampler.go ├── count_aware_sampler_test.go ├── duration_aware_sampler.go ├── duration_aware_sampler_test.go ├── error_aware_sampler.go ├── error_aware_sampler_test.go ├── sampler.go ├── time_aware_sampler.go └── time_aware_sampler_test.go ├── test └── util.go ├── thundra └── thundra.go ├── trace ├── constants.go ├── data.go ├── erroneous.go ├── trace.go ├── trace_integration_test.go ├── trace_opentracing_test.go └── trace_support.go ├── tracer ├── error_injector_span_listener.go ├── error_injector_span_listener_test.go ├── filtering_span_listener.go ├── filtering_span_listener_test.go ├── latency_injector_span_listener.go ├── latency_injector_span_listener_test.go ├── raw_span.go ├── recorder.go ├── security_aware_span_listener.go ├── security_aware_span_listener_test.go ├── span.go ├── span_context.go ├── span_filter.go ├── span_listener.go ├── span_test.go ├── tag_injector_span_listener.go ├── tracer.go ├── tracer_support.go └── tracer_test.go ├── utils └── utils.go └── wrappers ├── apexgateway └── thundra_apex_gateway.go ├── apexgatewayv2 └── thundra_apex_gateway_v2.go ├── aws ├── athena.go ├── aws.go ├── aws_operation_types.go ├── aws_test.go ├── default.go ├── dynamodb.go ├── firehose.go ├── integration.go ├── kinesis.go ├── lambda.go ├── s3.go ├── ses.go ├── sns.go └── sqs.go ├── elastic ├── integration.go └── olivere │ ├── elastic.go │ └── elastic_test.go ├── http ├── client.go └── client_test.go ├── mongodb ├── mongodb.go └── mongodb_test.go ├── rdb ├── mysql.go ├── mysql_test.go ├── postgresql.go ├── postgresql_test.go ├── rdb.go └── rdb_test.go └── redis ├── go-redis ├── client.go └── client_test.go ├── integration.go └── redigo ├── redigo.go └── redigo_test.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI Check 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: "1.11" 20 | - name: Start Docker Images 21 | run: docker-compose up -d --quiet-pull --no-recreate 22 | - name: Run Tests 23 | run: go test -v ./... 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | TODO -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | @GoAgentOwners 2 | -------------------------------------------------------------------------------- /agent/agent.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "sort" 9 | "time" 10 | 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 12 | 13 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 14 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 15 | ) 16 | 17 | // Agent is thundra agent implementation 18 | type Agent struct { 19 | Plugins []plugin.Plugin 20 | Reporter reporter 21 | WarmUp bool 22 | TimeoutMargin time.Duration 23 | } 24 | 25 | // New is used to collect basic invocation data with thundra. Use NewBuilder and AddPlugin to access full functionality. 26 | func New() *Agent { 27 | return &Agent{ 28 | Reporter: newReporter(), 29 | WarmUp: config.WarmupEnabled, 30 | TimeoutMargin: config.TimeoutMargin, 31 | Plugins: []plugin.Plugin{}, 32 | } 33 | } 34 | 35 | // AddPlugin is used to enable plugins on thundra. You can use Trace, Metrics and Log plugins. 36 | // You need to initialize a plugin object and pass it as a parameter in order to enable it. 37 | // e.g. AddPlugin(trace.New()) 38 | func (a *Agent) AddPlugin(plugin plugin.Plugin) *Agent { 39 | if plugin.IsEnabled() { 40 | a.Plugins = append(a.Plugins, plugin) 41 | } 42 | 43 | return a 44 | } 45 | 46 | // SetReporter sets agent reporter 47 | func (a *Agent) SetReporter(r reporter) *Agent { 48 | a.Reporter = r 49 | return a 50 | } 51 | 52 | // ExecutePreHooks contains necessary works that should be done before user's handler 53 | func (a *Agent) ExecutePreHooks(ctx context.Context, request json.RawMessage) context.Context { 54 | a.Reporter.FlushFlag() 55 | 56 | // Sort plugins w.r.t their orders 57 | sort.Slice(a.Plugins, func(i, j int) bool { 58 | return a.Plugins[i].Order() < a.Plugins[j].Order() 59 | }) 60 | plugin.TraceID = utils.GenerateNewID() 61 | plugin.TransactionID = utils.GenerateNewID() 62 | 63 | // Traverse sorted plugin slice 64 | for _, p := range a.Plugins { 65 | ctx = p.BeforeExecution(ctx, request) 66 | } 67 | 68 | return ctx 69 | } 70 | 71 | // ExecutePostHooks contains necessary works that should be done after user's handler 72 | func (a *Agent) ExecutePostHooks(ctx context.Context, request json.RawMessage, response interface{}, err interface{}) { 73 | // Skip if it is already reported 74 | if *a.Reporter.Reported() == 1 { 75 | return 76 | } 77 | // Traverse the plugin slice in reverse order 78 | var messages []plugin.MonitoringDataWrapper 79 | for i := len(a.Plugins) - 1; i >= 0; i-- { 80 | p := a.Plugins[i] 81 | messages, ctx = p.AfterExecution(ctx, request, response, err) 82 | a.Reporter.Collect(messages) 83 | } 84 | a.Reporter.Report() 85 | a.Reporter.ClearData() 86 | } 87 | 88 | // CatchTimeout is checks for a timeout event and sends report if lambda is timedout 89 | func (a *Agent) CatchTimeout(ctx context.Context, payload json.RawMessage) { 90 | deadline, _ := ctx.Deadline() 91 | if deadline.IsZero() { 92 | return 93 | } 94 | timeoutDuration := deadline.Add(-a.TimeoutMargin) 95 | if config.DebugEnabled { 96 | log.Println("Timeout margin:", a.TimeoutMargin) 97 | } 98 | if time.Now().After(timeoutDuration) { 99 | return 100 | } 101 | timer := time.NewTimer(time.Until(timeoutDuration)) 102 | timeoutChannel := timer.C 103 | select { 104 | case <-timeoutChannel: 105 | log.Println("Function is timed out") 106 | a.ExecutePostHooks(ctx, payload, nil, timeoutError{}) 107 | return 108 | case <-ctx.Done(): 109 | // close timeoutChannel 110 | timer.Stop() 111 | return 112 | } 113 | } 114 | 115 | type timeoutError struct{} 116 | 117 | func (e timeoutError) Error() string { 118 | return fmt.Sprintf("Lambda is timed out") 119 | } 120 | -------------------------------------------------------------------------------- /agent/agent_test.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "log" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/mock" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 12 | "github.com/thundra-io/thundra-lambda-agent-go/v2/test" 13 | ) 14 | 15 | const ( 16 | generatedError = "Generated Error" 17 | testDataType = "TestDataType" 18 | ) 19 | 20 | type MockPlugin struct { 21 | mock.Mock 22 | } 23 | 24 | func (p *MockPlugin) IsEnabled() bool { 25 | return true 26 | } 27 | func (p *MockPlugin) Order() uint8 { 28 | return 4 29 | } 30 | func (p *MockPlugin) BeforeExecution(ctx context.Context, request json.RawMessage) context.Context { 31 | p.Called(ctx, request) 32 | return ctx 33 | } 34 | func (p *MockPlugin) AfterExecution(ctx context.Context, request json.RawMessage, response interface{}, err interface{}) ([]plugin.MonitoringDataWrapper, context.Context) { 35 | p.Called(ctx, request, response, err) 36 | return []plugin.MonitoringDataWrapper{}, ctx 37 | } 38 | func (p *MockPlugin) OnPanic(ctx context.Context, request json.RawMessage, err interface{}, stackTrace []byte) []plugin.MonitoringDataWrapper { 39 | p.Called(ctx, request, err, stackTrace) 40 | return []plugin.MonitoringDataWrapper{} 41 | } 42 | 43 | func TestExecutePreHooks(t *testing.T) { 44 | mT := new(MockPlugin) 45 | th := New().AddPlugin(mT) 46 | 47 | ctx := context.TODO() 48 | req := createRawMessage() 49 | 50 | mT.On("BeforeExecution", ctx, req, mock.Anything, mock.Anything).Return() 51 | th.ExecutePreHooks(ctx, req) 52 | mT.AssertExpectations(t) 53 | } 54 | 55 | func createRawMessage() json.RawMessage { 56 | var req json.RawMessage 57 | event := struct { 58 | name string 59 | }{ 60 | "gandalf", 61 | } 62 | 63 | req, err := json.Marshal(event) 64 | if err != nil { 65 | log.Println(err) 66 | } 67 | return req 68 | } 69 | 70 | func TestExecutePostHooks(t *testing.T) { 71 | type response struct { 72 | msg string 73 | } 74 | ctx := context.TODO() 75 | req := createRawMessage() 76 | resp := response{"Thundra"} 77 | var err1 error 78 | var err2 = errors.New("Error") 79 | 80 | r := test.NewMockReporter() 81 | 82 | mT := new(MockPlugin) 83 | mT.On("AfterExecution", ctx, req, resp, err1, mock.Anything).Return() 84 | 85 | th := New().AddPlugin(mT).SetReporter(r) 86 | th.ExecutePostHooks(ctx, req, resp, err1) 87 | th.ExecutePostHooks(ctx, req, resp, err2) 88 | 89 | mT.AssertExpectations(t) 90 | 91 | // Should only be called once because it is already reported 92 | mT.AssertNumberOfCalls(t, "AfterExecution", 1) 93 | r.AssertExpectations(t) 94 | } 95 | -------------------------------------------------------------------------------- /agent/reporter_test.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 12 | "github.com/thundra-io/thundra-lambda-agent-go/v2/test" 13 | ) 14 | 15 | type RoundTripFunc func(req *http.Request) (*http.Response, error) 16 | 17 | func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { 18 | return f(req) 19 | } 20 | 21 | func newTestClient(fn RoundTripFunc) *http.Client { 22 | return &http.Client{ 23 | Transport: RoundTripFunc(fn), 24 | } 25 | } 26 | 27 | func newTestReporter(fn RoundTripFunc) *reporterImpl { 28 | return &reporterImpl{ 29 | client: newTestClient(fn), 30 | reported: new(uint32), 31 | } 32 | } 33 | 34 | type mockDataModel struct{} 35 | 36 | var mockData mockDataModel 37 | 38 | func TestCollect(t *testing.T) { 39 | test.PrepareEnvironment() 40 | messages := []plugin.MonitoringDataWrapper{plugin.WrapMonitoringData(mockData, "Invocation")} 41 | testReporter := newTestReporter(func(req *http.Request) (*http.Response, error) { 42 | return &(http.Response{}), nil 43 | }) 44 | testReporter.Collect(messages) 45 | assert.Equal(t, messages, testReporter.messageQueue) 46 | test.CleanEnvironment() 47 | } 48 | 49 | func TestCollectAsyncCompositeDisabled(t *testing.T) { 50 | config.ReportCloudwatchEnabled = true 51 | config.ReportCloudwatchCompositeDataEnabled = false 52 | test.PrepareEnvironment() 53 | messages := []plugin.MonitoringDataWrapper{plugin.WrapMonitoringData(mockData, "Invocation")} 54 | testReporter := newTestReporter(func(req *http.Request) (*http.Response, error) { 55 | return &(http.Response{}), nil 56 | }) 57 | testReporter.Collect(messages) 58 | var expectedMessages []plugin.MonitoringDataWrapper 59 | assert.Equal(t, expectedMessages, testReporter.messageQueue) 60 | test.CleanEnvironment() 61 | } 62 | 63 | func TestClearData(t *testing.T) { 64 | test.PrepareEnvironment() 65 | messages := []plugin.MonitoringDataWrapper{plugin.WrapMonitoringData(mockData, "Invocation")} 66 | testReporter := newTestReporter(func(req *http.Request) (*http.Response, error) { 67 | return &(http.Response{}), nil 68 | }) 69 | testReporter.messageQueue = messages 70 | testReporter.ClearData() 71 | assert.Equal(t, []plugin.MonitoringDataWrapper{}, testReporter.messageQueue) 72 | test.CleanEnvironment() 73 | } 74 | 75 | func TestReportComposite(t *testing.T) { 76 | test.PrepareEnvironment() 77 | messages := []plugin.MonitoringDataWrapper{plugin.WrapMonitoringData(mockData, "Invocation")} 78 | testReporter := newTestReporter(func(req *http.Request) (*http.Response, error) { 79 | body, _ := ioutil.ReadAll(req.Body) 80 | var data plugin.MonitoringDataWrapper 81 | json.Unmarshal(body, &data) 82 | assert.Equal(t, "Composite", data.Type) 83 | return &(http.Response{}), nil 84 | }) 85 | testReporter.messageQueue = messages 86 | testReporter.Report() 87 | test.CleanEnvironment() 88 | } 89 | 90 | func TestReportCompositeDisabled(t *testing.T) { 91 | config.ReportRestCompositeDataEnabled = false 92 | test.PrepareEnvironment() 93 | messages := []plugin.MonitoringDataWrapper{plugin.WrapMonitoringData(mockData, "Invocation")} 94 | testReporter := newTestReporter(func(req *http.Request) (*http.Response, error) { 95 | body, _ := ioutil.ReadAll(req.Body) 96 | var data []plugin.MonitoringDataWrapper 97 | json.Unmarshal(body, &data) 98 | assert.Equal(t, "Invocation", data[0].Type) 99 | return &(http.Response{}), nil 100 | }) 101 | testReporter.messageQueue = messages 102 | testReporter.Report() 103 | test.CleanEnvironment() 104 | } 105 | -------------------------------------------------------------------------------- /agent/warmup.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Please visit https://github.com/thundra-io/thundra-lambda-warmup to learn more about warmup. 12 | 13 | // checkAndHandleWarmupRequest is used to detect thundra-lambda-warmup requests. 14 | // If the incoming request is a warmup request thundra will return nil and stop execution. 15 | func checkAndHandleWarmupRequest(payload json.RawMessage) bool { 16 | if json.Valid(payload) { 17 | paylaodStr := string(payload) 18 | if strings.HasPrefix(paylaodStr, `"#warmup`) { 19 | paylaodStr, err := strconv.Unquote(paylaodStr) 20 | if err != nil { 21 | log.Println("Bad string format while checking warmup") 22 | return false 23 | } 24 | paylaodStr = strings.TrimLeft(paylaodStr, " ") 25 | 26 | // Warmup data has the following format "#warmup wait=200 k1=v1" 27 | //Therefore we need to parse it to only have arguments in k=v format 28 | delay := 0 29 | sp := strings.SplitAfter(paylaodStr, "#warmup")[1] 30 | args := strings.Fields(sp) 31 | // Iterate over all warmup arguments 32 | for _, a := range args { 33 | argParts := strings.Split(a, "=") 34 | // Check whether argument is in key=value format 35 | if len(argParts) == 2 { 36 | k := argParts[0] 37 | v := argParts[1] 38 | // Check whether argument is "wait" argument 39 | // which specifies extra wait time before returning from request 40 | if k == "wait" { 41 | w, err := strconv.Atoi(v) 42 | if err != nil { 43 | log.Println(err) 44 | } else { 45 | delay += w 46 | } 47 | } 48 | } 49 | } 50 | log.Println("Received warmup request as warmup message. Handling with ", delay, " milliseconds delay ...") 51 | time.Sleep(time.Millisecond * time.Duration(delay)) 52 | return true 53 | 54 | } else { 55 | j := make(map[string]interface{}) 56 | err := json.Unmarshal(payload, &j) 57 | if err != nil { 58 | log.Println("Bad json format while checking warmup") 59 | return false 60 | } 61 | 62 | if len(j) == 0 { 63 | log.Println("Received warmup request as empty message. Handling with 100 milliseconds delay ...") 64 | time.Sleep(time.Millisecond * 100) 65 | return true 66 | } 67 | } 68 | 69 | } 70 | return false 71 | } 72 | -------------------------------------------------------------------------------- /agent/warmup_test.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCheckAndHandleWarmupNonEmptyPayload(t *testing.T) { 11 | payload := json.RawMessage(`{"firstName":"John","lastName":"Dow"}}`) 12 | assert.False(t, checkAndHandleWarmupRequest(payload)) 13 | } 14 | 15 | func TestCheckAndHandleWarmupWarmCommand(t *testing.T) { 16 | payload := `#warmup wait=200` 17 | 18 | rawMessage, err := json.Marshal(payload) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | assert.True(t, checkAndHandleWarmupRequest(rawMessage)) 24 | } 25 | 26 | func TestCheckAndHandleWarmupRequestEmptyPayload(t *testing.T) { 27 | payload := json.RawMessage(`{}`) 28 | assert.True(t, checkAndHandleWarmupRequest(payload)) 29 | } 30 | -------------------------------------------------------------------------------- /agent/wrapper.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 12 | ) 13 | 14 | type lambdaFunction func(context.Context, json.RawMessage) (interface{}, error) 15 | 16 | // Wrap is used for wrapping your lambda functions and start monitoring it by following the thundra objects settings 17 | // It wraps your lambda function and return a new lambda function. By that, AWS will be able to run this function 18 | // and Thundra will be able to collect monitoring data from your function. 19 | func (a *Agent) Wrap(handler interface{}) interface{} { 20 | if config.ThundraDisabled { 21 | return handler 22 | } 23 | 24 | if handler == nil { 25 | return thundraErrorHandler(fmt.Errorf("handler is nil")) 26 | } 27 | handlerType := reflect.TypeOf(handler) 28 | handlerValue := reflect.ValueOf(handler) 29 | 30 | if handlerType.Kind() != reflect.Func { 31 | return thundraErrorHandler(fmt.Errorf("handler kind %s is not %s", handlerType.Kind(), reflect.Func)) 32 | } 33 | 34 | takesContext, err := validateArguments(handlerType) 35 | 36 | if err != nil { 37 | return thundraErrorHandler(err) 38 | } 39 | 40 | if err := validateReturns(handlerType); err != nil { 41 | return thundraErrorHandler(err) 42 | } 43 | 44 | return func(ctx context.Context, payload json.RawMessage) (interface{}, error) { 45 | defer func() { 46 | if err := recover(); err != nil { 47 | a.ExecutePostHooks(ctx, payload, nil, err) 48 | panic(err) 49 | } 50 | }() 51 | 52 | if a.WarmUp && checkAndHandleWarmupRequest(payload) { 53 | return nil, nil 54 | } 55 | 56 | // Timeout handler 57 | go a.CatchTimeout(ctx, payload) 58 | 59 | var args []reflect.Value 60 | var elem reflect.Value 61 | 62 | if (handlerType.NumIn() == 1 && !takesContext) || handlerType.NumIn() == 2 { 63 | newEventType := handlerType.In(handlerType.NumIn() - 1) 64 | newEvent := reflect.New(newEventType) 65 | 66 | if err := json.Unmarshal(payload, newEvent.Interface()); err != nil { 67 | return nil, err 68 | } 69 | 70 | elem = newEvent.Elem() 71 | ctx = utils.SetEventTypeToContext(ctx, newEventType) 72 | } 73 | 74 | plugin.InitBaseData(ctx) 75 | 76 | ctxAfterPreHooks := a.ExecutePreHooks(ctx, payload) 77 | 78 | if takesContext { 79 | args = append(args, reflect.ValueOf(ctxAfterPreHooks)) 80 | } 81 | 82 | if elem.IsValid() { 83 | args = append(args, elem) 84 | } 85 | 86 | response := handlerValue.Call(args) 87 | 88 | var err error 89 | if len(response) > 0 { 90 | if errVal, ok := response[len(response)-1].Interface().(error); ok { 91 | err = errVal 92 | } 93 | } 94 | var val interface{} 95 | if len(response) > 1 { 96 | val = response[0].Interface() 97 | } 98 | 99 | if err != nil { 100 | val = nil 101 | } 102 | 103 | a.ExecutePostHooks(ctxAfterPreHooks, payload, &val, err) 104 | 105 | return val, err 106 | } 107 | } 108 | 109 | func thundraErrorHandler(e error) lambdaFunction { 110 | return func(ctx context.Context, event json.RawMessage) (interface{}, error) { 111 | return nil, e 112 | } 113 | } 114 | 115 | func validateArguments(handler reflect.Type) (bool, error) { 116 | handlerTakesContext := false 117 | if handler.NumIn() > 2 { 118 | return false, fmt.Errorf("handlers may not take more than two arguments, but handler takes %d", handler.NumIn()) 119 | } else if handler.NumIn() > 0 { 120 | contextType := reflect.TypeOf((*context.Context)(nil)).Elem() 121 | argumentType := handler.In(0) 122 | handlerTakesContext = argumentType.Implements(contextType) 123 | if handler.NumIn() > 1 && !handlerTakesContext { 124 | return false, fmt.Errorf("handler takes two arguments, but the first is not Context. got %s", argumentType.Kind()) 125 | } 126 | } 127 | 128 | return handlerTakesContext, nil 129 | } 130 | 131 | func validateReturns(handler reflect.Type) error { 132 | errorType := reflect.TypeOf((*error)(nil)).Elem() 133 | if handler.NumOut() > 2 { 134 | return fmt.Errorf("handler may not return more than two values") 135 | } else if handler.NumOut() > 1 { 136 | if !handler.Out(1).Implements(errorType) { 137 | return fmt.Errorf("handler returns two values, but the second does not implement error") 138 | } 139 | } else if handler.NumOut() == 1 { 140 | if !handler.Out(0).Implements(errorType) { 141 | return fmt.Errorf("handler returns a single value, but it does not implement error") 142 | } 143 | } 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /application/application_support.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 9 | ) 10 | 11 | func parseApplicationTags() { 12 | clearApplicationTags() 13 | tagPrefix := constants.ApplicationTagPrefixProp 14 | prefixLen := len(tagPrefix) 15 | for _, pair := range os.Environ() { 16 | if strings.HasPrefix(pair, tagPrefix) { 17 | splits := strings.SplitN(pair[prefixLen:], "=", 2) 18 | key, val := splits[0], splits[1] 19 | ApplicationTags[key] = parseStringToVal(val) 20 | } 21 | } 22 | } 23 | 24 | func parseStringToVal(s string) interface{} { 25 | if v, err := strconv.ParseBool(s); err == nil { 26 | return v 27 | } 28 | if v, err := strconv.ParseInt(s, 10, 32); err == nil { 29 | return v 30 | } 31 | if v, err := strconv.ParseFloat(s, 32); err == nil { 32 | return v 33 | } 34 | return strings.Trim(s, "\"") 35 | } 36 | 37 | func clearApplicationTags() { 38 | ApplicationTags = make(map[string]interface{}) 39 | } 40 | -------------------------------------------------------------------------------- /application/application_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 10 | ) 11 | 12 | func TestParseApplicationTags(t *testing.T) { 13 | cases := []struct { 14 | name string 15 | key string 16 | val string 17 | expectedKey string 18 | expectedVal interface{} 19 | }{ 20 | { 21 | name: "set int application tag", 22 | key: constants.ApplicationTagPrefixProp + "intKey", 23 | val: "37", 24 | expectedKey: "intKey", 25 | expectedVal: int64(37), 26 | }, 27 | { 28 | name: "set bool application tag", 29 | key: constants.ApplicationTagPrefixProp + "boolKey", 30 | val: "true", 31 | expectedKey: "boolKey", 32 | expectedVal: true, 33 | }, 34 | { 35 | name: "set bool application tag", 36 | key: constants.ApplicationTagPrefixProp + "boolKey", 37 | val: "t", 38 | expectedKey: "boolKey", 39 | expectedVal: true, 40 | }, 41 | { 42 | name: "set bool application tag", 43 | key: constants.ApplicationTagPrefixProp + "boolKey", 44 | val: "1", 45 | expectedKey: "boolKey", 46 | expectedVal: true, 47 | }, 48 | { 49 | name: "set bool application tag", 50 | key: constants.ApplicationTagPrefixProp + "boolKey", 51 | val: "false", 52 | expectedKey: "boolKey", 53 | expectedVal: false, 54 | }, 55 | { 56 | name: "set bool application tag", 57 | key: constants.ApplicationTagPrefixProp + "boolKey", 58 | val: "f", 59 | expectedKey: "boolKey", 60 | expectedVal: false, 61 | }, 62 | { 63 | name: "set bool application tag", 64 | key: constants.ApplicationTagPrefixProp + "boolKey", 65 | val: "0", 66 | expectedKey: "boolKey", 67 | expectedVal: false, 68 | }, 69 | { 70 | name: "set float application tag", 71 | key: constants.ApplicationTagPrefixProp + "floatKey", 72 | val: "1.5", 73 | expectedKey: "floatKey", 74 | expectedVal: 1.5, 75 | }, 76 | { 77 | name: "set float application tag", 78 | key: constants.ApplicationTagPrefixProp + "stringKey", 79 | val: "foobar", 80 | expectedKey: "stringKey", 81 | expectedVal: "foobar", 82 | }, 83 | } 84 | 85 | for i, testCase := range cases { 86 | t.Run(fmt.Sprintf("testCase[%d] %s", i, testCase.name), func(t *testing.T) { 87 | os.Setenv(testCase.key, testCase.val) 88 | parseApplicationTags() 89 | assert.Equal(t, testCase.expectedVal, ApplicationTags[testCase.expectedKey]) 90 | os.Unsetenv(testCase.key) 91 | }) 92 | } 93 | } 94 | 95 | func TestApplicationDomainNameFromEnv(t *testing.T) { 96 | os.Setenv(constants.ApplicationDomainProp, "fooDomain") 97 | domainName := getApplicationDomainName() 98 | assert.Equal(t, domainName, "fooDomain") 99 | os.Unsetenv(constants.ApplicationDomainProp) 100 | } 101 | 102 | func TestApplicationDomainNameDefault(t *testing.T) { 103 | domainName := getApplicationDomainName() 104 | assert.Equal(t, domainName, constants.AwsLambdaApplicationDomain) 105 | } 106 | 107 | func TestApplicationClassNameFromEnv(t *testing.T) { 108 | os.Setenv(constants.ApplicationClassProp, "fooClass") 109 | className := getApplicationClassName() 110 | assert.Equal(t, className, "fooClass") 111 | os.Unsetenv(constants.ApplicationClassProp) 112 | } 113 | 114 | func TestApplicationClassNameDefault(t *testing.T) { 115 | className := getApplicationClassName() 116 | assert.Equal(t, className, constants.AwsLambdaApplicationClass) 117 | } 118 | 119 | func TestApplicationStageFromEnv(t *testing.T) { 120 | os.Setenv(constants.ApplicationStageProp, "fooStage") 121 | stage := getApplicationStage() 122 | assert.Equal(t, stage, "fooStage") 123 | os.Unsetenv(constants.ApplicationStageProp) 124 | } 125 | 126 | func TestApplicationStageDefault(t *testing.T) { 127 | stage := getApplicationStage() 128 | assert.Equal(t, stage, os.Getenv(constants.ThundraApplicationStage)) 129 | } 130 | -------------------------------------------------------------------------------- /application/constants.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | const ApplicationRuntime = "go" 4 | const ApplicationRuntimeVersion = "1.x" 5 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetDefaultTimeoutMargin(t *testing.T) { 10 | AwsLambdaRegion = "us-west-2" 11 | timeoutMargin := getDefaultTimeoutMargin() 12 | assert.Equal(t, 200, timeoutMargin) 13 | 14 | AwsLambdaRegion = "us-west-1" 15 | timeoutMargin = getDefaultTimeoutMargin() 16 | assert.Equal(t, 400, timeoutMargin) 17 | 18 | AwsLambdaRegion = "us-east-1" 19 | timeoutMargin = getDefaultTimeoutMargin() 20 | assert.Equal(t, 600, timeoutMargin) 21 | 22 | AwsLambdaRegion = "eu-west-2" 23 | timeoutMargin = getDefaultTimeoutMargin() 24 | assert.Equal(t, 1000, timeoutMargin) 25 | 26 | AwsLambdaRegion = "eu-west-2" 27 | AwsLambdaFunctionMemorySize = 128 28 | timeoutMargin = getDefaultTimeoutMargin() 29 | assert.Equal(t, 3000, timeoutMargin) 30 | 31 | AwsLambdaRegion = "eu-west-2" 32 | AwsLambdaFunctionMemorySize = 256 33 | timeoutMargin = getDefaultTimeoutMargin() 34 | assert.Equal(t, 1500, timeoutMargin) 35 | } 36 | 37 | func TestGetDefaultCollector(t *testing.T) { 38 | regions := []string{ 39 | "us-west-2", "us-west-1", 40 | "us-east-2", "us-east-1", 41 | "ca-central-1", "sa-east-1", 42 | "eu-central-1", "eu-west-1", 43 | "eu-west-2", "eu-west-3", 44 | "eu-north-1", "eu-south-1", 45 | "ap-south-1", "ap-northeast-1", 46 | "ap-northeast-2", "ap-southeast-1", 47 | "ap-southeast-2", "ap-east-1", 48 | "af-south-1", "me-south-1", 49 | } 50 | 51 | for _, region := range regions { 52 | AwsLambdaRegion = region 53 | collector := getDefaultCollector() 54 | assert.Equal(t, region+".collector.thundra.io", collector) 55 | } 56 | 57 | AwsLambdaRegion = "" 58 | collector := getDefaultCollector() 59 | assert.Equal(t, "collector.thundra.io", collector) 60 | } 61 | -------------------------------------------------------------------------------- /constants/lambda.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ApplicationPlatform = "AWS Lambda" 4 | const AwsDefaultRegion = "AWS_DEFAULT_REGION" 5 | const ThundraLambdaDebugEnable = "thundra_lambda_debug_enable" 6 | const AwsLambdaInvocationRequestId = "aws.lambda.invocation.request_id" 7 | const AwsLambdaInvocationRequest = "aws.lambda.invocation.request" 8 | const AwsLambdaInvocationResponse = "aws.lambda.invocation.response" 9 | const AwsLambdaARN = "aws.lambda.arn" 10 | const AwsAccountNo = "aws.account_no" 11 | const AwsLambdaInvocationColdStart = "aws.lambda.invocation.coldstart" 12 | const AwsLambdaInvocationTimeout = "aws.lambda.invocation.timeout" 13 | const AwsLambdaLogGroupName = "aws.lambda.log_group_name" 14 | const AwsLambdaLogStreamName = "aws.lambda.log_stream_name" 15 | const AwsLambdaMemoryLimit = "aws.lambda.memory_limit" 16 | const AwsLambdaMemoryUsage = "aws.lambda.invocation.memory_usage" 17 | const AwsLambdaName = "aws.lambda.name" 18 | const AwsRegion = "aws.region" 19 | const AwsError = "error" 20 | const AwsErrorKind = "error.kind" 21 | const AwsErrorMessage = "error.message" 22 | const AwsErrorStack = "error.stack" 23 | const AwsLambdaApplicationDomain = "API" 24 | const AwsLambdaApplicationClass = "AWS-Lambda" 25 | 26 | const AwsLambdaTriggerOperationName = "x-thundra-trigger-operation-name" 27 | const AwsLambdaTriggerDomainName = "x-thundra-trigger-domain-name" 28 | const AwsLambdaTriggerClassName = "x-thundra-trigger-class-name" 29 | const AwsLambdaTriggerResourceName = "x-thundra-resource-name" 30 | 31 | const AwsLambdaFunctionMemorySize = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE" 32 | const AwsLambdaRegion = "AWS_REGION" 33 | const AwsSAMLocal = "AWS_SAM_LOCAL" 34 | 35 | const AwsXRayTraceHeader = "_X_AMZN_TRACE_ID" 36 | const AwsXRayTraceContextKey = "x-amzn-trace-id" 37 | const AwsXRaySegmentID = "aws.xray.segment.id" 38 | const AwsXRayTraceID = "aws.xray.trace.id" 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | postgres: 4 | image: postgres:latest 5 | environment: 6 | - POSTGRES_PASSWORD=userpass 7 | - POSTGRES_USER=user 8 | - POSTGRES_DB=db 9 | ports: 10 | - "127.0.0.1:5432:5432" 11 | mysql: 12 | image: mysql:5.7 13 | environment: 14 | - MYSQL_ROOT_PASSWORD=rootpass 15 | - MYSQL_PASSWORD=userpass 16 | - MYSQL_USER=user 17 | - MYSQL_DATABASE=db 18 | ports: 19 | - "127.0.0.1:3306:3306" 20 | elasticsearch: 21 | image: docker.elastic.co/elasticsearch/elasticsearch:6.6.1 22 | container_name: elasticsearch 23 | environment: 24 | - cluster.name=docker-cluster 25 | - bootstrap.memory_lock=true 26 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 27 | ulimits: 28 | memlock: 29 | soft: -1 30 | hard: -1 31 | volumes: 32 | - esdata1:/usr/share/elasticsearch/data 33 | ports: 34 | - 9200:9200 35 | redis: 36 | image: redis:4.0-alpine 37 | ports: 38 | - "127.0.0.1:6379:6379" 39 | mongo: 40 | image: mongo 41 | ports: 42 | - "27017:27017" 43 | 44 | volumes: 45 | esdata1: 46 | driver: local 47 | -------------------------------------------------------------------------------- /ext/constants.go: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | const ( 4 | // ThundraTagPrefix is prefix for the tags that are used in thundra internals 5 | ThundraTagPrefix = "thundra.span" 6 | // ClassNameKey defines class name 7 | ClassNameKey = ThundraTagPrefix + ".className" 8 | DomainNameKey = ThundraTagPrefix + ".domainName" 9 | OperationTypeKey = "operation.type" 10 | ) -------------------------------------------------------------------------------- /ext/span_options.go: -------------------------------------------------------------------------------- 1 | package ext 2 | 3 | import ( 4 | ot "github.com/opentracing/opentracing-go" 5 | ) 6 | 7 | func ClassName(className string) ot.StartSpanOption { 8 | return ot.Tag { 9 | Key: ClassNameKey, 10 | Value: className, 11 | } 12 | } 13 | 14 | func DomainName(domainName string) ot.StartSpanOption { 15 | return ot.Tag { 16 | Key: DomainNameKey, 17 | Value: domainName, 18 | } 19 | } 20 | 21 | func OperationType(operationType string) ot.StartSpanOption { 22 | return ot.Tag { 23 | Key: OperationTypeKey, 24 | Value: operationType, 25 | } 26 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thundra-io/thundra-lambda-agent-go/v2 2 | 3 | require ( 4 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect 5 | github.com/aws/aws-lambda-go v1.19.1 6 | github.com/aws/aws-sdk-go v1.34.30 7 | github.com/fortytw2/leaktest v1.3.0 // indirect 8 | github.com/go-ole/go-ole v1.2.4 // indirect 9 | github.com/google/uuid v1.1.2 10 | github.com/mailru/easyjson v0.7.6 // indirect 11 | github.com/onsi/ginkgo v1.14.1 // indirect 12 | github.com/onsi/gomega v1.10.2 // indirect 13 | github.com/opentracing/opentracing-go v1.2.0 14 | github.com/pkg/errors v0.9.1 15 | github.com/shirou/gopsutil v2.20.8+incompatible 16 | github.com/stretchr/testify v1.6.1 17 | github.com/apex/gateway v1.1.2 18 | github.com/apex/gateway/v2 v2.0.0 19 | ) 20 | -------------------------------------------------------------------------------- /invocation/constants.go: -------------------------------------------------------------------------------- 1 | package invocation 2 | 3 | const invocationType = "Invocation" 4 | const pluginOrder = 10 5 | const defaultErrorCode = "-1" 6 | -------------------------------------------------------------------------------- /invocation/data.go: -------------------------------------------------------------------------------- 1 | package invocation 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 7 | 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 12 | ) 13 | 14 | // invocationPlugin is the simplest form of data collected from lambda functions. It is collected for any case. 15 | type invocationDataModel struct { 16 | //Base fields 17 | plugin.BaseDataModel 18 | ID string `json:"id"` 19 | Type string `json:"type"` 20 | TraceID string `json:"traceId"` 21 | TransactionID string `json:"transactionId"` 22 | SpanID string `json:"spanId"` 23 | ApplicationPlatform string `json:"applicationPlatform"` 24 | FunctionRegion string `json:"functionRegion"` 25 | StartTimestamp int64 `json:"startTimestamp"` // Invocation start time in UNIX Epoch milliseconds 26 | FinishTimestamp int64 `json:"finishTimestamp"` // Invocation end time in UNIX Epoch milliseconds 27 | Duration int64 `json:"duration"` // Invocation time in milliseconds 28 | Erroneous bool `json:"erroneous"` // Shows if the invocationPlugin failed with an error 29 | ErrorType string `json:"errorType"` // Type of the thrown error 30 | ErrorMessage string `json:"errorMessage"` // Message of the thrown error 31 | ErrorCode string `json:"errorCode"` // Numeric code of the error, such as 404 for HttpError 32 | ColdStart bool `json:"coldStart"` // Shows if the invocationPlugin is cold started 33 | Timeout bool `json:"timeout"` // Shows if the invocationPlugin is timed out 34 | Tags map[string]interface{} `json:"tags"` 35 | UserTags map[string]interface{} `json:"userTags"` 36 | IncomingTraceLinks []string `json:"incomingTraceLinks"` 37 | OutgoingTraceLinks []string `json:"outgoingTraceLinks"` 38 | Resources []Resource `json:"resources"` 39 | } 40 | 41 | func (ip *invocationPlugin) prepareData(ctx context.Context) invocationDataModel { 42 | spanID := "" 43 | if ip.rootSpan != nil { 44 | spanID = ip.rootSpan.Context().(tracer.SpanContext).SpanID 45 | } 46 | tags := ip.prepareTags(ctx) 47 | 48 | return invocationDataModel{ 49 | BaseDataModel: plugin.GetBaseData(), 50 | ID: utils.GenerateNewID(), 51 | Type: invocationType, 52 | TraceID: plugin.TraceID, 53 | TransactionID: plugin.TransactionID, 54 | SpanID: spanID, 55 | ApplicationPlatform: constants.ApplicationPlatform, 56 | FunctionRegion: application.FunctionRegion, 57 | StartTimestamp: ip.data.startTimestamp, 58 | FinishTimestamp: ip.data.finishTimestamp, 59 | Duration: ip.data.duration, 60 | Erroneous: ip.data.erroneous, 61 | ErrorType: ip.data.errorType, 62 | ErrorMessage: ip.data.errorMessage, 63 | ErrorCode: ip.data.errorCode, 64 | ColdStart: ip.data.coldStart, 65 | Timeout: ip.data.timeout, 66 | IncomingTraceLinks: getIncomingTraceLinks(), 67 | OutgoingTraceLinks: getOutgoingTraceLinks(), 68 | Tags: tags, 69 | UserTags: userInvocationTags, 70 | Resources: getResources(spanID), 71 | } 72 | } 73 | 74 | func (ip *invocationPlugin) prepareTags(ctx context.Context) map[string]interface{} { 75 | tags := invocationTags 76 | 77 | // Put error related tags 78 | if ip.data.erroneous { 79 | tags["error"] = true 80 | tags["error.kind"] = ip.data.errorType 81 | tags["error.message"] = ip.data.errorMessage 82 | } 83 | arn := application.GetInvokedFunctionArn(ctx) 84 | xrayTraceID, xraySegmentID := utils.GetXRayTraceInfo(ctx) 85 | if len(xrayTraceID) > 0 { 86 | tags[constants.AwsXRayTraceID] = xrayTraceID 87 | } 88 | if len(xraySegmentID) > 0 { 89 | tags[constants.AwsXRaySegmentID] = xraySegmentID 90 | } 91 | tags[constants.AwsLambdaARN] = arn 92 | tags[constants.AwsAccountNo] = application.GetAwsAccountNo(arn) 93 | tags[constants.AwsLambdaInvocationColdStart] = ip.data.coldStart 94 | tags[constants.AwsLambdaInvocationRequestId] = application.GetAwsRequestID(ctx) 95 | tags[constants.AwsLambdaLogGroupName] = application.LogGroupName 96 | tags[constants.AwsLambdaLogStreamName] = application.LogStreamName 97 | tags[constants.AwsLambdaMemoryLimit] = application.MemoryLimit 98 | tags[constants.AwsLambdaMemoryUsage] = application.MemoryUsed 99 | tags[constants.AwsLambdaName] = application.ApplicationName 100 | tags[constants.AwsRegion] = application.FunctionRegion 101 | return tags 102 | } 103 | -------------------------------------------------------------------------------- /invocation/invocation.go: -------------------------------------------------------------------------------- 1 | package invocation 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | opentracing "github.com/opentracing/opentracing-go" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 11 | ) 12 | 13 | var invocationCount uint32 14 | 15 | type invocationPlugin struct { 16 | data *invocationData 17 | rootSpan opentracing.Span 18 | } 19 | 20 | type invocationData struct { 21 | startTimestamp int64 22 | finishTimestamp int64 23 | duration int64 24 | erroneous bool 25 | errorMessage string 26 | errorType string 27 | errorCode string 28 | coldStart bool 29 | timeout bool 30 | } 31 | 32 | // New initializes and returns a new invocationPlugin object. 33 | func New() *invocationPlugin { 34 | return &invocationPlugin{ 35 | data: &invocationData{}, 36 | } 37 | } 38 | 39 | func (ip *invocationPlugin) IsEnabled() bool { 40 | return true 41 | } 42 | 43 | func (ip *invocationPlugin) Order() uint8 { 44 | return pluginOrder 45 | } 46 | 47 | func (ip *invocationPlugin) BeforeExecution(ctx context.Context, request json.RawMessage) context.Context { 48 | startTime, ctx := plugin.StartTimeFromContext(ctx) 49 | ip.rootSpan = opentracing.SpanFromContext(ctx) 50 | ip.data = &invocationData{ 51 | startTimestamp: startTime, 52 | } 53 | 54 | setInvocationTriggerTags(ctx, request) 55 | if GetAgentTag(constants.SpanTags["TRIGGER_CLASS_NAME"]) != nil { 56 | triggerClassName, ok := GetAgentTag(constants.SpanTags["TRIGGER_CLASS_NAME"]).(string) 57 | if ok { 58 | plugin.TriggerClassName = triggerClassName 59 | } 60 | } 61 | return ctx 62 | } 63 | 64 | func (ip *invocationPlugin) AfterExecution(ctx context.Context, request json.RawMessage, response interface{}, err interface{}) ([]plugin.MonitoringDataWrapper, context.Context) { 65 | finishTime, ctx := plugin.EndTimeFromContext(ctx) 66 | ip.data.finishTimestamp = finishTime 67 | ip.data.duration = ip.data.finishTimestamp - ip.data.startTimestamp 68 | 69 | if userError != nil { 70 | ip.data.erroneous = true 71 | ip.data.errorMessage = utils.GetErrorMessage(userError) 72 | ip.data.errorType = utils.GetErrorType(userError) 73 | ip.data.errorCode = defaultErrorCode 74 | utils.SetSpanError(ip.rootSpan, userError) 75 | } 76 | 77 | if err != nil { 78 | ip.data.erroneous = true 79 | ip.data.errorMessage = utils.GetErrorMessage(err) 80 | ip.data.errorType = utils.GetErrorType(err) 81 | ip.data.errorCode = defaultErrorCode 82 | } 83 | 84 | ip.data.coldStart = isColdStarted() 85 | ip.data.timeout = utils.IsTimeout(err) 86 | 87 | data := ip.prepareData(ctx) 88 | 89 | if response != nil { 90 | responseInterface, ok := (response).(*interface{}) 91 | if ok { 92 | statusCode, err := utils.GetStatusCode(responseInterface) 93 | if err == nil { 94 | SetTag(constants.HTTPTags["STATUS"], statusCode) 95 | } 96 | } 97 | } 98 | 99 | ip.Reset() 100 | 101 | return []plugin.MonitoringDataWrapper{plugin.WrapMonitoringData(data, "Invocation")}, ctx 102 | } 103 | 104 | func (ip *invocationPlugin) Reset() { 105 | Clear() 106 | clearTraceLinks() 107 | } 108 | 109 | // isColdStarted returns if the lambda instance is cold started. Cold Start only happens on the first invocationPlugin. 110 | func isColdStarted() (coldStart bool) { 111 | if invocationCount++; invocationCount == 1 { 112 | coldStart = true 113 | } 114 | return coldStart 115 | } 116 | -------------------------------------------------------------------------------- /invocation/invocation_support.go: -------------------------------------------------------------------------------- 1 | package invocation 2 | 3 | var invocationTags = make(map[string]interface{}) 4 | var userInvocationTags = make(map[string]interface{}) 5 | var userError error 6 | 7 | // SetTag sets the given tag for invocation 8 | func SetTag(key string, value interface{}) { 9 | userInvocationTags[key] = value 10 | } 11 | 12 | // GetTag returns invocation tag for key 13 | func GetTag(key string) interface{} { 14 | return userInvocationTags[key] 15 | } 16 | 17 | // GetTags returns invocation tags 18 | func GetTags() map[string]interface{} { 19 | return userInvocationTags 20 | } 21 | 22 | // SetAgentTag sets the given tag for invocation 23 | func SetAgentTag(key string, value interface{}) { 24 | invocationTags[key] = value 25 | } 26 | 27 | // GetAgentTag returns invocation tag for key 28 | func GetAgentTag(key string) interface{} { 29 | return invocationTags[key] 30 | } 31 | 32 | // GetAgentTags returns invocation tags 33 | func GetAgentTags() map[string]interface{} { 34 | return invocationTags 35 | } 36 | 37 | // set error provided from user 38 | func SetError(exception error) { 39 | userError = exception 40 | } 41 | 42 | // Clear clears the invocation tags and error 43 | func Clear() { 44 | invocationTags = make(map[string]interface{}) 45 | userInvocationTags = make(map[string]interface{}) 46 | userError = nil 47 | } 48 | -------------------------------------------------------------------------------- /invocation/invocation_trace_support_test.go: -------------------------------------------------------------------------------- 1 | package invocation 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/opentracing/opentracing-go" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/ext" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/trace" 11 | ) 12 | 13 | func createMockSpans() { 14 | spans := []opentracing.Span{ 15 | opentracing.StartSpan( 16 | "www.test.com", 17 | ext.ClassName("HTTP"), 18 | opentracing.Tag{Key: constants.SpanTags["OPERATION_TYPE"], Value: "GET"}, 19 | opentracing.Tag{Key: constants.SpanTags["TOPOLOGY_VERTEX"], Value: true}, 20 | ), opentracing.StartSpan( 21 | "www.test.com", 22 | ext.ClassName("HTTP"), 23 | opentracing.Tag{Key: constants.SpanTags["OPERATION_TYPE"], Value: "GET"}, 24 | opentracing.Tag{Key: constants.SpanTags["TOPOLOGY_VERTEX"], Value: true}, 25 | ), 26 | opentracing.StartSpan( 27 | "localhost", 28 | ext.ClassName("Redis"), 29 | opentracing.Tag{Key: constants.SpanTags["OPERATION_TYPE"], Value: "READ"}, 30 | opentracing.Tag{Key: constants.SpanTags["TOPOLOGY_VERTEX"], Value: true}, 31 | opentracing.Tag{Key: constants.AwsError, Value: true}, 32 | opentracing.Tag{Key: constants.AwsErrorKind, Value: "testErr"}, 33 | ), 34 | } 35 | defer func() { 36 | for _, s := range spans { 37 | s.Finish() 38 | } 39 | }() 40 | } 41 | 42 | func TestGetResources(t *testing.T) { 43 | tp := trace.GetInstance() 44 | tp.Reset() 45 | 46 | createMockSpans() 47 | resources := getResources("") 48 | 49 | var resource1, resource2 Resource 50 | if resources[0].ResourceType == "HTTP" { 51 | resource1 = resources[0] 52 | resource2 = resources[1] 53 | } else { 54 | resource2 = resources[0] 55 | resource1 = resources[1] 56 | } 57 | 58 | assert.Equal(t, "HTTP", resource1.ResourceType) 59 | assert.Equal(t, "www.test.com", resource1.ResourceName) 60 | assert.Equal(t, "GET", resource1.ResourceOperation) 61 | assert.Equal(t, 2, resource1.ResourceCount) 62 | assert.Equal(t, 0, resource1.ResourceErrorCount) 63 | 64 | assert.Equal(t, "Redis", resource2.ResourceType) 65 | assert.Equal(t, "localhost", resource2.ResourceName) 66 | assert.Equal(t, "READ", resource2.ResourceOperation) 67 | assert.Equal(t, 1, resource2.ResourceCount) 68 | assert.Equal(t, 1, resource2.ResourceErrorCount) 69 | assert.ElementsMatch(t, []string{"testErr"}, resource2.ResourceErrors) 70 | 71 | tp.Reset() 72 | } 73 | -------------------------------------------------------------------------------- /invocation/testdata/alb-lambda-target-request-headers-only.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestContext": { 3 | "elb": { 4 | "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-1:123456789012:targetgroup/lambda-target/abcdefg" 5 | } 6 | }, 7 | "httpMethod": "GET", 8 | "path": "/", 9 | "queryStringParameters": { 10 | "key": "hello" 11 | }, 12 | "headers": { 13 | "accept": "*/*", 14 | "connection": "keep-alive", 15 | "host": "lambda-test-alb-1334523864.us-east-1.elb.amazonaws.com", 16 | "user-agent": "curl/7.54.0", 17 | "x-amzn-trace-id": "Root=1-5c34e93e-4dea0086f9763ac0667b115a", 18 | "x-forwarded-for": "25.12.198.67", 19 | "x-forwarded-port": "80", 20 | "x-forwarded-proto": "http", 21 | "x-imforwards": "20", 22 | "x-myheader": "123" 23 | }, 24 | "body": "", 25 | "isBase64Encoded": false 26 | } -------------------------------------------------------------------------------- /invocation/testdata/api-gw-missing-fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "body-json": {}, 3 | "params": { 4 | "path": {}, 5 | "querystring": {}, 6 | "header": { 7 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 8 | "Accept-Encoding": "br, gzip, deflate", 9 | "Accept-Language": "tr-tr", 10 | "CloudFront-Forwarded-Proto": "https", 11 | "CloudFront-Is-Desktop-Viewer": "true", 12 | "CloudFront-Is-Mobile-Viewer": "false", 13 | "CloudFront-Is-SmartTV-Viewer": "false", 14 | "CloudFront-Is-Tablet-Viewer": "false", 15 | "CloudFront-Viewer-Country": "TR", 16 | "User-Agent": "Mozilla/5.0 ", 17 | "Via": "2.0 7c2d73d3cd46e357090188fa2946f746.cloudfront.net (CloudFront)", 18 | "X-Amz-Cf-Id": "2oERVyfE28F7rylVV0ZOdEBnmogTSblZNOrSON_vGJFBweD1tIM-dg==", 19 | "X-Amzn-Trace-Id": "Root=1-5c3d8b9e-794ee8faf33ffce551c0146b", 20 | "X-Forwarded-Port": "443", 21 | "X-Forwarded-Proto": "https" 22 | } 23 | }, 24 | "stage-variables": {}, 25 | "context": { 26 | "account-id": "", 27 | "api-id": "random", 28 | "api-key": "", 29 | "authorizer-principal-id": "", 30 | "caller": "", 31 | "cognito-authentication-provider": "", 32 | "cognito-authentication-type": "", 33 | "cognito-identity-id": "", 34 | "cognito-identity-pool-id": "", 35 | "http-method": "GET", 36 | "source-ip": "", 37 | "user": "", 38 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14)", 39 | "user-arn": "", 40 | "request-id": "27eca8d1-1897-11e9-9eed-0d1fbe8bcba6", 41 | "resource-id": "3ggrja", 42 | "resource-path": "/hello" 43 | } 44 | } -------------------------------------------------------------------------------- /invocation/testdata/api-gw.json: -------------------------------------------------------------------------------- 1 | { 2 | "body-json": {}, 3 | "params": { 4 | "path": {}, 5 | "querystring": {}, 6 | "header": { 7 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 8 | "Accept-Encoding": "br, gzip, deflate", 9 | "Accept-Language": "tr-tr", 10 | "CloudFront-Forwarded-Proto": "https", 11 | "CloudFront-Is-Desktop-Viewer": "true", 12 | "CloudFront-Is-Mobile-Viewer": "false", 13 | "CloudFront-Is-SmartTV-Viewer": "false", 14 | "CloudFront-Is-Tablet-Viewer": "false", 15 | "CloudFront-Viewer-Country": "TR", 16 | "Host": "random.execute-api.us-west-2.amazonaws.com", 17 | "User-Agent": "Mozilla/5.0 ", 18 | "Via": "2.0 7c2d73d3cd46e357090188fa2946f746.cloudfront.net (CloudFront)", 19 | "X-Amz-Cf-Id": "2oERVyfE28F7rylVV0ZOdEBnmogTSblZNOrSON_vGJFBweD1tIM-dg==", 20 | "X-Amzn-Trace-Id": "Root=1-5c3d8b9e-794ee8faf33ffce551c0146b", 21 | "X-Forwarded-Port": "443", 22 | "X-Forwarded-Proto": "https" 23 | } 24 | }, 25 | "stage-variables": {}, 26 | "context": { 27 | "account-id": "", 28 | "api-id": "random", 29 | "api-key": "", 30 | "authorizer-principal-id": "", 31 | "caller": "", 32 | "cognito-authentication-provider": "", 33 | "cognito-authentication-type": "", 34 | "cognito-identity-id": "", 35 | "cognito-identity-pool-id": "", 36 | "http-method": "GET", 37 | "stage": "dev", 38 | "source-ip": "", 39 | "user": "", 40 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14)", 41 | "user-arn": "", 42 | "request-id": "27eca8d1-1897-11e9-9eed-0d1fbe8bcba6", 43 | "resource-id": "3ggrja", 44 | "resource-path": "/hello" 45 | } 46 | } -------------------------------------------------------------------------------- /invocation/testdata/apigw-request-missing-fields.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/{proxy+}", 3 | "path": "/hello/world", 4 | "httpMethod": "POST", 5 | "headers": { 6 | "Accept": "*/*", 7 | "Accept-Encoding": "gzip, deflate", 8 | "cache-control": "no-cache", 9 | "CloudFront-Forwarded-Proto": "https", 10 | "CloudFront-Is-Desktop-Viewer": "true", 11 | "CloudFront-Is-Mobile-Viewer": "false", 12 | "CloudFront-Is-SmartTV-Viewer": "false", 13 | "CloudFront-Is-Tablet-Viewer": "false", 14 | "CloudFront-Viewer-Country": "US", 15 | "Content-Type": "application/json", 16 | "headerName": "headerValue", 17 | "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", 18 | "User-Agent": "PostmanRuntime/2.4.5", 19 | "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", 20 | "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", 21 | "X-Forwarded-For": "54.240.196.186, 54.182.214.83", 22 | "X-Forwarded-Port": "443", 23 | "X-Forwarded-Proto": "https" 24 | }, 25 | "multiValueHeaders": { 26 | "Accept": ["*/*"], 27 | "Accept-Encoding": ["gzip, deflate"], 28 | "cache-control": ["no-cache"], 29 | "CloudFront-Forwarded-Proto": ["https"], 30 | "CloudFront-Is-Desktop-Viewer": ["true"], 31 | "CloudFront-Is-Mobile-Viewer": ["false"], 32 | "CloudFront-Is-SmartTV-Viewer": ["false"], 33 | "CloudFront-Is-Tablet-Viewer": ["false"], 34 | "CloudFront-Viewer-Country": ["US"], 35 | "Content-Type": ["application/json"], 36 | "headerName": ["headerValue"], 37 | "Host": ["gy415nuibc.execute-api.us-east-1.amazonaws.com"], 38 | "Postman-Token": ["9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f"], 39 | "User-Agent": ["PostmanRuntime/2.4.5"], 40 | "Via": ["1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)"], 41 | "X-Amz-Cf-Id": ["pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A=="], 42 | "X-Forwarded-For": ["54.240.196.186, 54.182.214.83"], 43 | "X-Forwarded-Port": ["443"], 44 | "X-Forwarded-Proto": ["https"] 45 | }, 46 | "queryStringParameters": { 47 | "name": "me" 48 | }, 49 | "multiValueQueryStringParameters": { 50 | "name": ["me"] 51 | }, 52 | "pathParameters": { 53 | "proxy": "hello/world" 54 | }, 55 | "stageVariables": { 56 | "stageVariableName": "stageVariableValue" 57 | }, 58 | "requestContext": { 59 | "accountId": "12345678912", 60 | "resourceId": "roq9wj", 61 | "stage": "testStage", 62 | "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", 63 | "identity": { 64 | "cognitoIdentityPoolId": "theCognitoIdentityPoolId", 65 | "accountId": "theAccountId", 66 | "cognitoIdentityId": "theCognitoIdentityId", 67 | "caller": "theCaller", 68 | "apiKey": "theApiKey", 69 | "accessKey": "ANEXAMPLEOFACCESSKEY", 70 | "sourceIp": "192.168.196.186", 71 | "cognitoAuthenticationType": "theCognitoAuthenticationType", 72 | "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", 73 | "userArn": "theUserArn", 74 | "userAgent": "PostmanRuntime/2.4.5", 75 | "user": "theUser" 76 | }, 77 | "authorizer": { 78 | "principalId": "admin", 79 | "clientId": 1, 80 | "clientName": "Exata" 81 | }, 82 | "resourcePath": "/{proxy+}", 83 | "httpMethod": "POST", 84 | "apiId": "gy415nuibc" 85 | }, 86 | "body": "{\r\n\t\"a\": 1\r\n}" 87 | } 88 | -------------------------------------------------------------------------------- /invocation/testdata/apigw-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/{proxy+}", 3 | "path": "/hello/world", 4 | "httpMethod": "POST", 5 | "headers": { 6 | "Accept": "*/*", 7 | "Accept-Encoding": "gzip, deflate", 8 | "cache-control": "no-cache", 9 | "CloudFront-Forwarded-Proto": "https", 10 | "CloudFront-Is-Desktop-Viewer": "true", 11 | "CloudFront-Is-Mobile-Viewer": "false", 12 | "CloudFront-Is-SmartTV-Viewer": "false", 13 | "CloudFront-Is-Tablet-Viewer": "false", 14 | "CloudFront-Viewer-Country": "US", 15 | "Content-Type": "application/json", 16 | "headerName": "headerValue", 17 | "Host": "gy415nuibc.execute-api.us-east-1.amazonaws.com", 18 | "Postman-Token": "9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f", 19 | "User-Agent": "PostmanRuntime/2.4.5", 20 | "Via": "1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)", 21 | "X-Amz-Cf-Id": "pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A==", 22 | "X-Forwarded-For": "54.240.196.186, 54.182.214.83", 23 | "X-Forwarded-Port": "443", 24 | "X-Forwarded-Proto": "https", 25 | "x-thundra-span-id": "test_span_id" 26 | }, 27 | "multiValueHeaders": { 28 | "Accept": ["*/*"], 29 | "Accept-Encoding": ["gzip, deflate"], 30 | "cache-control": ["no-cache"], 31 | "CloudFront-Forwarded-Proto": ["https"], 32 | "CloudFront-Is-Desktop-Viewer": ["true"], 33 | "CloudFront-Is-Mobile-Viewer": ["false"], 34 | "CloudFront-Is-SmartTV-Viewer": ["false"], 35 | "CloudFront-Is-Tablet-Viewer": ["false"], 36 | "CloudFront-Viewer-Country": ["US"], 37 | "Content-Type": ["application/json"], 38 | "headerName": ["headerValue"], 39 | "Host": ["gy415nuibc.execute-api.us-east-1.amazonaws.com"], 40 | "Postman-Token": ["9f583ef0-ed83-4a38-aef3-eb9ce3f7a57f"], 41 | "User-Agent": ["PostmanRuntime/2.4.5"], 42 | "Via": ["1.1 d98420743a69852491bbdea73f7680bd.cloudfront.net (CloudFront)"], 43 | "X-Amz-Cf-Id": ["pn-PWIJc6thYnZm5P0NMgOUglL1DYtl0gdeJky8tqsg8iS_sgsKD1A=="], 44 | "X-Forwarded-For": ["54.240.196.186, 54.182.214.83"], 45 | "X-Forwarded-Port": ["443"], 46 | "X-Forwarded-Proto": ["https"] 47 | }, 48 | "queryStringParameters": { 49 | "name": "me" 50 | }, 51 | "multiValueQueryStringParameters": { 52 | "name": ["me"] 53 | }, 54 | "pathParameters": { 55 | "proxy": "hello/world" 56 | }, 57 | "stageVariables": { 58 | "stageVariableName": "stageVariableValue" 59 | }, 60 | "requestContext": { 61 | "accountId": "12345678912", 62 | "resourceId": "roq9wj", 63 | "stage": "testStage", 64 | "requestId": "deef4878-7910-11e6-8f14-25afc3e9ae33", 65 | "identity": { 66 | "cognitoIdentityPoolId": "theCognitoIdentityPoolId", 67 | "accountId": "theAccountId", 68 | "cognitoIdentityId": "theCognitoIdentityId", 69 | "caller": "theCaller", 70 | "apiKey": "theApiKey", 71 | "accessKey": "ANEXAMPLEOFACCESSKEY", 72 | "sourceIp": "192.168.196.186", 73 | "cognitoAuthenticationType": "theCognitoAuthenticationType", 74 | "cognitoAuthenticationProvider": "theCognitoAuthenticationProvider", 75 | "userArn": "theUserArn", 76 | "userAgent": "PostmanRuntime/2.4.5", 77 | "user": "theUser" 78 | }, 79 | "authorizer": { 80 | "principalId": "admin", 81 | "clientId": 1, 82 | "clientName": "Exata" 83 | }, 84 | "resourcePath": "/{proxy+}", 85 | "httpMethod": "POST", 86 | "apiId": "gy415nuibc" 87 | }, 88 | "body": "{\r\n\t\"a\": 1\r\n}" 89 | } 90 | -------------------------------------------------------------------------------- /invocation/testdata/cf-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "cf": { 5 | "config": { 6 | "distributionId": "EXAMPLE" 7 | }, 8 | "request": { 9 | "uri": "/test", 10 | "method": "GET", 11 | "clientIp": "2001:cdba::3257:9652", 12 | "headers": { 13 | "user-agent": [ 14 | { 15 | "key": "User-Agent", 16 | "value": "Test Agent" 17 | } 18 | ], 19 | "host": [ 20 | { 21 | "key": "Host", 22 | "value": "d123.cf.net" 23 | } 24 | ], 25 | "cookie": [ 26 | { 27 | "key": "Cookie", 28 | "value": "SomeCookie=1; AnotherOne=A; X-Experiment-Name=B" 29 | } 30 | ] 31 | } 32 | } 33 | } 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /invocation/testdata/cloudwatch-logs-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "awslogs": { 3 | "data": "H4sIAAAAAAAAAHWPwQqCQBCGX0Xm7EFtK+smZBEUgXoLCdMhFtKV3akI8d0bLYmibvPPN3wz00CJxmQnTO41whwWQRIctmEcB6sQbFC3CjW3XW8kxpOpP+OC22d1Wml1qZkQGtoMsScxaczKN3plG8zlaHIta5KqWsozoTYw3/djzwhpLwivWFGHGpAFe7DL68JlBUk+l7KSN7tCOEJ4M3/qOI49vMHj+zCKdlFqLaU2ZHV2a4Ct/an0/ivdX8oYc1UVX860fQDQiMdxRQEAAA==" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /invocation/testdata/dynamodb-event-malformed.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "f07f8ca4b0b26cb9c4e5e77e69f274ee", 5 | "eventName": "INSERT", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "us-east-1", 9 | "dynamodb": { 10 | "ApproximateCreationDateTime": 1480642020, 11 | "Keys": { 12 | "val": { 13 | "S": "data" 14 | }, 15 | "key": { 16 | "S": "binary" 17 | } 18 | }, 19 | "NewImage": { 20 | "val": { 21 | "S": "data" 22 | }, 23 | "asdf1": { 24 | "B": "AAEqQQ==" 25 | }, 26 | "asdf2": { 27 | "BS": [ 28 | "AAEqQQ==", 29 | "QSoBAA==" 30 | ] 31 | }, 32 | "key": { 33 | "S": "binary" 34 | } 35 | }, 36 | "SequenceNumber": "1405400000000002063282832", 37 | "SizeBytes": 54, 38 | "StreamViewType": "NEW_AND_OLD_IMAGES" 39 | }, 40 | "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000" 41 | }, 42 | { 43 | "eventID": "f07f8ca4b0b26cb9c4e5e77e42f274ee", 44 | "eventName": "INSERT", 45 | "eventVersion": "1.1", 46 | "eventSource": "aws:dynamodb", 47 | "awsRegion": "us-east-1", 48 | "dynamodb": { 49 | "ApproximateCreationDateTime": 1480642020, 50 | "Keys": { 51 | "val": { 52 | "S": "data" 53 | }, 54 | "key": { 55 | "S": "binary" 56 | } 57 | }, 58 | "NewImage": { 59 | "val": { 60 | "S": "data" 61 | }, 62 | "asdf1": { 63 | "B": "AAEqQQ==" 64 | }, 65 | "b2": { 66 | "B": "test" 67 | }, 68 | "asdf2": { 69 | "BS": [ 70 | "AAEqQQ==", 71 | "QSoBAA==", 72 | "AAEqQQ==" 73 | ] 74 | }, 75 | "key": { 76 | "S": "binary" 77 | }, 78 | "Binary": { 79 | "B": "AAEqQQ==" 80 | }, 81 | "Boolean": { 82 | "BOOL": true 83 | }, 84 | "BinarySet": { 85 | "BS": [ 86 | "AAEqQQ==", 87 | "AAEqQQ==" 88 | ] 89 | }, 90 | "List": { 91 | "L": [ 92 | { 93 | "S": "Cookies" 94 | }, 95 | { 96 | "S": "Coffee" 97 | }, 98 | { 99 | "N": "3.14159" 100 | } 101 | ] 102 | }, 103 | "Map": { 104 | "M": { 105 | "Name": { 106 | "S": "Joe" 107 | }, 108 | "Age": { 109 | "N": "35" 110 | } 111 | } 112 | }, 113 | "FloatNumber": { 114 | "N": "123.45" 115 | }, 116 | "IntegerNumber": { 117 | "N": "123" 118 | }, 119 | "NumberSet": { 120 | "NS": [ 121 | "1234", 122 | "567.8" 123 | ] 124 | }, 125 | "Null": { 126 | "NULL": true 127 | }, 128 | "String": { 129 | "S": "Hello" 130 | }, 131 | "StringSet": { 132 | "SS": [ 133 | "Giraffe", 134 | "Zebra" 135 | -------------------------------------------------------------------------------- /invocation/testdata/dynamodb-event-wrong-arn.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "f07f8ca4b0b26cb9c4e5e77e69f274ee", 5 | "eventName": "INSERT", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "us-east-1", 9 | "userIdentity":{ 10 | "type":"Service", 11 | "principalId":"dynamodb.amazonaws.com" 12 | }, 13 | "dynamodb": { 14 | "ApproximateCreationDateTime": 1480642020, 15 | "Keys": { 16 | "val": { 17 | "S": "data" 18 | }, 19 | "key": { 20 | "S": "binary" 21 | } 22 | }, 23 | "NewImage": { 24 | "val": { 25 | "S": "data" 26 | }, 27 | "asdf1": { 28 | "B": "AAEqQQ==" 29 | }, 30 | "asdf2": { 31 | "BS": [ 32 | "AAEqQQ==", 33 | "QSoBAA==" 34 | ] 35 | }, 36 | "key": { 37 | "S": "binary" 38 | } 39 | }, 40 | "SequenceNumber": "1405400000000002063282832", 41 | "SizeBytes": 54, 42 | "StreamViewType": "NEW_AND_OLD_IMAGES" 43 | }, 44 | "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table" 45 | }, 46 | { 47 | "eventID": "f07f8ca4b0b26cb9c4e5e77e42f274ee", 48 | "eventName": "INSERT", 49 | "eventVersion": "1.1", 50 | "eventSource": "aws:dynamodb", 51 | "awsRegion": "us-east-1", 52 | "dynamodb": { 53 | "ApproximateCreationDateTime": 1480642020, 54 | "Keys": { 55 | "val": { 56 | "S": "data" 57 | }, 58 | "key": { 59 | "S": "binary" 60 | } 61 | }, 62 | "NewImage": { 63 | "val": { 64 | "S": "data" 65 | }, 66 | "asdf1": { 67 | "B": "AAEqQQ==" 68 | }, 69 | "b2": { 70 | "B": "test" 71 | }, 72 | "asdf2": { 73 | "BS": [ 74 | "AAEqQQ==", 75 | "QSoBAA==", 76 | "AAEqQQ==" 77 | ] 78 | }, 79 | "key": { 80 | "S": "binary" 81 | }, 82 | "Binary": { 83 | "B": "AAEqQQ==" 84 | }, 85 | "Boolean": { 86 | "BOOL": true 87 | }, 88 | "BinarySet": { 89 | "BS": [ 90 | "AAEqQQ==", 91 | "AAEqQQ==" 92 | ] 93 | }, 94 | "List": { 95 | "L": [ 96 | { 97 | "S": "Cookies" 98 | }, 99 | { 100 | "S": "Coffee" 101 | }, 102 | { 103 | "N": "3.14159" 104 | } 105 | ] 106 | }, 107 | "Map": { 108 | "M": { 109 | "Name": { 110 | "S": "Joe" 111 | }, 112 | "Age": { 113 | "N": "35" 114 | } 115 | } 116 | }, 117 | "FloatNumber": { 118 | "N": "123.45" 119 | }, 120 | "IntegerNumber": { 121 | "N": "123" 122 | }, 123 | "NumberSet": { 124 | "NS": [ 125 | "1234", 126 | "567.8" 127 | ] 128 | }, 129 | "Null": { 130 | "NULL": true 131 | }, 132 | "String": { 133 | "S": "Hello" 134 | }, 135 | "StringSet": { 136 | "SS": [ 137 | "Giraffe", 138 | "Zebra" 139 | ] 140 | }, 141 | "EmptyStringSet": { 142 | "SS": [] 143 | } 144 | }, 145 | "SequenceNumber": "1405400000000002063282832", 146 | "SizeBytes": 54, 147 | "StreamViewType": "NEW_AND_OLD_IMAGES" 148 | }, 149 | "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table" 150 | } 151 | ] 152 | } 153 | -------------------------------------------------------------------------------- /invocation/testdata/dynamodb-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventID": "f07f8ca4b0b26cb9c4e5e77e69f274ee", 5 | "eventName": "INSERT", 6 | "eventVersion": "1.1", 7 | "eventSource": "aws:dynamodb", 8 | "awsRegion": "us-east-1", 9 | "userIdentity":{ 10 | "type":"Service", 11 | "principalId":"dynamodb.amazonaws.com" 12 | }, 13 | "dynamodb": { 14 | "ApproximateCreationDateTime": 1480642020, 15 | "Keys": { 16 | "val": { 17 | "S": "data" 18 | }, 19 | "key": { 20 | "S": "binary" 21 | } 22 | }, 23 | "NewImage": { 24 | "val": { 25 | "S": "data" 26 | }, 27 | "asdf1": { 28 | "B": "AAEqQQ==" 29 | }, 30 | "asdf2": { 31 | "BS": [ 32 | "AAEqQQ==", 33 | "QSoBAA==" 34 | ] 35 | }, 36 | "key": { 37 | "S": "binary" 38 | } 39 | }, 40 | "SequenceNumber": "1405400000000002063282832", 41 | "SizeBytes": 54, 42 | "StreamViewType": "NEW_AND_OLD_IMAGES" 43 | }, 44 | "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000" 45 | }, 46 | { 47 | "eventID": "f07f8ca4b0b26cb9c4e5e77e42f274ee", 48 | "eventName": "INSERT", 49 | "eventVersion": "1.1", 50 | "eventSource": "aws:dynamodb", 51 | "awsRegion": "us-east-1", 52 | "dynamodb": { 53 | "ApproximateCreationDateTime": 1480642020, 54 | "Keys": { 55 | "val": { 56 | "S": "data" 57 | }, 58 | "key": { 59 | "S": "binary" 60 | } 61 | }, 62 | "NewImage": { 63 | "val": { 64 | "S": "data" 65 | }, 66 | "asdf1": { 67 | "B": "AAEqQQ==" 68 | }, 69 | "b2": { 70 | "B": "test" 71 | }, 72 | "asdf2": { 73 | "BS": [ 74 | "AAEqQQ==", 75 | "QSoBAA==", 76 | "AAEqQQ==" 77 | ] 78 | }, 79 | "key": { 80 | "S": "binary" 81 | }, 82 | "Binary": { 83 | "B": "AAEqQQ==" 84 | }, 85 | "Boolean": { 86 | "BOOL": true 87 | }, 88 | "BinarySet": { 89 | "BS": [ 90 | "AAEqQQ==", 91 | "AAEqQQ==" 92 | ] 93 | }, 94 | "List": { 95 | "L": [ 96 | { 97 | "S": "Cookies" 98 | }, 99 | { 100 | "S": "Coffee" 101 | }, 102 | { 103 | "N": "3.14159" 104 | } 105 | ] 106 | }, 107 | "Map": { 108 | "M": { 109 | "Name": { 110 | "S": "Joe" 111 | }, 112 | "Age": { 113 | "N": "35" 114 | } 115 | } 116 | }, 117 | "FloatNumber": { 118 | "N": "123.45" 119 | }, 120 | "IntegerNumber": { 121 | "N": "123" 122 | }, 123 | "NumberSet": { 124 | "NS": [ 125 | "1234", 126 | "567.8" 127 | ] 128 | }, 129 | "Null": { 130 | "NULL": true 131 | }, 132 | "String": { 133 | "S": "Hello" 134 | }, 135 | "StringSet": { 136 | "SS": [ 137 | "Giraffe", 138 | "Zebra" 139 | ] 140 | }, 141 | "EmptyStringSet": { 142 | "SS": [] 143 | } 144 | }, 145 | "SequenceNumber": "1405400000000002063282832", 146 | "SizeBytes": 54, 147 | "StreamViewType": "NEW_AND_OLD_IMAGES" 148 | }, 149 | "eventSourceARN": "arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table2/stream/2016-12-01T00:00:00.000" 150 | } 151 | ] 152 | } 153 | -------------------------------------------------------------------------------- /invocation/testdata/kinesis-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "kinesis": { 5 | "kinesisSchemaVersion": "1.0", 6 | "partitionKey": "s1", 7 | "sequenceNumber": "49568167373333333333333333333333333333333333333333333333", 8 | "data": "SGVsbG8gV29ybGQ=", 9 | "approximateArrivalTimestamp": 1480641523.477 10 | }, 11 | "eventSource": "aws:kinesis", 12 | "eventVersion": "1.0", 13 | "eventID": "shardId-000000000000:49568167373333333333333333333333333333333333333333333333", 14 | "eventName": "aws:kinesis:record", 15 | "invokeIdentityArn": "arn:aws:iam::123456789012:role/LambdaRole", 16 | "awsRegion": "us-east-1", 17 | "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/simple-stream" 18 | }, 19 | { 20 | "kinesis": { 21 | "kinesisSchemaVersion": "1.0", 22 | "partitionKey": "s1", 23 | "sequenceNumber": "49568167373333333334444444444444444444444444444444444444", 24 | "data": "SGVsbG8gV29ybGQ=", 25 | "approximateArrivalTimestamp": 1480841523.477 26 | }, 27 | "eventSource": "aws:kinesis", 28 | "eventVersion": "1.0", 29 | "eventID": "shardId-000000000000:49568167373333333334444444444444444444444444444444444444", 30 | "eventName": "aws:kinesis:record", 31 | "invokeIdentityArn": "arn:aws:iam::123456789012:role/LambdaRole", 32 | "awsRegion": "us-east-1", 33 | "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/simple-stream" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /invocation/testdata/kinesis-firehose-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "invocationId": "invoked123", 3 | "deliveryStreamArn": "aws:lambda:events:EXAMPLE/exampleStream", 4 | "region": "us-west-2", 5 | "records": [ 6 | { 7 | "data": "SGVsbG8gV29ybGQ=", 8 | "recordId": "record1", 9 | "approximateArrivalTimestamp": 1507217624302, 10 | "kinesisRecordMetadata": { 11 | "shardId": "shardId-000000000000", 12 | "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c317a", 13 | "approximateArrivalTimestamp": 1507217624302, 14 | "sequenceNumber": "49546986683135544286507457936321625675700192471156785154", 15 | "subsequenceNumber": 0 16 | } 17 | }, 18 | { 19 | "data": "SGVsbG8gV29ybGQ=", 20 | "recordId": "record2", 21 | "approximateArrivalTimestamp": 1507217624302, 22 | "kinesisRecordMetadata": { 23 | "shardId": "shardId-000000000001", 24 | "partitionKey": "4d1ad2b9-24f8-4b9d-a088-76e9947c318a", 25 | "approximateArrivalTimestamp": 1507217624302, 26 | "sequenceNumber": "49546986683135544286507457936321625675700192471156785155", 27 | "subsequenceNumber": 0 28 | } 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /invocation/testdata/s3-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "eventVersion": "2.0", 5 | "eventSource": "aws:s3", 6 | "awsRegion": "us-east-1", 7 | "eventTime": "1970-01-01T00:00:00.123Z", 8 | "eventName": "ObjectCreated:Put", 9 | "userIdentity": { 10 | "principalId": "EXAMPLE" 11 | }, 12 | "requestParameters": { 13 | "sourceIPAddress": "127.0.0.1" 14 | }, 15 | "responseElements": { 16 | "x-amz-request-id": "C3D13FE58DE4C810", 17 | "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD" 18 | }, 19 | "s3": { 20 | "s3SchemaVersion": "1.0", 21 | "configurationId": "testConfigRule", 22 | "bucket": { 23 | "name": "sourcebucket", 24 | "ownerIdentity": { 25 | "principalId": "EXAMPLE" 26 | }, 27 | "arn": "arn:aws:s3:::mybucket" 28 | }, 29 | "object": { 30 | "key": "HappyFace.jpg", 31 | "size": 1024, 32 | "urlDecodedKey": "HappyFace.jpg", 33 | "versionId": "version", 34 | "eTag": "d41d8cd98f00b204e9800998ecf8427e", 35 | "sequencer": "Happy Sequencer" 36 | } 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /invocation/testdata/schedule-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c", 3 | "detail-type": "Scheduled Event", 4 | "source": "aws.events", 5 | "account": "{{account-id}}", 6 | "time": "1970-01-01T00:00:00Z", 7 | "region": "eu-west-2", 8 | "resources": [ 9 | "arn:aws:events:eu-west-2:123456789012:rule/ExampleRule" 10 | ], 11 | "detail": {} 12 | } -------------------------------------------------------------------------------- /invocation/testdata/sns-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "EventVersion": "1.0", 5 | "EventSubscriptionArn": "arn:aws:sns:EXAMPLE", 6 | "EventSource": "aws:sns", 7 | "Sns": { 8 | "Signature": "EXAMPLE", 9 | "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e", 10 | "Type": "Notification", 11 | "TopicArn": "arn:aws:sns:EXAMPLE", 12 | "MessageAttributes": { 13 | "Test": { 14 | "Type": "String", 15 | "Value": "TestString" 16 | }, 17 | "TestBinary": { 18 | "Type": "Binary", 19 | "Value": "TestBinary" 20 | } 21 | }, 22 | "SignatureVersion": "1", 23 | "Timestamp": "2015-06-03T17:43:27.123Z", 24 | "SigningCertUrl": "EXAMPLE", 25 | "Message": "Hello from SNS!", 26 | "UnsubscribeUrl": "EXAMPLE", 27 | "Subject": "TestInvoke" 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /invocation/testdata/sqs-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "messageId" : "MessageID_1", 5 | "receiptHandle" : "MessageReceiptHandle", 6 | "body" : "Message Body", 7 | "md5OfBody" : "fce0ea8dd236ccb3ed9b37dae260836f", 8 | "md5OfMessageAttributes" : "582c92c5c5b6ac403040a4f3ab3115c9", 9 | "eventSourceARN": "arn:aws:sqs:us-west-2:123456789012:SQSQueue", 10 | "eventSource": "aws:sqs", 11 | "awsRegion": "us-west-2", 12 | "attributes" : { 13 | "ApproximateReceiveCount" : "2", 14 | "SentTimestamp" : "1520621625029", 15 | "SenderId" : "AROAIWPX5BD2BHG722MW4:sender", 16 | "ApproximateFirstReceiveTimestamp" : "1520621634884" 17 | }, 18 | "messageAttributes" : { 19 | "Attribute3" : { 20 | "binaryValue" : "MTEwMA==", 21 | "stringListValues" : ["abc", "123"], 22 | "binaryListValues" : ["MA==", "MQ==", "MA=="], 23 | "dataType" : "Binary" 24 | }, 25 | "Attribute2" : { 26 | "stringValue" : "123", 27 | "stringListValues" : [ ], 28 | "binaryListValues" : ["MQ==", "MA=="], 29 | "dataType" : "Number" 30 | }, 31 | "Attribute1" : { 32 | "stringValue" : "AttributeValue1", 33 | "stringListValues" : [ ], 34 | "binaryListValues" : [ ], 35 | "dataType" : "String" 36 | } 37 | } 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /log/constants.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | const pluginOrder = 20 4 | const logType = "Log" 5 | 6 | const traceLogLevel = "TRACE" 7 | const debugLogLevel = "DEBUG" 8 | const infoLogLevel = "INFO" 9 | const warnLogLevel = "WARN" 10 | const errorLogLevel = "ERROR" 11 | const noneLogLevel = "NONE" 12 | 13 | const traceLogLevelCode = 0 14 | const debugLogLevelCode = 1 15 | const infoLogLevelCode = 2 16 | const warnLogLevelCode = 3 17 | const errorLogLevelCode = 4 18 | const noneLogLevelCode = 6 19 | -------------------------------------------------------------------------------- /log/data.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 5 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 6 | ) 7 | 8 | type logData struct { 9 | //Base fields 10 | plugin.BaseDataModel 11 | ID string `json:"id"` 12 | Type string `json:"type"` 13 | TraceID string `json:"traceId"` 14 | TransactionID string `json:"transactionId"` 15 | SpanID string `json:"spanId"` 16 | LogMessage string `json:"logMessage"` 17 | LogContextName string `json:"logContextName"` 18 | LogTimestamp int64 `json:"logTimestamp"` 19 | LogLevel string `json:"logLevel"` 20 | LogLevelCode int `json:"logLevelCode"` 21 | Tags map[string]interface{} `json:"tags"` 22 | } 23 | 24 | type monitoringLog struct { 25 | logMessage string 26 | logContextName string 27 | logTimestamp int64 28 | logLevel string 29 | logLevelCode int 30 | spanID string 31 | } 32 | 33 | func prepareLogData(log *monitoringLog) logData { 34 | return logData{ 35 | BaseDataModel: plugin.GetBaseData(), 36 | ID: utils.GenerateNewID(), 37 | Type: logType, 38 | TraceID: plugin.TraceID, 39 | TransactionID: plugin.TransactionID, 40 | SpanID: log.spanID, 41 | LogMessage: log.logMessage, 42 | LogContextName: log.logContextName, 43 | LogTimestamp: log.logTimestamp, 44 | LogLevel: log.logLevel, 45 | LogLevelCode: log.logLevelCode, 46 | Tags: map[string]interface{}{}, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 8 | 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 10 | ) 11 | 12 | type logPlugin struct{} 13 | 14 | // New creates and returns new logPlugin 15 | func New() *logPlugin { 16 | return &logPlugin{} 17 | } 18 | 19 | func (p *logPlugin) IsEnabled() bool { 20 | return !config.LogDisabled 21 | } 22 | 23 | func (p *logPlugin) Order() uint8 { 24 | return pluginOrder 25 | } 26 | 27 | func (p *logPlugin) BeforeExecution(ctx context.Context, request json.RawMessage) context.Context { 28 | logManager.clearLogs() 29 | return ctx 30 | } 31 | 32 | func (p *logPlugin) AfterExecution(ctx context.Context, request json.RawMessage, response interface{}, err interface{}) ([]plugin.MonitoringDataWrapper, context.Context) { 33 | var collectedData []plugin.MonitoringDataWrapper 34 | for _, l := range logManager.logs { 35 | data := prepareLogData(l) 36 | sampler := GetSampler() 37 | if sampler == nil || sampler.IsSampled(data) { 38 | collectedData = append(collectedData, plugin.WrapMonitoringData(data, logType)) 39 | } 40 | } 41 | return collectedData, ctx 42 | } 43 | 44 | func (p *logPlugin) OnPanic(ctx context.Context, request json.RawMessage, err interface{}, stackTrace []byte) []plugin.MonitoringDataWrapper { 45 | var collectedData []plugin.MonitoringDataWrapper 46 | for _, l := range logManager.logs { 47 | data := prepareLogData(l) 48 | sampler := GetSampler() 49 | if sampler == nil || sampler.IsSampled(data) { 50 | collectedData = append(collectedData, plugin.WrapMonitoringData(data, logType)) 51 | } 52 | } 53 | return collectedData 54 | } 55 | -------------------------------------------------------------------------------- /log/log_support.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "github.com/thundra-io/thundra-lambda-agent-go/v2/samplers" 4 | 5 | var _sampler samplers.Sampler 6 | 7 | func GetSampler() samplers.Sampler { 8 | return _sampler 9 | } 10 | 11 | func SetSampler(sampler samplers.Sampler) { 12 | _sampler = sampler 13 | } 14 | -------------------------------------------------------------------------------- /log/tlogger_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 10 | ) 11 | 12 | const ( 13 | testMessage = "testMessage" 14 | expectedTestMessage = "testMessage\n" 15 | formattedTestMessage = "[testMessage]\n" 16 | ) 17 | 18 | func TestLoggerTrace(t *testing.T) { 19 | Logger.Trace(testMessage) 20 | assert.Equal(t, traceLogLevel, logManager.recentLogLevel) 21 | assert.Equal(t, traceLogLevelCode, logManager.recentLogLevelCode) 22 | assert.Equal(t, expectedTestMessage, logManager.logs[0].logMessage) 23 | logManager.clearLogs() 24 | } 25 | 26 | func TestLoggerDebug(t *testing.T) { 27 | Logger.Debug(testMessage) 28 | assert.Equal(t, debugLogLevel, logManager.recentLogLevel) 29 | assert.Equal(t, debugLogLevelCode, logManager.recentLogLevelCode) 30 | assert.Equal(t, expectedTestMessage, logManager.logs[0].logMessage) 31 | logManager.clearLogs() 32 | } 33 | 34 | func TestLoggerInfo(t *testing.T) { 35 | Logger.Info(testMessage) 36 | assert.Equal(t, infoLogLevel, logManager.recentLogLevel) 37 | assert.Equal(t, infoLogLevelCode, logManager.recentLogLevelCode) 38 | assert.Equal(t, expectedTestMessage, logManager.logs[0].logMessage) 39 | logManager.clearLogs() 40 | } 41 | 42 | func TestLoggerWarn(t *testing.T) { 43 | Logger.Warn(testMessage) 44 | assert.Equal(t, warnLogLevel, logManager.recentLogLevel) 45 | assert.Equal(t, warnLogLevelCode, logManager.recentLogLevelCode) 46 | assert.Equal(t, testMessage+"\n", logManager.logs[0].logMessage) 47 | logManager.clearLogs() 48 | } 49 | 50 | func TestLoggerError(t *testing.T) { 51 | Logger.Error(testMessage) 52 | assert.Equal(t, errorLogLevel, logManager.recentLogLevel) 53 | assert.Equal(t, errorLogLevelCode, logManager.recentLogLevelCode) 54 | assert.Equal(t, expectedTestMessage, logManager.logs[0].logMessage) 55 | logManager.clearLogs() 56 | } 57 | 58 | func TestLoggerPrintf(t *testing.T) { 59 | Logger.Printf("[%s]", testMessage) 60 | assert.Equal(t, infoLogLevel, logManager.recentLogLevel) 61 | assert.Equal(t, infoLogLevelCode, logManager.recentLogLevelCode) 62 | assert.Equal(t, formattedTestMessage, logManager.logs[0].logMessage) 63 | logManager.clearLogs() 64 | } 65 | 66 | func TestLoggerPrint(t *testing.T) { 67 | Logger.Print(testMessage) 68 | assert.Equal(t, infoLogLevel, logManager.recentLogLevel) 69 | assert.Equal(t, infoLogLevelCode, logManager.recentLogLevelCode) 70 | assert.Equal(t, expectedTestMessage, logManager.logs[0].logMessage) 71 | logManager.clearLogs() 72 | } 73 | 74 | func TestLoggerPrintln(t *testing.T) { 75 | Logger.Println(testMessage) 76 | assert.Equal(t, infoLogLevel, logManager.recentLogLevel) 77 | assert.Equal(t, infoLogLevelCode, logManager.recentLogLevelCode) 78 | assert.Equal(t, expectedTestMessage, logManager.logs[0].logMessage) 79 | logManager.clearLogs() 80 | } 81 | 82 | func TestLoggerPanicf(t *testing.T) { 83 | panicTestFunc := func() { 84 | Logger.Panicf("[%s]", testMessage) 85 | } 86 | assert.Panics(t, panicTestFunc) 87 | assert.Equal(t, errorLogLevel, logManager.recentLogLevel) 88 | assert.Equal(t, errorLogLevelCode, logManager.recentLogLevelCode) 89 | assert.Equal(t, formattedTestMessage, logManager.logs[0].logMessage) 90 | logManager.clearLogs() 91 | } 92 | 93 | func TestLoggerPanic(t *testing.T) { 94 | panicTestFunc := func() { 95 | Logger.Panic(testMessage) 96 | } 97 | assert.Panics(t, panicTestFunc) 98 | assert.Equal(t, errorLogLevel, logManager.recentLogLevel) 99 | assert.Equal(t, errorLogLevelCode, logManager.recentLogLevelCode) 100 | assert.Equal(t, expectedTestMessage, logManager.logs[0].logMessage) 101 | logManager.clearLogs() 102 | } 103 | 104 | func TestLoggerPanicln(t *testing.T) { 105 | panicTestFunc := func() { 106 | Logger.Panicln(testMessage) 107 | } 108 | assert.Panics(t, panicTestFunc) 109 | assert.Equal(t, errorLogLevel, logManager.recentLogLevel) 110 | assert.Equal(t, errorLogLevelCode, logManager.recentLogLevelCode) 111 | assert.Equal(t, expectedTestMessage, logManager.logs[0].logMessage) 112 | logManager.clearLogs() 113 | } 114 | 115 | func TestLogManagerWrite(t *testing.T) { 116 | p1, err1 := json.Marshal("testMessage") 117 | p2, err2 := json.Marshal("anotherTestMessage") 118 | if err1 != nil || err2 != nil { 119 | log.Println(err1, err2) 120 | } 121 | logManager.Write(p1) 122 | logManager.Write(p2) 123 | 124 | testMonitoredLogSetCorrectly(t, logManager.logs[0], "\"testMessage\"") 125 | testMonitoredLogSetCorrectly(t, logManager.logs[1], "\"anotherTestMessage\"") 126 | } 127 | 128 | func testMonitoredLogSetCorrectly(t *testing.T, m *monitoringLog, expectedMessage string) { 129 | assert.Equal(t, expectedMessage, m.logMessage) 130 | 131 | now := utils.GetTimestamp() 132 | assert.True(t, now >= m.logTimestamp) 133 | } 134 | -------------------------------------------------------------------------------- /metric/constants.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | const pluginOrder = 15 4 | const metricType = "Metric" 5 | const cpuMetric = "CPUMetric" 6 | const gcMetric = "GcMetric" 7 | const goroutineMetric = "GoroutineMetric" 8 | const heapMetric = "HeapMetric" 9 | const memoryMetric = "MemoryMetric" 10 | const diskMetric = "DiskMetric" 11 | const netMetric = "NetMetric" 12 | 13 | // CPU Metrics 14 | const appCPULoad = "app.cpuLoad" 15 | const sysCPULoad = "sys.cpuLoad" 16 | 17 | // GC Metrics 18 | const pauseTotalNs = "pauseTotalNs" 19 | const pauseNs = "pauseNs" 20 | const numGc = "numGC" 21 | const nextGc = "nextGC" 22 | const gcCPUFraction = "gcCPUFraction" 23 | const deltaNumGc = "deltaNumGC" 24 | const deltaPauseTotalNs = "deltaPauseTotalNs" 25 | 26 | // Disk Metrics 27 | const readBytes = "readBytes" 28 | const writeBytes = "writeBytes" 29 | const readCount = "readCount" 30 | const writeCount = "writeCount" 31 | 32 | // GC Metrics 33 | const numGoroutine = "numGoroutine" 34 | 35 | // Heap Metrics 36 | const heapAlloc = "heapAlloc" 37 | const heapSys = "heapSys" 38 | const heapInuse = "heapInuse" 39 | const heapObjects = "heapObjects" 40 | const memoryPercent = "memoryPercent" 41 | 42 | // Net Metrics 43 | const bytesRecv = "bytesRecv" 44 | const bytesSent = "bytesSent" 45 | const packetsRecv = "packetsRecv" 46 | const packetsSent = "packetsSent" 47 | const errIn = "errIn" 48 | const errOut = "errOut" 49 | 50 | // Memory Metrics 51 | const appUsedMemory = "app.usedMemory" 52 | const appMaxMemory = "app.maxMemory" 53 | const sysUsedMemory = "sys.usedMemory" 54 | const sysMaxMemory = "sys.maxMemory" 55 | -------------------------------------------------------------------------------- /metric/cpu.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "math" 5 | 6 | uuid "github.com/google/uuid" 7 | ) 8 | 9 | func prepareCPUMetricsData(mp *metricPlugin, base metricDataModel) metricDataModel { 10 | base.ID = uuid.New().String() 11 | base.MetricName = cpuMetric 12 | base.Metrics = map[string]interface{}{ 13 | appCPULoad: mp.data.appCPULoad, 14 | sysCPULoad: mp.data.systemCPULoad, 15 | } 16 | 17 | return base 18 | } 19 | 20 | func getSystemCPULoad(mp *metricPlugin) float64 { 21 | // Skip test 22 | if mp.data.startCPUTimeStat == nil { 23 | return 0 24 | } 25 | dSysUsed := mp.data.endCPUTimeStat.sysUsed() - mp.data.startCPUTimeStat.sysUsed() 26 | dTotal := mp.data.endCPUTimeStat.total() - mp.data.startCPUTimeStat.total() 27 | s := float64(dSysUsed) / float64(dTotal) 28 | if s <= 0 { 29 | s = 0 30 | } else if s >= 1 { 31 | s = 1 32 | } else if math.IsNaN(s) { 33 | s = 0 34 | } 35 | return s 36 | } 37 | 38 | func getProcessCPULoad(mp *metricPlugin) float64 { 39 | // Skip test 40 | if mp.data.startCPUTimeStat == nil { 41 | return 0 42 | } 43 | dProcUsed := mp.data.endCPUTimeStat.procUsed() - mp.data.startCPUTimeStat.procUsed() 44 | dTotal := mp.data.endCPUTimeStat.total() - mp.data.startCPUTimeStat.total() 45 | p := float64(dProcUsed) / float64(dTotal) 46 | if p <= 0 { 47 | p = 0 48 | } else if p >= 1 { 49 | p = 1 50 | } else if math.IsNaN(p) { 51 | p = 0 52 | } 53 | return p 54 | } 55 | -------------------------------------------------------------------------------- /metric/cpu_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPrepareCPUMetricsData(t *testing.T) { 10 | mp := New() 11 | base := mp.prepareMetricsData() 12 | cpuStatsData := prepareCPUMetricsData(mp, base) 13 | 14 | assert.True(t, len(cpuStatsData.ID) != 0) 15 | assert.Equal(t, cpuMetric, cpuStatsData.MetricName) 16 | assert.Equal(t, mp.data.metricTimestamp, cpuStatsData.MetricTimestamp) 17 | } 18 | -------------------------------------------------------------------------------- /metric/cpu_times.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type cpuTimesStat struct { 11 | *procPidTimesStat 12 | *procTimesStat 13 | } 14 | 15 | type procPidTimesStat struct { 16 | Utime uint64 `json:"utime"` 17 | Stime uint64 `json:"stime"` 18 | } 19 | 20 | type procTimesStat struct { 21 | User uint64 `json:"user"` 22 | System uint64 `json:"system"` 23 | Idle uint64 `json:"idle"` 24 | Nice uint64 `json:"nice"` 25 | Iowait uint64 `json:"iowait"` 26 | } 27 | 28 | func (t *cpuTimesStat) total() uint64 { 29 | return t.User + t.Nice + t.System + t.Idle + t.Iowait 30 | } 31 | 32 | func (t *cpuTimesStat) sysUsed() uint64 { 33 | return t.User + t.Nice + t.System 34 | } 35 | 36 | func (t *cpuTimesStat) procUsed() uint64 { 37 | return t.Utime + t.Stime 38 | } 39 | 40 | func sampleCPUtimesStat() *cpuTimesStat { 41 | pps := getProcPidStat() 42 | ps := getProcStat() 43 | 44 | if pps == nil || ps == nil { 45 | return nil 46 | } 47 | 48 | return &cpuTimesStat{ 49 | procPidTimesStat: pps, 50 | procTimesStat: ps, 51 | } 52 | } 53 | 54 | // Reads utime and stime from /proc/[pid]/stat file 55 | func getProcPidStat() *procPidTimesStat { 56 | contents, err := ioutil.ReadFile("/proc/" + pid + "/stat") 57 | if err != nil { 58 | log.Println(err.Error()) 59 | return nil 60 | } 61 | fields := strings.Fields(string(contents)) 62 | utime, err := strconv.ParseUint(fields[13], 10, 64) 63 | if err != nil { 64 | log.Println("procStat[13]: ", err.Error()) 65 | } 66 | stime, err := strconv.ParseUint(fields[14], 10, 64) 67 | if err != nil { 68 | log.Println("procStat[13]: ", err.Error()) 69 | } 70 | return &procPidTimesStat{ 71 | Utime: utime, 72 | Stime: stime, 73 | } 74 | } 75 | 76 | // Reads stats from /proc/stat file 77 | func getProcStat() *procTimesStat { 78 | contents, err := ioutil.ReadFile("/proc/stat") 79 | if err != nil { 80 | log.Println(err.Error()) 81 | return nil 82 | } 83 | fields := strings.Fields(string(contents)) 84 | user, err := strconv.ParseUint(fields[1], 10, 64) 85 | if err != nil { 86 | log.Println("procStat[0] ", err.Error()) 87 | } 88 | nice, err := strconv.ParseUint(fields[2], 10, 64) 89 | if err != nil { 90 | log.Println("procStat[1] ", err.Error()) 91 | } 92 | system, err := strconv.ParseUint(fields[3], 10, 64) 93 | if err != nil { 94 | log.Println("procStat[2] ", err.Error()) 95 | } 96 | idle, err := strconv.ParseUint(fields[4], 10, 64) 97 | if err != nil { 98 | log.Println("procStat[3] ", err.Error()) 99 | } 100 | iowait, err := strconv.ParseUint(fields[5], 10, 64) 101 | if err != nil { 102 | log.Println("procStat[4] ", err.Error()) 103 | } 104 | return &procTimesStat{ 105 | User: user, 106 | System: system, 107 | Idle: idle, 108 | Nice: nice, 109 | Iowait: iowait, 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /metric/data.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 5 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 6 | ) 7 | 8 | type metricDataModel struct { 9 | //Base fields 10 | plugin.BaseDataModel 11 | ID string `json:"id"` 12 | Type string `json:"type"` 13 | TraceID string `json:"traceId"` 14 | TransactionID string `json:"transactionId"` 15 | SpanID string `json:"spanId"` 16 | MetricName string `json:"metricName"` 17 | MetricTimestamp int64 `json:"metricTimestamp"` 18 | Metrics map[string]interface{} `json:"metrics"` 19 | Tags map[string]interface{} `json:"tags"` 20 | } 21 | 22 | func (mp *metricPlugin) prepareMetricsData() metricDataModel { 23 | return metricDataModel{ 24 | BaseDataModel: plugin.GetBaseData(), 25 | Type: metricType, 26 | TraceID: plugin.TraceID, 27 | TransactionID: plugin.TransactionID, 28 | SpanID: "", // Optional 29 | MetricTimestamp: mp.data.metricTimestamp, 30 | Metrics: map[string]interface{}{}, 31 | Tags: map[string]interface{}{ 32 | "aws.region": application.FunctionRegion, 33 | }, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /metric/disk.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "log" 5 | 6 | uuid "github.com/google/uuid" 7 | "github.com/shirou/gopsutil/process" 8 | ) 9 | 10 | func prepareDiskMetricsData(mp *metricPlugin, base metricDataModel) metricDataModel { 11 | base.ID = uuid.New().String() 12 | base.MetricName = diskMetric 13 | df := takeDiskFrame(mp) 14 | base.Metrics = map[string]interface{}{ 15 | // ReadBytes is the number of bytes read from disk 16 | readBytes: df.readBytes, 17 | // WriteBytes is the number of bytes write to disk 18 | writeBytes: df.writeBytes, 19 | // ReadCount is the number read operations from disk 20 | readCount: df.readCount, 21 | // WriteCount is the number write operations to disk 22 | writeCount: df.writeCount, 23 | } 24 | 25 | return base 26 | } 27 | 28 | type diskFrame struct { 29 | readBytes uint64 30 | writeBytes uint64 31 | readCount uint64 32 | writeCount uint64 33 | } 34 | 35 | //Since lambda works continuously we should subtract io values in order to get correct results per invocation 36 | //takeDiskFrame returns IO operations count for a specific time range 37 | func takeDiskFrame(mp *metricPlugin) *diskFrame { 38 | if mp.data.endDiskStat == nil || mp.data.startDiskStat == nil { 39 | return &diskFrame{} 40 | } 41 | rb := mp.data.endDiskStat.ReadBytes - mp.data.startDiskStat.ReadBytes 42 | wb := mp.data.endDiskStat.WriteBytes - mp.data.startDiskStat.WriteBytes 43 | 44 | rc := mp.data.endDiskStat.ReadCount - mp.data.startDiskStat.ReadCount 45 | wc := mp.data.endDiskStat.WriteCount - mp.data.startDiskStat.WriteCount 46 | 47 | return &diskFrame{ 48 | readBytes: rb, 49 | writeBytes: wb, 50 | readCount: rc, 51 | writeCount: wc, 52 | } 53 | } 54 | 55 | func sampleDiskStat() *process.IOCountersStat { 56 | diskStat, err := proc.IOCounters() 57 | if err != nil { 58 | log.Println("Error sampling disk stat", err) 59 | } 60 | return diskStat 61 | } 62 | -------------------------------------------------------------------------------- /metric/gc.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "runtime" 5 | 6 | uuid "github.com/google/uuid" 7 | ) 8 | 9 | func prepareGCMetricsData(mp *metricPlugin, memStats *runtime.MemStats, base metricDataModel) metricDataModel { 10 | base.ID = uuid.New().String() 11 | base.MetricName = gcMetric 12 | base.Metrics = map[string]interface{}{ 13 | // PauseTotalNs is the cumulative nanoseconds in GC 14 | // stop-the-world pauses since the program started. 15 | pauseTotalNs: memStats.PauseTotalNs, 16 | // PauseNs is recent GC stop-the-world pause time in nanoseconds. 17 | pauseNs: memStats.PauseNs[(memStats.NumGC+255)%256], 18 | // NumGC is the number of completed GC cycles. 19 | numGc: memStats.NumGC, 20 | // NextGC is the target heap size of the next GC cycle. 21 | nextGc: memStats.NextGC, 22 | // GCCPUFraction is the fraction of this program's available 23 | // CPU time used by the GC since the program started. 24 | gcCPUFraction: memStats.GCCPUFraction, 25 | //DeltaNumGc is the change in NUMGC from before execution to after execution. 26 | deltaNumGc: mp.data.endGCCount - mp.data.startGCCount, 27 | //DeltaPauseTotalNs is pause total change from before execution to after execution. 28 | deltaPauseTotalNs: mp.data.endPauseTotalNs - mp.data.startPauseTotalNs, 29 | } 30 | 31 | return base 32 | } 33 | -------------------------------------------------------------------------------- /metric/gc_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | const garbageCollectionCount = 5 11 | 12 | func TestPrepareGCMetricsData(t *testing.T) { 13 | mp := New() 14 | mp.data.startGCCount = 0 15 | mp.data.endGCCount = garbageCollectionCount 16 | 17 | makeMultipleGCCalls(garbageCollectionCount) 18 | memStats := &runtime.MemStats{} 19 | runtime.ReadMemStats(memStats) 20 | base := mp.prepareMetricsData() 21 | gcStatsData := prepareGCMetricsData(mp, memStats, base) 22 | 23 | assert.True(t, len(gcStatsData.ID) != 0) 24 | assert.Equal(t, gcMetric, gcStatsData.MetricName) 25 | assert.Equal(t, memStats.PauseTotalNs, gcStatsData.Metrics[pauseTotalNs]) 26 | assert.Equal(t, memStats.PauseNs[(memStats.NumGC+255)%256], gcStatsData.Metrics[pauseNs]) 27 | 28 | assert.Equal(t, uint32(garbageCollectionCount), gcStatsData.Metrics[numGc]) 29 | assert.Equal(t, memStats.NextGC, gcStatsData.Metrics[nextGc]) 30 | assert.Equal(t, memStats.GCCPUFraction, gcStatsData.Metrics[gcCPUFraction]) 31 | 32 | //DeltaGCCount equals to endGCCount - startGCCount 33 | assert.Equal(t, uint32(garbageCollectionCount), gcStatsData.Metrics[deltaNumGc]) 34 | } 35 | 36 | func makeMultipleGCCalls(times int) { 37 | for i := 0; i < times; i++ { 38 | runtime.GC() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /metric/goroutine.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "runtime" 5 | 6 | uuid "github.com/google/uuid" 7 | ) 8 | 9 | func prepareGoRoutineMetricsData(mp *metricPlugin, base metricDataModel) metricDataModel { 10 | base.ID = uuid.New().String() 11 | base.MetricName = goroutineMetric 12 | base.Metrics = map[string]interface{}{ 13 | // NumGoroutine is the number of goroutines on execution 14 | numGoroutine: uint64(runtime.NumGoroutine()), 15 | } 16 | 17 | return base 18 | } 19 | -------------------------------------------------------------------------------- /metric/goroutine_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 8 | ) 9 | 10 | const numGoroutines = 5 11 | 12 | //There are 2 goroutines running as default on testing 13 | //One is the main and the other one is the testing 14 | const defaultGoroutines = 2 15 | 16 | func TestPrepareGoroutineMetricsData(t *testing.T) { 17 | mp := New() 18 | mp.data.metricTimestamp = utils.GetTimestamp() 19 | mp.data.startGCCount = 1 20 | mp.data.endGCCount = 2 21 | 22 | done := make(chan bool) 23 | generateGoroutines(done, numGoroutines) 24 | base := mp.prepareMetricsData() 25 | grMetric := prepareGoRoutineMetricsData(mp, base) 26 | 27 | assert.True(t, len(grMetric.ID) != 0) 28 | assert.Equal(t, goroutineMetric, grMetric.MetricName) 29 | assert.Equal(t, mp.data.metricTimestamp, grMetric.MetricTimestamp) 30 | 31 | assert.Equal(t, uint64(numGoroutines+defaultGoroutines), grMetric.Metrics[numGoroutine]) 32 | killGeneratedGoroutines(done, numGoroutines) 33 | } 34 | 35 | //Generates a number of Goroutines and wait for done signal 36 | func generateGoroutines(done chan bool, numGoroutines int) { 37 | for i := 0; i < numGoroutines; i++ { 38 | go func(done chan bool) { 39 | <-done 40 | }(done) 41 | } 42 | } 43 | 44 | //Finished waiting goroutines 45 | func killGeneratedGoroutines(done chan bool, numGoroutines int) { 46 | for i := 0; i < numGoroutines; i++ { 47 | done <- true 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /metric/heap.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "log" 5 | "runtime" 6 | 7 | uuid "github.com/google/uuid" 8 | ) 9 | 10 | func prepareHeapMetricsData(metric *metricPlugin, memStats *runtime.MemStats, base metricDataModel) metricDataModel { 11 | base.ID = uuid.New().String() 12 | base.MetricName = heapMetric 13 | 14 | memPercent, err := proc.MemoryPercent() 15 | if err != nil { 16 | log.Println(err) 17 | } 18 | 19 | base.Metrics = map[string]interface{}{ 20 | // heapAlloc is bytes of allocated heap objects. 21 | // 22 | // "Allocated" heap objects include all reachable objects, as 23 | // well as unreachable objects that the garbage collector has 24 | // not yet freed. 25 | heapAlloc: memStats.HeapAlloc, 26 | // heapSys estimates the largest size the heap has had. 27 | heapSys: memStats.HeapSys, 28 | // heapInuse is bytes in in-use spans. 29 | // In-use spans have at least one object in them. These spans 30 | // can only be used for other objects of roughly the same 31 | // size. 32 | heapInuse: memStats.HeapInuse, 33 | // heapObjects is the number of allocated heap objects. 34 | heapObjects: memStats.HeapObjects, 35 | // memoryPercent returns how many percent of the total RAM this process uses 36 | memoryPercent: memPercent, 37 | } 38 | 39 | return base 40 | } 41 | -------------------------------------------------------------------------------- /metric/heap_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPrepareHeapMetricsData(t *testing.T) { 11 | mp := New() 12 | 13 | memStats := &runtime.MemStats{} 14 | base := mp.prepareMetricsData() 15 | heapMetricsData := prepareHeapMetricsData(mp, memStats, base) 16 | 17 | assert.True(t, len(heapMetricsData.ID) != 0) 18 | assert.Equal(t, heapMetric, heapMetricsData.MetricName) 19 | 20 | assert.Equal(t, memStats.HeapAlloc, heapMetricsData.Metrics[heapAlloc]) 21 | assert.Equal(t, memStats.HeapSys, heapMetricsData.Metrics[heapSys]) 22 | assert.Equal(t, memStats.HeapInuse, heapMetricsData.Metrics[heapInuse]) 23 | assert.Equal(t, memStats.HeapObjects, heapMetricsData.Metrics[heapObjects]) 24 | } 25 | -------------------------------------------------------------------------------- /metric/memory.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "log" 5 | 6 | uuid "github.com/google/uuid" 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 8 | ) 9 | 10 | const miBToB = 1024 * 1024 11 | 12 | func prepareMemoryMetricsData(mp *metricPlugin, base metricDataModel) metricDataModel { 13 | base.ID = uuid.New().String() 14 | base.MetricName = memoryMetric 15 | 16 | procMemInfo, err := proc.MemoryInfo() 17 | if err != nil { 18 | log.Println(err) 19 | } 20 | 21 | application.MemoryUsed = int(procMemInfo.RSS / miBToB) 22 | 23 | base.Metrics = map[string]interface{}{ 24 | appUsedMemory: procMemInfo.RSS, 25 | appMaxMemory: application.MemoryLimit * miBToB, 26 | } 27 | 28 | return base 29 | } 30 | -------------------------------------------------------------------------------- /metric/memory_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPrepareMemoryMetricsData(t *testing.T) { 11 | mp := New() 12 | 13 | base := mp.prepareMetricsData() 14 | memoryMetricsData := prepareMemoryMetricsData(mp, base) 15 | 16 | assert.True(t, len(memoryMetricsData.ID) != 0) 17 | assert.Equal(t, memoryMetric, memoryMetricsData.MetricName) 18 | assert.Equal(t, application.MemoryUsed, int(memoryMetricsData.Metrics[appUsedMemory].(uint64)/miBToB)) 19 | } 20 | -------------------------------------------------------------------------------- /metric/metric.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "runtime" 7 | 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 9 | 10 | "github.com/shirou/gopsutil/net" 11 | "github.com/shirou/gopsutil/process" 12 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 13 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 14 | ) 15 | 16 | var proc *process.Process 17 | var pid string 18 | 19 | type metricPlugin struct { 20 | data *metricData 21 | disableGCMetrics bool 22 | disableHeapMetrics bool 23 | disableGoroutineMetrics bool 24 | disableCPUMetrics bool 25 | disableDiskMetrics bool 26 | disableNetMetrics bool 27 | disableMemoryMetrics bool 28 | } 29 | 30 | type metricData struct { 31 | metricTimestamp int64 32 | startGCCount uint32 33 | endGCCount uint32 34 | startPauseTotalNs uint64 35 | endPauseTotalNs uint64 36 | startCPUTimeStat *cpuTimesStat 37 | endCPUTimeStat *cpuTimesStat 38 | appCPULoad float64 39 | systemCPULoad float64 40 | endDiskStat *process.IOCountersStat 41 | startDiskStat *process.IOCountersStat 42 | endNetStat *net.IOCountersStat 43 | startNetStat *net.IOCountersStat 44 | } 45 | 46 | // New returns new metric plugin initialized with empty metrics data 47 | func New() *metricPlugin { 48 | pid = utils.GetPid() 49 | proc = utils.GetThisProcess() 50 | 51 | return &metricPlugin{ 52 | data: &metricData{}, 53 | } 54 | } 55 | 56 | func (mp *metricPlugin) IsEnabled() bool { 57 | return !config.MetricDisabled 58 | } 59 | 60 | func (mp *metricPlugin) Order() uint8 { 61 | return pluginOrder 62 | } 63 | 64 | func (mp *metricPlugin) BeforeExecution(ctx context.Context, request json.RawMessage) context.Context { 65 | mp.data = &metricData{} 66 | mp.data.metricTimestamp = utils.GetTimestamp() 67 | 68 | if !mp.disableGCMetrics { 69 | m := &runtime.MemStats{} 70 | runtime.ReadMemStats(m) 71 | 72 | mp.data.startGCCount = m.NumGC 73 | mp.data.startPauseTotalNs = m.PauseTotalNs 74 | } 75 | 76 | if !mp.disableCPUMetrics { 77 | mp.data.startCPUTimeStat = sampleCPUtimesStat() 78 | } 79 | 80 | if !mp.disableDiskMetrics { 81 | mp.data.startDiskStat = sampleDiskStat() 82 | } 83 | 84 | if !mp.disableNetMetrics { 85 | mp.data.startNetStat = sampleNetStat() 86 | } 87 | 88 | return ctx 89 | } 90 | 91 | func (mp *metricPlugin) AfterExecution(ctx context.Context, request json.RawMessage, response interface{}, err interface{}) ([]plugin.MonitoringDataWrapper, context.Context) { 92 | mStats := &runtime.MemStats{} 93 | runtime.ReadMemStats(mStats) 94 | 95 | var stats []plugin.MonitoringDataWrapper 96 | 97 | base := mp.prepareMetricsData() 98 | 99 | if GetSampler() != nil { 100 | if !GetSampler().IsSampled(base) { 101 | return stats, ctx 102 | } 103 | } 104 | 105 | if !mp.disableGCMetrics { 106 | mp.data.endGCCount = mStats.NumGC 107 | mp.data.endPauseTotalNs = mStats.PauseTotalNs 108 | 109 | gc := prepareGCMetricsData(mp, mStats, base) 110 | stats = append(stats, plugin.WrapMonitoringData(gc, metricType)) 111 | } 112 | 113 | if !mp.disableHeapMetrics { 114 | h := prepareHeapMetricsData(mp, mStats, base) 115 | stats = append(stats, plugin.WrapMonitoringData(h, metricType)) 116 | } 117 | 118 | if !mp.disableGoroutineMetrics { 119 | g := prepareGoRoutineMetricsData(mp, base) 120 | stats = append(stats, plugin.WrapMonitoringData(g, metricType)) 121 | } 122 | 123 | if !mp.disableCPUMetrics { 124 | mp.data.endCPUTimeStat = sampleCPUtimesStat() 125 | 126 | mp.data.appCPULoad = getProcessCPULoad(mp) 127 | mp.data.systemCPULoad = getSystemCPULoad(mp) 128 | 129 | c := prepareCPUMetricsData(mp, base) 130 | stats = append(stats, plugin.WrapMonitoringData(c, metricType)) 131 | } 132 | 133 | if !mp.disableDiskMetrics { 134 | mp.data.endDiskStat = sampleDiskStat() 135 | d := prepareDiskMetricsData(mp, base) 136 | stats = append(stats, plugin.WrapMonitoringData(d, metricType)) 137 | } 138 | 139 | if !mp.disableNetMetrics { 140 | mp.data.endNetStat = sampleNetStat() 141 | n := prepareNetMetricsData(mp, base) 142 | stats = append(stats, plugin.WrapMonitoringData(n, metricType)) 143 | } 144 | 145 | if !mp.disableMemoryMetrics { 146 | mm := prepareMemoryMetricsData(mp, base) 147 | stats = append(stats, plugin.WrapMonitoringData(mm, metricType)) 148 | } 149 | 150 | return stats, ctx 151 | } 152 | -------------------------------------------------------------------------------- /metric/metric_support.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import "github.com/thundra-io/thundra-lambda-agent-go/v2/samplers" 4 | 5 | var _sampler = samplers.NewCompositeSampler([]samplers.Sampler{samplers.NewTimeAwareSampler(), samplers.NewCountAwareSampler()}, "or") 6 | 7 | func GetSampler() samplers.Sampler { 8 | return _sampler 9 | } 10 | 11 | func SetSampler(sampler samplers.Sampler) { 12 | _sampler = sampler 13 | } 14 | -------------------------------------------------------------------------------- /metric/metric_test.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/test" 11 | ) 12 | 13 | func TestNewMetricPlugin(t *testing.T) { 14 | test.PrepareEnvironment() 15 | mp := New() 16 | 17 | assert.NotNil(t, proc) 18 | assert.NotNil(t, mp.data) 19 | 20 | assert.False(t, mp.disableDiskMetrics) 21 | assert.False(t, mp.disableNetMetrics) 22 | assert.False(t, mp.disableCPUMetrics) 23 | assert.False(t, mp.disableGoroutineMetrics) 24 | assert.False(t, mp.disableHeapMetrics) 25 | assert.False(t, mp.disableGCMetrics) 26 | test.CleanEnvironment() 27 | } 28 | 29 | func TestMetric_BeforeExecution(t *testing.T) { 30 | const MaxUint32 = ^uint32(0) 31 | const MaxUint64 = ^uint64(0) 32 | 33 | mp := New() 34 | mp.data.startGCCount = MaxUint32 35 | mp.data.startPauseTotalNs = MaxUint64 36 | 37 | mp.BeforeExecution(context.TODO(), json.RawMessage{}) 38 | assert.NotNil(t, mp) 39 | 40 | // In order to ensure startGCCount and startPauseTotalNs are assigned, 41 | // check it's initial value is changed. 42 | // Initial values are the maximum numbers to eliminate unlucky conditions from happenning. 43 | assert.NotEqual(t, MaxUint32, mp.data.startGCCount) 44 | assert.NotEqual(t, MaxUint64, mp.data.startPauseTotalNs) 45 | } 46 | 47 | func TestMetric_AfterExecution(t *testing.T) { 48 | 49 | mp := New() 50 | 51 | stats, _ := mp.AfterExecution(context.TODO(), json.RawMessage{}, nil, nil) 52 | 53 | // Assert all stats are collected, heap, gc, goroutine, cpu, net, disk 54 | // Note that this fails on MACOSX and returns 6 instead of 7 55 | if runtime.GOOS != "darwin" { 56 | assert.Equal(t, 7, len(stats)) 57 | } 58 | 59 | for _, stat := range stats { 60 | assert.Equal(t, metricType, stat.Type) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /metric/net.go: -------------------------------------------------------------------------------- 1 | package metric 2 | 3 | import ( 4 | "log" 5 | 6 | uuid "github.com/google/uuid" 7 | "github.com/shirou/gopsutil/net" 8 | ) 9 | 10 | const all = 0 11 | 12 | func prepareNetMetricsData(mp *metricPlugin, base metricDataModel) metricDataModel { 13 | base.ID = uuid.New().String() 14 | base.MetricName = netMetric 15 | nf := takeNetFrame(mp) 16 | base.Metrics = map[string]interface{}{ 17 | // BytesRecv is how many bytes received from network 18 | bytesRecv: nf.bytesRecv, 19 | // BytesSent is how many bytes sent to network 20 | bytesSent: nf.bytesSent, 21 | // PacketsRecv is how many packets received from network 22 | packetsRecv: nf.packetsRecv, 23 | // PacketsSent is how many packets sent to network 24 | packetsSent: nf.packetsSent, 25 | // ErrIn is the number of errors while sending packet 26 | errIn: nf.errin, 27 | // ErrOut is the number of errors while receiving packet 28 | errOut: nf.errout, 29 | } 30 | 31 | return base 32 | } 33 | 34 | type netFrame struct { 35 | bytesSent uint64 36 | bytesRecv uint64 37 | packetsRecv uint64 38 | packetsSent uint64 39 | errin uint64 40 | errout uint64 41 | } 42 | 43 | //Since lambda works continuously we should subtract io values in order to get correct results per invocation 44 | func takeNetFrame(mp *metricPlugin) *netFrame { 45 | // If nil, return an empty netFrame 46 | if mp.data.endNetStat == nil || mp.data.startNetStat == nil { 47 | return &netFrame{} 48 | } 49 | 50 | br := mp.data.endNetStat.BytesRecv - mp.data.startNetStat.BytesRecv 51 | bs := mp.data.endNetStat.BytesSent - mp.data.startNetStat.BytesSent 52 | ps := mp.data.endNetStat.PacketsSent - mp.data.startNetStat.PacketsSent 53 | pr := mp.data.endNetStat.PacketsRecv - mp.data.startNetStat.PacketsRecv 54 | ei := mp.data.endNetStat.Errin - mp.data.startNetStat.Errin 55 | eo := mp.data.endNetStat.Errout - mp.data.startNetStat.Errout 56 | 57 | return &netFrame{ 58 | bytesRecv: br, 59 | bytesSent: bs, 60 | packetsRecv: pr, 61 | packetsSent: ps, 62 | errin: ei, 63 | errout: eo, 64 | } 65 | } 66 | 67 | func sampleNetStat() *net.IOCountersStat { 68 | netIOStat, err := net.IOCounters(false) 69 | if err != nil { 70 | log.Println("Error sampling net stat", err) 71 | return nil 72 | } 73 | return &netIOStat[all] 74 | } 75 | -------------------------------------------------------------------------------- /plugin/data.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 10 | ) 11 | 12 | type CompositeDataModel struct { 13 | BaseDataModel 14 | ID string `json:"id"` 15 | Type string `json:"type"` 16 | AllMonitoringData interface{} `json:"allMonitoringData"` 17 | } 18 | 19 | type BaseDataModel struct { 20 | AgentVersion *string `json:"agentVersion,omitempty"` 21 | DataModelVersion *string `json:"dataModelVersion,omitempty"` 22 | ApplicationID *string `json:"applicationId,omitempty"` 23 | ApplicationInstanceID *string `json:"applicationInstanceId,omitempty"` 24 | ApplicationDomainName *string `json:"applicationDomainName,omitempty"` 25 | ApplicationClassName *string `json:"applicationClassName,omitempty"` 26 | ApplicationName *string `json:"applicationName,omitempty"` 27 | ApplicationVersion *string `json:"applicationVersion,omitempty"` 28 | ApplicationStage *string `json:"applicationStage,omitempty"` 29 | ApplicationRuntime *string `json:"applicationRuntime,omitempty"` 30 | ApplicationRuntimeVersion *string `json:"applicationRuntimeVersion,omitempty"` 31 | ApplicationTags *map[string]interface{} `json:"applicationTags,omitempty"` 32 | } 33 | 34 | func PrepareCompositeData(baseDataModel BaseDataModel, allData []MonitoringDataWrapper) CompositeDataModel { 35 | 36 | var allDataUnwrapped []Data 37 | for i := range allData { 38 | allDataUnwrapped = append(allDataUnwrapped, allData[i].Data) 39 | } 40 | 41 | return CompositeDataModel{ 42 | BaseDataModel: baseDataModel, 43 | ID: utils.GenerateNewID(), 44 | Type: "Composite", 45 | AllMonitoringData: allDataUnwrapped, 46 | } 47 | } 48 | 49 | func InitBaseData(ctx context.Context) { 50 | application.ApplicationID = application.GetApplicationID(ctx) 51 | } 52 | 53 | func PrepareBaseData() BaseDataModel { 54 | agentVersion := constants.AgentVersion 55 | dataModelVersion := constants.DataModelVersion 56 | applicationRuntime := application.ApplicationRuntime 57 | applicationRuntimeVersion := application.ApplicationRuntimeVersion 58 | return BaseDataModel{ 59 | AgentVersion: &agentVersion, 60 | DataModelVersion: &dataModelVersion, 61 | ApplicationID: &application.ApplicationID, 62 | ApplicationInstanceID: &application.ApplicationInstanceID, 63 | ApplicationDomainName: &application.ApplicationDomainName, 64 | ApplicationClassName: &application.ApplicationClassName, 65 | ApplicationName: &application.ApplicationName, 66 | ApplicationVersion: &application.ApplicationVersion, 67 | ApplicationStage: &application.ApplicationStage, 68 | ApplicationRuntime: &applicationRuntime, 69 | ApplicationRuntimeVersion: &applicationRuntimeVersion, 70 | ApplicationTags: &application.ApplicationTags, 71 | } 72 | } 73 | 74 | func GetBaseData() BaseDataModel { 75 | if (config.ReportRestCompositeDataEnabled && !config.ReportCloudwatchEnabled) || 76 | (config.ReportCloudwatchEnabled && config.ReportCloudwatchCompositeDataEnabled) { 77 | return BaseDataModel{} 78 | } 79 | return PrepareBaseData() 80 | } 81 | -------------------------------------------------------------------------------- /plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 10 | ) 11 | 12 | var TraceID string 13 | var TransactionID string 14 | var TriggerClassName string 15 | 16 | // Plugin interface provides necessary methods for the plugins to be used in thundra agent 17 | type Plugin interface { 18 | BeforeExecution(ctx context.Context, request json.RawMessage) context.Context 19 | AfterExecution(ctx context.Context, request json.RawMessage, response interface{}, err interface{}) ([]MonitoringDataWrapper, context.Context) 20 | IsEnabled() bool 21 | Order() uint8 22 | } 23 | 24 | type Data interface{} 25 | 26 | // MonitoringDataWrapper defines the structure that given dataformat follows by Thundra. In here data could be a trace, metric or log data. 27 | type MonitoringDataWrapper struct { 28 | DataModelVersion string `json:"dataModelVersion"` 29 | Type string `json:"type"` 30 | Data Data `json:"data"` 31 | APIKey string `json:"apiKey"` 32 | Compressed bool `json:"compressed"` 33 | } 34 | 35 | func WrapMonitoringData(data interface{}, dataType string) MonitoringDataWrapper { 36 | return MonitoringDataWrapper{ 37 | DataModelVersion: constants.DataModelVersion, 38 | Type: dataType, 39 | Data: data, 40 | APIKey: config.APIKey, 41 | } 42 | } 43 | 44 | type key struct{} 45 | type startTimeKey key 46 | type endTimeKey key 47 | 48 | func StartTimeFromContext(ctx context.Context) (int64, context.Context) { 49 | startTime, ok := ctx.Value(startTimeKey{}).(int64) 50 | if ok { 51 | return startTime, ctx 52 | } 53 | startTime = utils.GetTimestamp() 54 | return startTime, context.WithValue(ctx, startTimeKey{}, startTime) 55 | } 56 | 57 | func EndTimeFromContext(ctx context.Context) (int64, context.Context) { 58 | endTime, ok := ctx.Value(endTimeKey{}).(int64) 59 | if ok { 60 | return endTime, ctx 61 | } 62 | endTime = utils.GetTimestamp() 63 | return endTime, context.WithValue(ctx, startTimeKey{}, endTime) 64 | } 65 | -------------------------------------------------------------------------------- /samplers/composite_sampler.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | type compositeSampler struct { 4 | samplers []Sampler 5 | operator string 6 | } 7 | 8 | var defaultOperator = "or" 9 | 10 | func (c *compositeSampler) IsSampled(data interface{}) bool { 11 | if c.samplers == nil || len(c.samplers) == 0 { 12 | return false 13 | } 14 | sampled := false 15 | if c.operator == "or" { 16 | for _, sampler := range c.samplers { 17 | sampled = sampler.IsSampled(data) || sampled 18 | } 19 | return sampled 20 | } else if c.operator == "and" { 21 | sampled = true 22 | for _, sampler := range c.samplers { 23 | sampled = sampler.IsSampled(data) && sampled 24 | } 25 | return sampled 26 | } 27 | 28 | return sampled 29 | } 30 | 31 | func NewCompositeSampler(samplers []Sampler, operator string) Sampler { 32 | _operator := operator 33 | if operator != "or" && operator != "and" { 34 | _operator = defaultOperator 35 | } 36 | return &compositeSampler{samplers, _operator} 37 | } 38 | -------------------------------------------------------------------------------- /samplers/composite_sampler_test.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type mockSampler struct { 10 | sampled bool 11 | } 12 | 13 | func (c *mockSampler) IsSampled(data interface{}) bool { 14 | return c.sampled 15 | } 16 | 17 | func newMockSampler(sampled bool) Sampler { 18 | return &mockSampler{sampled} 19 | } 20 | 21 | func TestWithNoSamplers(t *testing.T) { 22 | cms := NewCompositeSampler(nil, "") 23 | assert.False(t, cms.IsSampled(nil)) 24 | } 25 | 26 | func TestAndOperator(t *testing.T) { 27 | samplers := []Sampler{newMockSampler(true), newMockSampler(true)} 28 | 29 | cms := NewCompositeSampler(samplers, "and") 30 | assert.True(t, cms.IsSampled(nil)) 31 | 32 | samplers = append(samplers, newMockSampler(false)) 33 | 34 | cms = NewCompositeSampler(samplers, "and") 35 | assert.False(t, cms.IsSampled(nil)) 36 | } 37 | 38 | func TestOrOperator(t *testing.T) { 39 | samplers := []Sampler{newMockSampler(false), newMockSampler(false)} 40 | 41 | cms := NewCompositeSampler(samplers, "or") 42 | assert.False(t, cms.IsSampled(nil)) 43 | 44 | samplers = append(samplers, newMockSampler(true)) 45 | 46 | cms = NewCompositeSampler(samplers, "or") 47 | assert.True(t, cms.IsSampled(nil)) 48 | } 49 | 50 | func TestDefaultOperator(t *testing.T) { 51 | samplers := []Sampler{newMockSampler(false), newMockSampler(false)} 52 | 53 | cms := NewCompositeSampler(samplers, "") 54 | assert.False(t, cms.IsSampled(nil)) 55 | 56 | samplers = append(samplers, newMockSampler(true)) 57 | 58 | cms = NewCompositeSampler(samplers, "") 59 | assert.True(t, cms.IsSampled(nil)) 60 | } 61 | -------------------------------------------------------------------------------- /samplers/count_aware_sampler.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | import ( 4 | "sync/atomic" 5 | 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 8 | ) 9 | 10 | type countAwareSampler struct { 11 | countFreq int64 12 | counter int64 13 | } 14 | 15 | func (c *countAwareSampler) IsSampled(interface{}) bool { 16 | counter := atomic.AddInt64(&c.counter, 1) 17 | return (counter % c.countFreq) == 0 18 | } 19 | 20 | func NewCountAwareSampler(params ...int64) Sampler { 21 | var freq int64 22 | 23 | if config.SamplingCountFrequency > 0 { 24 | freq = int64(config.SamplingCountFrequency) 25 | } else if len(params) > 0 { 26 | freq = params[0] 27 | } else { 28 | freq = int64(constants.DefaultSamplingCountFreq) 29 | } 30 | 31 | return &countAwareSampler{countFreq: freq, counter: -1} 32 | } 33 | -------------------------------------------------------------------------------- /samplers/count_aware_sampler_test.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 9 | ) 10 | 11 | func TestDefaultCountFreq(t *testing.T) { 12 | 13 | cas := NewCountAwareSampler() 14 | 15 | assert.Equal(t, int64(constants.DefaultSamplingCountFreq), cas.(*countAwareSampler).countFreq) 16 | } 17 | 18 | func TestCountFreqFromEnv(t *testing.T) { 19 | config.SamplingCountFrequency = 10 20 | cas := NewCountAwareSampler() 21 | 22 | assert.Equal(t, int64(config.SamplingCountFrequency), cas.(*countAwareSampler).countFreq) 23 | } 24 | 25 | func TestFreqFromParam(t *testing.T) { 26 | config.SamplingCountFrequency = -1 27 | cas := NewCountAwareSampler(5) 28 | 29 | assert.Equal(t, int64(5), cas.(*countAwareSampler).countFreq) 30 | } 31 | 32 | func TestSampledCountAware(t *testing.T) { 33 | config.SamplingCountFrequency = -1 34 | cas := NewCountAwareSampler(2) 35 | 36 | assert.True(t, cas.IsSampled(nil)) 37 | assert.False(t, cas.IsSampled(nil)) 38 | assert.True(t, cas.IsSampled(nil)) 39 | } 40 | -------------------------------------------------------------------------------- /samplers/duration_aware_sampler.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | import ( 4 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 5 | ) 6 | 7 | type durationAwareSampler struct { 8 | duration int64 9 | longerThan bool 10 | } 11 | 12 | func (d *durationAwareSampler) IsSampled(message interface{}) bool { 13 | if message != nil { 14 | switch data := message.(type) { 15 | case *tracer.RawSpan: 16 | if data != nil { 17 | duration := data.Duration() 18 | if d.longerThan { 19 | return duration > d.duration 20 | } 21 | return duration < d.duration 22 | } 23 | } 24 | } 25 | return false 26 | } 27 | 28 | func NewDurationAwareSampler(duration int64, longerThanArr ...bool) Sampler { 29 | var longerThan bool 30 | if len(longerThanArr) > 0 { 31 | longerThan = longerThanArr[0] 32 | } 33 | return &durationAwareSampler{duration: duration, longerThan: longerThan} 34 | } 35 | -------------------------------------------------------------------------------- /samplers/duration_aware_sampler_test.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 9 | ) 10 | 11 | func TestDurationAwareSample(t *testing.T) { 12 | 13 | das := NewDurationAwareSampler(100) 14 | assert.True(t, das.IsSampled(&tracer.RawSpan{StartTimestamp: 0, EndTimestamp: 10})) 15 | assert.False(t, das.IsSampled(&tracer.RawSpan{StartTimestamp: 0, EndTimestamp: 150})) 16 | assert.False(t, das.IsSampled(nil)) 17 | } 18 | 19 | func TestDurationAwareSampleLongerThan(t *testing.T) { 20 | das := NewDurationAwareSampler(100, true) 21 | assert.False(t, das.IsSampled(&tracer.RawSpan{StartTimestamp: 0, EndTimestamp: 10})) 22 | assert.True(t, das.IsSampled(&tracer.RawSpan{StartTimestamp: 0, EndTimestamp: 110})) 23 | } 24 | -------------------------------------------------------------------------------- /samplers/error_aware_sampler.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | import ( 4 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 5 | ) 6 | 7 | type errorAwareSampler struct { 8 | } 9 | 10 | func (e *errorAwareSampler) IsSampled(message interface{}) bool { 11 | if message != nil { 12 | switch data := message.(type) { 13 | case *tracer.RawSpan: 14 | if data != nil && data.Tags != nil { 15 | return data.Tags["error"] != nil 16 | } 17 | } 18 | } 19 | return false 20 | } 21 | 22 | func NewErrorAwareSampler() Sampler { 23 | return &errorAwareSampler{} 24 | } 25 | -------------------------------------------------------------------------------- /samplers/error_aware_sampler_test.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | import ( 4 | "testing" 5 | 6 | ot "github.com/opentracing/opentracing-go" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 9 | ) 10 | 11 | func TestErrorAwareSample(t *testing.T) { 12 | 13 | eas := NewErrorAwareSampler() 14 | assert.True(t, eas.IsSampled(&tracer.RawSpan{Tags: ot.Tags{"error": true}})) 15 | assert.False(t, eas.IsSampled(&tracer.RawSpan{})) 16 | } 17 | -------------------------------------------------------------------------------- /samplers/sampler.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | // Sampler interface enables sampling of reported data 4 | type Sampler interface { 5 | IsSampled(interface{}) bool 6 | } 7 | -------------------------------------------------------------------------------- /samplers/time_aware_sampler.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 9 | ) 10 | 11 | type timeAwareSampler struct { 12 | timeFreq int64 13 | latestTime int64 14 | } 15 | 16 | func (t *timeAwareSampler) IsSampled(interface{}) bool { 17 | sampled := false 18 | now := time.Now().UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond)) 19 | latestTime := atomic.LoadInt64(&t.latestTime) 20 | if now > latestTime+t.timeFreq { 21 | atomic.StoreInt64(&t.latestTime, now) 22 | sampled = true 23 | } 24 | return sampled 25 | } 26 | 27 | func NewTimeAwareSampler(params ...int64) Sampler { 28 | var freq int64 29 | 30 | if config.SamplingTimeFrequency > 0 { 31 | freq = int64(config.SamplingTimeFrequency) 32 | } else if len(params) > 0 { 33 | freq = params[0] 34 | } else { 35 | freq = int64(constants.DefaultSamplingTimeFreq) 36 | } 37 | 38 | return &timeAwareSampler{timeFreq: freq} 39 | } 40 | -------------------------------------------------------------------------------- /samplers/time_aware_sampler_test.go: -------------------------------------------------------------------------------- 1 | package samplers 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 10 | ) 11 | 12 | func TestDefaultTimeFreq(t *testing.T) { 13 | 14 | tas := NewTimeAwareSampler() 15 | 16 | assert.Equal(t, int64(constants.DefaultSamplingTimeFreq), tas.(*timeAwareSampler).timeFreq) 17 | } 18 | 19 | func TestTimeFreqFromEnv(t *testing.T) { 20 | config.SamplingTimeFrequency = 10 21 | tas := NewTimeAwareSampler() 22 | 23 | assert.Equal(t, int64(config.SamplingTimeFrequency), tas.(*timeAwareSampler).timeFreq) 24 | } 25 | 26 | func TestTimeFreqFromParam(t *testing.T) { 27 | config.SamplingTimeFrequency = -1 28 | tas := NewTimeAwareSampler(10) 29 | 30 | assert.Equal(t, int64(10), tas.(*timeAwareSampler).timeFreq) 31 | } 32 | 33 | func TestSampledTimeAware(t *testing.T) { 34 | tas := NewTimeAwareSampler(1) 35 | 36 | assert.True(t, tas.IsSampled(nil)) 37 | time.Sleep(2000000) 38 | assert.True(t, tas.IsSampled(nil)) 39 | } 40 | -------------------------------------------------------------------------------- /test/util.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "sync/atomic" 5 | 6 | "github.com/aws/aws-lambda-go/lambdacontext" 7 | "github.com/stretchr/testify/mock" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 10 | ) 11 | 12 | const ( 13 | ApplicationName = "TestFunctionName" 14 | FunctionName = "TestFunctionName" 15 | ApplicationID = "aws:lambda:TestRegion:guest:TestFunctionName" 16 | LogStreamName = "2018/01/01/[$LATEST]1234567890" 17 | ApplicationInstanceID = "1234567890" 18 | FunctionVersion = "$Version" 19 | ApplicationStage = "TestStage" 20 | Region = "TestRegion" 21 | MemoryLimit = 512 22 | LogGroupName = "TestLogGroupName" 23 | ) 24 | 25 | //MockReporter is used in tests for mock reporter 26 | type MockReporter struct { 27 | mock.Mock 28 | MessageQueue []plugin.MonitoringDataWrapper 29 | ReportedFlag *uint32 30 | } 31 | 32 | func (r *MockReporter) Collect(messages []plugin.MonitoringDataWrapper) { 33 | r.MessageQueue = append(r.MessageQueue, messages...) 34 | r.Called(messages) 35 | } 36 | 37 | func (r *MockReporter) Report() { 38 | r.Called() 39 | atomic.CompareAndSwapUint32(r.ReportedFlag, 0, 1) 40 | } 41 | 42 | func (r *MockReporter) ClearData() { 43 | r.Called() 44 | } 45 | 46 | func (r *MockReporter) Reported() *uint32 { 47 | return r.ReportedFlag 48 | } 49 | 50 | func (r *MockReporter) FlushFlag() { 51 | atomic.CompareAndSwapUint32(r.Reported(), 1, 0) 52 | } 53 | 54 | // NewMockReporter returns a new MockReporter 55 | func NewMockReporter() *MockReporter { 56 | r := &MockReporter{ 57 | ReportedFlag: new(uint32), 58 | } 59 | r.On("Report").Return() 60 | r.On("ClearData").Return() 61 | r.On("Collect", mock.Anything).Return() 62 | return r 63 | } 64 | 65 | func PrepareEnvironment() { 66 | lambdacontext.LogStreamName = LogStreamName 67 | application.ApplicationName = ApplicationName 68 | application.FunctionName = FunctionName 69 | application.ApplicationInstanceID = ApplicationInstanceID 70 | application.ApplicationID = ApplicationID 71 | application.ApplicationVersion = FunctionVersion 72 | application.ApplicationStage = ApplicationStage 73 | application.FunctionRegion = Region 74 | application.MemoryLimit = MemoryLimit 75 | application.LogGroupName = LogGroupName 76 | application.LogStreamName = LogStreamName 77 | } 78 | 79 | func CleanEnvironment() { 80 | application.ApplicationName = "" 81 | application.FunctionName = "" 82 | application.ApplicationID = "" 83 | application.ApplicationInstanceID = "" 84 | application.ApplicationVersion = "" 85 | application.ApplicationStage = "" 86 | application.FunctionRegion = "" 87 | application.MemoryLimit = 0 88 | } 89 | -------------------------------------------------------------------------------- /thundra/thundra.go: -------------------------------------------------------------------------------- 1 | package thundra 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/agent" 7 | ip "github.com/thundra-io/thundra-lambda-agent-go/v2/invocation" 8 | lp "github.com/thundra-io/thundra-lambda-agent-go/v2/log" 9 | mp "github.com/thundra-io/thundra-lambda-agent-go/v2/metric" 10 | tp "github.com/thundra-io/thundra-lambda-agent-go/v2/trace" 11 | ) 12 | 13 | var agentInstance *agent.Agent 14 | 15 | // Logger is main thundra logger 16 | var Logger = lp.Logger 17 | 18 | func addDefaultPlugins(a *agent.Agent) *agent.Agent { 19 | a.AddPlugin(ip.New()). 20 | AddPlugin(mp.New()). 21 | AddPlugin(tp.GetInstance()). 22 | AddPlugin(lp.New()) 23 | 24 | return a 25 | } 26 | 27 | // Wrap wraps the given handler function so that the 28 | // thundra agent integrates with given handler 29 | func Wrap(handler interface{}) interface{} { 30 | if agentInstance == nil { 31 | log.Println("thundra.go: agentInstance is nil") 32 | return handler 33 | } 34 | 35 | return agentInstance.Wrap(handler) 36 | } 37 | 38 | func init() { 39 | agentInstance = addDefaultPlugins(agent.New()) 40 | } 41 | -------------------------------------------------------------------------------- /trace/constants.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | //Trace 4 | const spanType = "Span" 5 | const traceType = "Trace" 6 | const pluginOrder = 5 7 | -------------------------------------------------------------------------------- /trace/data.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 9 | 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 11 | ) 12 | 13 | type traceDataModel struct { 14 | plugin.BaseDataModel 15 | ID string `json:"id"` 16 | Type string `json:"type"` 17 | RootSpanID string `json:"rootSpanId"` 18 | StartTimestamp int64 `json:"startTimestamp"` 19 | FinishTimestamp int64 `json:"finishTimestamp"` 20 | Duration int64 `json:"duration"` 21 | Tags map[string]interface{} `json:"tags"` 22 | } 23 | 24 | func (tr *tracePlugin) prepareTraceDataModel(ctx context.Context, request json.RawMessage, response interface{}) traceDataModel { 25 | return traceDataModel{ 26 | BaseDataModel: plugin.GetBaseData(), 27 | ID: plugin.TraceID, 28 | Type: traceType, 29 | RootSpanID: tr.RootSpan.Context().(tracer.SpanContext).SpanID, 30 | StartTimestamp: tr.Data.StartTime, 31 | FinishTimestamp: tr.Data.FinishTime, 32 | Duration: tr.Data.Duration, 33 | } 34 | } 35 | 36 | type spanDataModel struct { 37 | plugin.BaseDataModel 38 | ID string `json:"id"` 39 | Type string `json:"type"` 40 | TraceID string `json:"traceId"` 41 | TransactionID string `json:"transactionId"` 42 | ParentSpanID string `json:"parentSpanId"` 43 | SpanOrder int64 `json:"spanOrder"` 44 | DomainName string `json:"domainName"` 45 | ClassName string `json:"className"` 46 | ServiceName string `json:"serviceName"` 47 | OperationName string `json:"operationName"` 48 | StartTimestamp int64 `json:"startTimestamp"` 49 | FinishTimestamp int64 `json:"finishTimestamp"` 50 | Duration int64 `json:"duration"` 51 | Tags map[string]interface{} `json:"tags"` 52 | Logs map[string]spanLog `json:"logs"` 53 | } 54 | 55 | type spanLog struct { 56 | Name string `json:"name"` 57 | Value interface{} `json:"value"` 58 | Timestamp int64 `json:"timestamp"` 59 | } 60 | 61 | func (tr *tracePlugin) prepareSpanDataModel(ctx context.Context, span *tracer.RawSpan) spanDataModel { 62 | // If a span have no rootSpanID (other than the root span) 63 | // Set rootSpan's ID as the parent ID for that span 64 | rootSpanID := tr.RootSpan.Context().(tracer.SpanContext).SpanID 65 | if len(span.ParentSpanID) == 0 && span.Context.SpanID != rootSpanID { 66 | span.ParentSpanID = rootSpanID 67 | } 68 | return spanDataModel{ 69 | BaseDataModel: plugin.GetBaseData(), 70 | ID: span.Context.SpanID, 71 | Type: spanType, 72 | TraceID: span.Context.TraceID, 73 | TransactionID: span.Context.TransactionID, 74 | ParentSpanID: span.ParentSpanID, 75 | DomainName: span.DomainName, 76 | ClassName: span.ClassName, 77 | ServiceName: application.ApplicationName, 78 | OperationName: span.OperationName, 79 | StartTimestamp: span.StartTimestamp, 80 | FinishTimestamp: span.EndTimestamp, 81 | Duration: span.Duration(), 82 | Tags: span.GetTags(), 83 | Logs: map[string]spanLog{}, // TO DO get logs 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /trace/erroneous.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | type errorInfo struct { 4 | Message string 5 | Kind string 6 | } 7 | 8 | type panicInfo struct { 9 | Message string 10 | Stack string 11 | Kind string 12 | } 13 | -------------------------------------------------------------------------------- /trace/trace_opentracing_test.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "testing" 9 | "time" 10 | 11 | opentracing "github.com/opentracing/opentracing-go" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/mock" 14 | "github.com/thundra-io/thundra-lambda-agent-go/v2/agent" 15 | "github.com/thundra-io/thundra-lambda-agent-go/v2/test" 16 | ) 17 | 18 | const ( 19 | mainDuration = 100 20 | f1Duration = 50 21 | f1Name = "f1" 22 | f2Duration = 30 23 | f2Name = "f2" 24 | ) 25 | 26 | // EXAMPLE HANDLERS 27 | func handler1(ctx context.Context, s string) (string, error) { 28 | span, ctx := opentracing.StartSpanFromContext(ctx, "test-operation") 29 | defer span.Finish() 30 | 31 | span.SetTag("tagKey", "tagValue") 32 | time.Sleep(time.Millisecond * mainDuration) 33 | return fmt.Sprintf("Happy monitoring with %s!", s), nil 34 | } 35 | 36 | func handler2(ctx context.Context, s string) (string, error) { 37 | span, ctx := opentracing.StartSpanFromContext(ctx, "test-operation") 38 | defer span.Finish() 39 | 40 | span.SetTag("tagKey", "tagValue") 41 | 42 | f := func(ctx context.Context, operationName string, duration time.Duration) { 43 | data, ctx := opentracing.StartSpanFromContext(ctx, operationName) 44 | defer data.Finish() 45 | time.Sleep(time.Millisecond * duration) 46 | } 47 | f(ctx, "f1", f1Duration) 48 | f(ctx, "f2", f2Duration) 49 | 50 | time.Sleep(time.Millisecond * mainDuration) 51 | return fmt.Sprintf("Happy monitoring with %s!", s), nil 52 | } 53 | 54 | func TestSpanTransformation(t *testing.T) { 55 | // t.Skip("skipping TestSpanTransformation") 56 | testCases := []struct { 57 | name string 58 | input string 59 | expected expected 60 | handler interface{} 61 | }{ 62 | { 63 | name: "Span test with root data only", 64 | input: `"Thundra"`, 65 | expected: expected{"Thundra works!", nil}, 66 | handler: handler1, 67 | }, 68 | { 69 | name: "Span test with multiple children", 70 | input: `"Thundra"`, 71 | expected: expected{"Thundra works!", nil}, 72 | handler: handler2, 73 | }, 74 | } 75 | 76 | for i, testCase := range testCases { 77 | t.Run(fmt.Sprintf("testCase[%d] %s", i, testCase.name), func(t *testing.T) { 78 | r := test.NewMockReporter() 79 | r.On("Report", testAPIKey).Return() 80 | r.On("Clear").Return() 81 | r.On("Collect", mock.Anything).Return() 82 | 83 | tr := New() 84 | a := agent.New().AddPlugin(tr).SetReporter(r) 85 | lambdaHandler := a.Wrap(testCase.handler) 86 | h := lambdaHandler.(func(context.Context, json.RawMessage) (interface{}, error)) 87 | f := lambdaFunction(h) 88 | f(context.TODO(), []byte(testCase.input)) 89 | 90 | 91 | msg := r.MessageQueue[1] 92 | // Root Span Data 93 | rsd, ok := msg.Data.(spanDataModel) 94 | if !ok { 95 | log.Println("Can not convert to span data") 96 | } 97 | assert.Equal(t, "test-operation", rsd.OperationName) 98 | 99 | durationMain := rsd.Duration 100 | assert.True(t, durationMain >= mainDuration) 101 | 102 | tags := rsd.Tags 103 | assert.Equal(t, "tagValue", tags["tagKey"]) 104 | 105 | if i == 1 { 106 | f1Msg := r.MessageQueue[2] 107 | f2Msg := r.MessageQueue[3] 108 | // Child span data 109 | f1Span, ok := f1Msg.Data.(spanDataModel) 110 | if !ok { 111 | log.Println("Can not convert f1 span data") 112 | } 113 | 114 | f2Span, ok := f2Msg.Data.(spanDataModel) 115 | if !ok { 116 | log.Println("Can not convert f2 span data") 117 | } 118 | 119 | assert.Equal(t, "f1", f1Span.OperationName) 120 | assert.Equal(t, "f2", f2Span.OperationName) 121 | assert.True(t, f1Span.Duration >= f1Duration) 122 | assert.True(t, f2Span.Duration >= f2Duration) 123 | } 124 | 125 | }) 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /trace/trace_support.go: -------------------------------------------------------------------------------- 1 | package trace 2 | 3 | import "github.com/thundra-io/thundra-lambda-agent-go/v2/samplers" 4 | 5 | var _sampler samplers.Sampler 6 | 7 | func GetSampler() samplers.Sampler { 8 | return _sampler 9 | } 10 | 11 | func SetSampler(sampler samplers.Sampler) { 12 | _sampler = sampler 13 | } 14 | -------------------------------------------------------------------------------- /tracer/error_injector_span_listener.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "reflect" 5 | "sync/atomic" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 10 | ) 11 | 12 | var defaultErrorMessage = "Error injected by Thundra!" 13 | 14 | type ErrorInjectorSpanListener struct { 15 | ErrorMessage string 16 | ErrorType error 17 | InjectOnFinish bool 18 | InjectCountFreq int64 19 | counter int64 20 | AddInfoTags bool 21 | } 22 | 23 | func (e *ErrorInjectorSpanListener) OnSpanStarted(span *spanImpl) { 24 | 25 | if !e.InjectOnFinish && e.ableToRaise() { 26 | e.injectError(span) 27 | } 28 | } 29 | 30 | func (e *ErrorInjectorSpanListener) OnSpanFinished(span *spanImpl) { 31 | if e.InjectOnFinish && e.ableToRaise() { 32 | e.injectError(span) 33 | } 34 | } 35 | 36 | func (e *ErrorInjectorSpanListener) PanicOnError() bool { 37 | return true 38 | } 39 | 40 | func (e *ErrorInjectorSpanListener) ableToRaise() bool { 41 | counter := atomic.AddInt64(&e.counter, 1) 42 | countfreq := e.InjectCountFreq 43 | if e.InjectCountFreq < 1 { 44 | countfreq = 1 45 | } 46 | return (counter % countfreq) == 0 47 | } 48 | 49 | func (e *ErrorInjectorSpanListener) addInfoTags(span *spanImpl, err error) { 50 | infoTags := map[string]interface{}{ 51 | "type": "error_injecter_span_listener", 52 | "error_type": reflect.TypeOf(err).String(), 53 | "error_message": err.Error(), 54 | "inject_on_finish": e.InjectOnFinish, 55 | "inject_count_freq": e.InjectCountFreq, 56 | } 57 | span.SetTag(constants.ThundraLambdaSpanListenerInfoTag, infoTags) 58 | } 59 | 60 | func (e *ErrorInjectorSpanListener) injectError(span *spanImpl) { 61 | var err error 62 | var errMessage = defaultErrorMessage 63 | 64 | if e.ErrorMessage != "" { 65 | errMessage = e.ErrorMessage 66 | } 67 | 68 | if e.ErrorType != nil { 69 | err = e.ErrorType 70 | } else { 71 | err = errors.New(errMessage) 72 | } 73 | utils.SetSpanError(span, err) 74 | if e.AddInfoTags { 75 | e.addInfoTags(span, err) 76 | } 77 | panic(err) 78 | } 79 | 80 | // NewErrorInjectorSpanListener creates and returns a new ErrorInjectorSpanListener from config 81 | func NewErrorInjectorSpanListener(config map[string]interface{}) ThundraSpanListener { 82 | spanListener := &ErrorInjectorSpanListener{ErrorMessage: defaultErrorMessage, AddInfoTags: true, InjectCountFreq: 1} 83 | 84 | if errorMessage, ok := config["errorMessage"].(string); ok { 85 | spanListener.ErrorMessage = errorMessage 86 | } 87 | if injectOnFinish, ok := config["injectOnFinish"].(bool); ok { 88 | spanListener.InjectOnFinish = injectOnFinish 89 | } 90 | if injectCountFreq, ok := config["injectCountFreq"].(float64); ok { 91 | spanListener.InjectCountFreq = int64(injectCountFreq) 92 | } 93 | if addInfoTags, ok := config["addInfoTags"].(bool); ok { 94 | spanListener.AddInfoTags = addInfoTags 95 | } 96 | spanListener.ErrorType = errors.New(spanListener.ErrorMessage) 97 | 98 | return spanListener 99 | } 100 | -------------------------------------------------------------------------------- /tracer/error_injector_span_listener_test.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | type customError struct { 10 | } 11 | 12 | func (e *customError) Error() string { 13 | return "Custom Error" 14 | } 15 | 16 | func TestFrequency(t *testing.T) { 17 | esl := ErrorInjectorSpanListener{InjectCountFreq: 3} 18 | 19 | for i := 0; i < 10; i++ { 20 | esl.OnSpanFinished(nil) 21 | } 22 | assert.Equal(t, int64(0), esl.counter) 23 | 24 | var errCount int64 25 | for i := 0; i < 9; i++ { 26 | func() { 27 | defer func() { 28 | if recover() != nil { 29 | errCount++ 30 | } 31 | }() 32 | esl.OnSpanStarted(nil) 33 | }() 34 | 35 | } 36 | assert.Equal(t, int64(3), errCount) 37 | assert.Equal(t, int64(9), esl.counter) 38 | } 39 | 40 | func TestError(t *testing.T) { 41 | errorMessage := "Your name is not good for this mission!" 42 | esl := ErrorInjectorSpanListener{ErrorMessage: errorMessage, ErrorType: &customError{}} 43 | 44 | var errorPanicked error 45 | func() { 46 | defer func() { 47 | errorPanicked = recover().(error) 48 | }() 49 | esl.OnSpanStarted(&spanImpl{}) 50 | }() 51 | 52 | assert.Equal(t, esl.ErrorType, errorPanicked) 53 | assert.Equal(t, esl.ErrorMessage, errorMessage) 54 | 55 | } 56 | 57 | func TestNewListenerFromConfig(t *testing.T) { 58 | config := map[string]interface{}{ 59 | "errorMessage": "You have a very funny name!", 60 | "injectOnFinish": true, 61 | "injectCountFreq": float64(7), 62 | "addInfoTags": false, 63 | "foo": "bar", 64 | } 65 | 66 | esl := NewErrorInjectorSpanListener(config).(*ErrorInjectorSpanListener) 67 | 68 | assert.Equal(t, "You have a very funny name!", esl.ErrorMessage) 69 | assert.Equal(t, int64(7), esl.InjectCountFreq) 70 | assert.Equal(t, true, esl.InjectOnFinish) 71 | assert.Equal(t, false, esl.AddInfoTags) 72 | } 73 | 74 | func TestNewListenerFromConfigWithTypeErrors(t *testing.T) { 75 | config := map[string]interface{}{ 76 | "injectOnFinish": 37, 77 | "injectCountFreq": "message", 78 | } 79 | 80 | esl := NewErrorInjectorSpanListener(config).(*ErrorInjectorSpanListener) 81 | 82 | assert.Equal(t, false, esl.InjectOnFinish) 83 | assert.Equal(t, int64(1), esl.InjectCountFreq) 84 | assert.Equal(t, true, esl.AddInfoTags) 85 | } 86 | -------------------------------------------------------------------------------- /tracer/filtering_span_listener.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import "log" 4 | 5 | type FilteringSpanListener struct { 6 | Listener ThundraSpanListener 7 | Filterer SpanFilterer 8 | } 9 | 10 | func (f *FilteringSpanListener) OnSpanStarted(span *spanImpl) { 11 | if f.Listener == nil { 12 | return 13 | } 14 | 15 | if f.Filterer == nil || f.Filterer.Accept(span) { 16 | f.Listener.OnSpanStarted(span) 17 | } 18 | } 19 | 20 | func (f *FilteringSpanListener) OnSpanFinished(span *spanImpl) { 21 | if f.Listener == nil { 22 | return 23 | } 24 | 25 | if f.Filterer == nil || f.Filterer.Accept(span) { 26 | f.Listener.OnSpanFinished(span) 27 | } 28 | } 29 | 30 | func (f *FilteringSpanListener) PanicOnError() bool { 31 | return true 32 | } 33 | 34 | // NewFilteringSpanListener creates and returns a new FilteringSpanListener from config 35 | func NewFilteringSpanListener(config map[string]interface{}) ThundraSpanListener { 36 | filterer := &ThundraSpanFilterer{spanFilters: []SpanFilter{}} 37 | 38 | listenerDef, ok := config["listener"].(map[string]interface{}) 39 | if !ok { 40 | log.Println("Listener configuration is not valid for FilteringSpanListener") 41 | return nil 42 | } 43 | 44 | listenerName, ok := listenerDef["type"].(string) 45 | listenerConstructor, ok := SpanListenerConstructorMap[listenerName] 46 | if !ok { 47 | log.Println("Given listener type is not valid for FilteringSpanListener") 48 | return nil 49 | } 50 | 51 | listenerConfig, ok := listenerDef["config"].(map[string]interface{}) 52 | listener := listenerConstructor(listenerConfig) 53 | 54 | if all, ok := config["all"].(bool); ok { 55 | filterer.all = all 56 | } 57 | 58 | if filterConfigs, ok := config["filters"].([]interface{}); ok { 59 | filterer.spanFilters = crateFiltersFromConfig(filterConfigs) 60 | } 61 | 62 | return &FilteringSpanListener{listener, filterer} 63 | } 64 | 65 | func crateFiltersFromConfig(filterConfigs []interface{}) []SpanFilter { 66 | filters := []SpanFilter{} 67 | for _, filterConfig := range filterConfigs { 68 | if filterConfig, ok := filterConfig.(map[string]interface{}); ok { 69 | if composite, ok := filterConfig["composite"].(bool); ok && composite { 70 | cf := &CompositeSpanFilter{ 71 | spanFilters: []SpanFilter{}, 72 | all: false, 73 | composite: true, 74 | } 75 | 76 | if all, ok := filterConfig["all"].(bool); ok { 77 | cf.all = all 78 | } 79 | 80 | if compositeFilterConfigs, ok := filterConfig["filters"].([]interface{}); ok { 81 | cf.spanFilters = crateFiltersFromConfig(compositeFilterConfigs) 82 | } 83 | 84 | filters = append(filters, cf) 85 | } else { 86 | filters = append(filters, NewThundraSpanFilter(filterConfig)) 87 | } 88 | } 89 | } 90 | 91 | return filters 92 | } 93 | -------------------------------------------------------------------------------- /tracer/latency_injector_span_listener.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 8 | ) 9 | 10 | var defaultDelay int64 = 100 11 | 12 | type LatencyInjectorSpanListener struct { 13 | Delay int64 14 | InjectOnFinish bool 15 | RandomizeDelay bool 16 | AddInfoTags bool 17 | } 18 | 19 | func (l *LatencyInjectorSpanListener) OnSpanStarted(span *spanImpl) { 20 | if !l.InjectOnFinish { 21 | l.injectDelay(span) 22 | } 23 | } 24 | 25 | func (l *LatencyInjectorSpanListener) OnSpanFinished(span *spanImpl) { 26 | if l.InjectOnFinish { 27 | l.injectDelay(span) 28 | } 29 | } 30 | 31 | func (l *LatencyInjectorSpanListener) PanicOnError() bool { 32 | return false 33 | } 34 | 35 | func (l *LatencyInjectorSpanListener) injectDelay(span *spanImpl) { 36 | delay := l.Delay 37 | if delay <= 0 { 38 | delay = defaultDelay 39 | } 40 | if l.RandomizeDelay { 41 | delay = rand.Int63n(delay) 42 | } 43 | if l.AddInfoTags { 44 | l.addInfoTags(span, delay) 45 | } 46 | time.Sleep(time.Duration(delay) * time.Millisecond) 47 | } 48 | 49 | func (l *LatencyInjectorSpanListener) addInfoTags(span *spanImpl, injectedDelay int64) { 50 | infoTags := map[string]interface{}{ 51 | "type": "latency_injecter_span_listener", 52 | "inject_on_finish": l.InjectOnFinish, 53 | "delay": l.Delay, 54 | "injected_delay": injectedDelay, 55 | } 56 | span.SetTag(constants.ThundraLambdaSpanListenerInfoTag, infoTags) 57 | } 58 | 59 | // NewLatencyInjectorSpanListener creates and returns a new LatencyInjectorSpanListener from config 60 | func NewLatencyInjectorSpanListener(config map[string]interface{}) ThundraSpanListener { 61 | spanListener := &LatencyInjectorSpanListener{Delay: defaultDelay, AddInfoTags: true} 62 | 63 | if delay, ok := config["delay"].(float64); ok { 64 | spanListener.Delay = int64(delay) 65 | } 66 | if injectOnFinish, ok := config["injectOnFinish"].(bool); ok { 67 | spanListener.InjectOnFinish = injectOnFinish 68 | } 69 | if randomizeDelay, ok := config["randomizeDelay"].(bool); ok { 70 | spanListener.RandomizeDelay = randomizeDelay 71 | } 72 | if addInfoTags, ok := config["addInfoTags"].(bool); ok { 73 | spanListener.AddInfoTags = addInfoTags 74 | } 75 | 76 | return spanListener 77 | } 78 | -------------------------------------------------------------------------------- /tracer/latency_injector_span_listener_test.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewLatencyInjectorFromConfig(t *testing.T) { 10 | config := map[string]interface{}{ 11 | "delay": float64(370), 12 | "injectOnFinish": true, 13 | "randomizeDelay": true, 14 | "addInfoTags": false, 15 | } 16 | 17 | lsl := NewLatencyInjectorSpanListener(config).(*LatencyInjectorSpanListener) 18 | 19 | assert.Equal(t, int64(370), lsl.Delay) 20 | assert.Equal(t, true, lsl.InjectOnFinish) 21 | assert.Equal(t, true, lsl.RandomizeDelay) 22 | assert.Equal(t, false, lsl.AddInfoTags) 23 | 24 | } 25 | 26 | func TestNewLatencyInjectorFromConfigWithTypeErrors(t *testing.T) { 27 | config := map[string]interface{}{ 28 | "injectOnFinish": 37, 29 | "delay": "foo", 30 | } 31 | 32 | lsl := NewLatencyInjectorSpanListener(config).(*LatencyInjectorSpanListener) 33 | 34 | assert.Equal(t, false, lsl.InjectOnFinish) 35 | assert.Equal(t, defaultDelay, lsl.Delay) 36 | assert.Equal(t, false, lsl.RandomizeDelay) 37 | assert.Equal(t, true, lsl.AddInfoTags) 38 | } 39 | -------------------------------------------------------------------------------- /tracer/raw_span.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 7 | 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/ext" 9 | 10 | ot "github.com/opentracing/opentracing-go" 11 | ) 12 | 13 | // RawSpan encapsulates all state associated with a (finished) Span. 14 | type RawSpan struct { 15 | Context SpanContext 16 | ParentSpanID string 17 | OperationName string 18 | StartTimestamp int64 19 | EndTimestamp int64 20 | DomainName string 21 | ClassName string 22 | Tags ot.Tags 23 | Logs []ot.LogRecord 24 | } 25 | 26 | // Duration calculates the spans duration 27 | func (s *RawSpan) Duration() int64 { 28 | if s.EndTimestamp != 0 { 29 | return s.EndTimestamp - s.StartTimestamp 30 | } 31 | 32 | return utils.GetTimestamp() - s.StartTimestamp 33 | } 34 | 35 | // GetTags filters the thundra tags and returns the remainings 36 | func (s *RawSpan) GetTags() ot.Tags { 37 | ft := ot.Tags{} 38 | 39 | for k, v := range s.Tags { 40 | if !strings.HasPrefix(k, ext.ThundraTagPrefix) { 41 | ft[k] = v 42 | } 43 | } 44 | 45 | return ft 46 | } 47 | 48 | // GetTag returns the value for key 49 | func (s *RawSpan) GetTag(key string) interface{} { 50 | return s.Tags[key] 51 | } 52 | -------------------------------------------------------------------------------- /tracer/recorder.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // SpanRecorder handles all of the `RawSpan` data generated via an 8 | // associated `Tracer` 9 | type SpanRecorder interface { 10 | GetSpans() []*RawSpan 11 | RecordSpan(span *RawSpan) 12 | Reset() 13 | } 14 | 15 | // InMemorySpanRecorder stores spans using a slice in a thread-safe way 16 | type InMemorySpanRecorder struct { 17 | sync.RWMutex 18 | spans []*RawSpan 19 | } 20 | 21 | // NewInMemoryRecorder creates new InMemorySpanRecorder 22 | func NewInMemoryRecorder() *InMemorySpanRecorder { 23 | return new(InMemorySpanRecorder) 24 | } 25 | 26 | // RecordSpan implements the respective method of SpanRecorder. 27 | func (r *InMemorySpanRecorder) RecordSpan(span *RawSpan) { 28 | r.Lock() 29 | defer r.Unlock() 30 | r.spans = append(r.spans, span) 31 | } 32 | 33 | // GetSpans returns a copy of the array of spans accumulated so far. 34 | func (r *InMemorySpanRecorder) GetSpans() []*RawSpan { 35 | r.RLock() 36 | defer r.RUnlock() 37 | spans := make([]*RawSpan, len(r.spans)) 38 | copy(spans, r.spans) 39 | return spans 40 | } 41 | 42 | // Reset clears the internal array of spans. 43 | func (r *InMemorySpanRecorder) Reset() { 44 | r.Lock() 45 | defer r.Unlock() 46 | r.spans = nil 47 | } 48 | -------------------------------------------------------------------------------- /tracer/security_aware_span_listener.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 8 | logger "log" 9 | ) 10 | 11 | var defaultSecurityMessage = "Operation was blocked due to security configuration" 12 | 13 | type SecurityAwareSpanListener struct { 14 | block bool 15 | whitelist *[]Operation 16 | blacklist *[]Operation 17 | } 18 | 19 | func (s *SecurityAwareSpanListener) OnSpanStarted(span *spanImpl) { 20 | if !s.isExternalOperation(span) { 21 | return 22 | } 23 | 24 | if s.blacklist != nil { 25 | for _, op := range *s.blacklist { 26 | if op.matches(span) { 27 | s.handleSecurityIssue(span) 28 | return 29 | } 30 | } 31 | } 32 | 33 | if s.whitelist != nil { 34 | for _, op := range *s.whitelist { 35 | if op.matches(span) { 36 | return 37 | } 38 | } 39 | s.handleSecurityIssue(span) 40 | } 41 | 42 | } 43 | 44 | func (s *SecurityAwareSpanListener) OnSpanFinished(span *spanImpl) { 45 | return 46 | } 47 | 48 | func (s *SecurityAwareSpanListener) PanicOnError() bool { 49 | return true 50 | } 51 | 52 | func (s *SecurityAwareSpanListener) handleSecurityIssue(span *spanImpl) { 53 | if s.block { 54 | err := errors.New(defaultSecurityMessage) 55 | span.SetTag(constants.SecurityTags["BLOCKED"], true) 56 | span.SetTag(constants.SecurityTags["VIOLATED"], true) 57 | utils.SetSpanError(span, err) 58 | panic(err) 59 | } else { 60 | span.SetTag(constants.SecurityTags["VIOLATED"], true) 61 | } 62 | } 63 | 64 | func (s *SecurityAwareSpanListener) isExternalOperation(span *spanImpl) bool { 65 | return span.raw.GetTag(constants.SpanTags["TOPOLOGY_VERTEX"]) == true 66 | } 67 | 68 | type Operation struct { 69 | ClassName string `json:"className"` 70 | Tags map[string][]string `json:"tags"` 71 | } 72 | 73 | func (o *Operation) matches(span *spanImpl) bool { 74 | var matched = true 75 | 76 | if o.ClassName != "" { 77 | matched = o.ClassName == "*" || o.ClassName == span.raw.ClassName 78 | } 79 | 80 | if matched && len(o.Tags) > 0 { 81 | for key, value := range o.Tags { 82 | if span.raw.GetTag(key) != nil { 83 | if utils.StringContains(value, "*") { 84 | continue 85 | } 86 | if !utils.StringContains(value, span.raw.GetTag(key).(string)) { 87 | matched = false 88 | break 89 | } 90 | } 91 | } 92 | } 93 | 94 | return matched 95 | } 96 | 97 | func NewSecurityAwareSpanListener(config map[string]interface{}) ThundraSpanListener { 98 | spanListener := &SecurityAwareSpanListener{} 99 | 100 | if block, ok := config["block"].(bool); ok { 101 | spanListener.block = block 102 | } 103 | 104 | if whitelist, ok := config["whitelist"].([]interface{}); ok { 105 | var wl []Operation 106 | for _, value := range whitelist { 107 | op := mapToOperation(value) 108 | wl = append(wl, op) 109 | } 110 | 111 | spanListener.whitelist = &wl 112 | } 113 | 114 | if blacklist, ok := config["blacklist"].([]interface{}); ok { 115 | var bl []Operation 116 | for _, value := range blacklist { 117 | op := mapToOperation(value) 118 | bl = append(bl, op) 119 | } 120 | spanListener.blacklist = &bl 121 | } 122 | 123 | return spanListener 124 | } 125 | 126 | func mapToOperation(opMap interface{}) Operation { 127 | jsonBody, err := json.Marshal(opMap) 128 | if err != nil { 129 | logger.Println("Error on marshal security operation:", err) 130 | return Operation{} 131 | } 132 | 133 | op := Operation{} 134 | if err := json.Unmarshal(jsonBody, &op); err != nil { 135 | logger.Println("Error on marshal security operation:", err) 136 | return op 137 | } 138 | 139 | return op 140 | } 141 | -------------------------------------------------------------------------------- /tracer/span_context.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | // SpanContext holds the basic Span metadata. 4 | type SpanContext struct { 5 | TransactionID string 6 | // A probabilistically unique identifier for a [multi-span] trace. 7 | TraceID string 8 | // A probabilistically unique identifier for a span. 9 | SpanID string 10 | // The span's associated baggage. 11 | Baggage map[string]string 12 | } 13 | 14 | // ForeachBaggageItem belongs to the opentracing.SpanContext interface 15 | func (c SpanContext) ForeachBaggageItem(handler func(k, v string) bool) { 16 | for k, v := range c.Baggage { 17 | if !handler(k, v) { 18 | break 19 | } 20 | } 21 | } 22 | 23 | // WithBaggageItem returns an entirely new basictracer SpanContext with the 24 | // given key:value baggage pair set. 25 | func (c SpanContext) WithBaggageItem(key, val string) SpanContext { 26 | var newBaggage map[string]string 27 | if c.Baggage == nil { 28 | newBaggage = map[string]string{key: val} 29 | } else { 30 | newBaggage = make(map[string]string, len(c.Baggage)+1) 31 | for k, v := range c.Baggage { 32 | newBaggage[k] = v 33 | } 34 | newBaggage[key] = val 35 | } 36 | // Use positional parameters so the compiler will help catch new fields. 37 | return SpanContext{c.TransactionID, c.TraceID, c.SpanID, newBaggage} 38 | } 39 | -------------------------------------------------------------------------------- /tracer/span_filter.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "fmt" 5 | 6 | ot "github.com/opentracing/opentracing-go" 7 | ) 8 | 9 | type SpanFilter interface { 10 | Accept(*spanImpl) bool 11 | } 12 | 13 | type SpanFilterer interface { 14 | Accept(*spanImpl) bool 15 | } 16 | 17 | type ThundraSpanFilterer struct { 18 | spanFilters []SpanFilter 19 | all bool 20 | } 21 | 22 | type ThundraSpanFilter struct { 23 | DomainName string 24 | ClassName string 25 | OperationName string 26 | Reverse bool 27 | Tags ot.Tags 28 | } 29 | 30 | type CompositeSpanFilter struct { 31 | spanFilters []SpanFilter 32 | all bool 33 | composite bool 34 | } 35 | 36 | func (f *CompositeSpanFilter) Accept(span *spanImpl) bool { 37 | res := f.all 38 | for _, sf := range f.spanFilters { 39 | if f.all { 40 | res = res && sf.Accept(span) 41 | } else { 42 | res = res || sf.Accept(span) 43 | } 44 | } 45 | return res 46 | } 47 | 48 | func (t *ThundraSpanFilterer) Accept(span *spanImpl) bool { 49 | res := t.all 50 | for _, sf := range t.spanFilters { 51 | if t.all { 52 | res = res && sf.Accept(span) 53 | } else { 54 | res = res || sf.Accept(span) 55 | } 56 | } 57 | return res 58 | } 59 | 60 | func (t *ThundraSpanFilterer) AddFilter(sf SpanFilter) { 61 | t.spanFilters = append(t.spanFilters, sf) 62 | } 63 | 64 | func (t *ThundraSpanFilterer) ClearFilters() { 65 | t.spanFilters = []SpanFilter{} 66 | } 67 | 68 | func (t *ThundraSpanFilter) Accept(span *spanImpl) bool { 69 | accepted := true 70 | if span == nil { 71 | return accepted 72 | } 73 | 74 | if t.DomainName != "" { 75 | accepted = (t.DomainName == span.raw.DomainName) 76 | } 77 | 78 | if accepted && t.ClassName != "" { 79 | accepted = (t.ClassName == span.raw.ClassName) 80 | } 81 | 82 | if accepted && t.OperationName != "" { 83 | accepted = (t.OperationName == span.raw.OperationName) 84 | } 85 | 86 | if accepted && t.Tags != nil { 87 | for k, v := range t.Tags { 88 | if fmt.Sprintf("%v", span.raw.GetTag(k)) != fmt.Sprintf("%v", v) { 89 | accepted = false 90 | break 91 | } 92 | } 93 | } 94 | 95 | if t.Reverse { 96 | return !accepted 97 | } 98 | 99 | return accepted 100 | } 101 | 102 | // NewThundraSpanFilter creates and returns a new ThundraSpanFilter from config 103 | func NewThundraSpanFilter(config map[string]interface{}) *ThundraSpanFilter { 104 | spanFilter := ThundraSpanFilter{} 105 | 106 | if domainName, ok := config["domainName"].(string); ok { 107 | spanFilter.DomainName = domainName 108 | } 109 | if className, ok := config["className"].(string); ok { 110 | spanFilter.ClassName = className 111 | } 112 | if operationName, ok := config["operationName"].(string); ok { 113 | spanFilter.OperationName = operationName 114 | } 115 | if reverse, ok := config["reverse"].(bool); ok { 116 | spanFilter.Reverse = reverse 117 | } 118 | if tags, ok := config["tags"].(map[string]interface{}); ok { 119 | spanFilter.Tags = tags 120 | } 121 | 122 | return &spanFilter 123 | } 124 | -------------------------------------------------------------------------------- /tracer/span_listener.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | type ThundraSpanListener interface { 4 | OnSpanStarted(*spanImpl) 5 | OnSpanFinished(*spanImpl) 6 | PanicOnError() bool 7 | } 8 | -------------------------------------------------------------------------------- /tracer/span_test.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSetOperationName(t *testing.T) { 10 | tracer, r := newTracerAndRecorder() 11 | 12 | s := tracer.StartSpan("foo") 13 | s.SetOperationName("bar") 14 | s.Finish() 15 | 16 | rs := r.GetSpans()[0] 17 | 18 | assert.True(t, rs.OperationName == "bar") 19 | } 20 | 21 | func TestSetTag(t *testing.T) { 22 | tracer, r := newTracerAndRecorder() 23 | 24 | s := tracer.StartSpan("spiderman") 25 | s.SetTag("peter", "parker") 26 | s.SetTag("mary", "jane") 27 | s.Finish() 28 | 29 | rs := r.GetSpans()[0] 30 | 31 | tags := rs.Tags 32 | 33 | assert.True(t, rs.OperationName == "spiderman") 34 | assert.True(t, tags["peter"] == "parker") 35 | assert.True(t, tags["mary"] == "jane") 36 | } 37 | 38 | func TestSetParent(t *testing.T) { 39 | tracer, _ := newTracerAndRecorder() 40 | 41 | ps := tracer.StartSpan("parentSpan") 42 | cs := tracer.StartSpan("childSpan") 43 | 44 | psi, ok := ps.(*spanImpl) 45 | assert.True(t, ok) 46 | 47 | csi, ok := cs.(*spanImpl) 48 | assert.True(t, ok) 49 | 50 | csi.setParent(psi.raw.Context) 51 | 52 | assert.True(t, csi.raw.ParentSpanID == psi.raw.Context.SpanID) 53 | } 54 | 55 | func TestLog(t *testing.T) { 56 | tracer, r := newTracerAndRecorder() 57 | 58 | s := tracer.StartSpan("foo") 59 | s.LogKV( 60 | "intKey", 37, 61 | "boolKey", true, 62 | "stringKey", "foo", 63 | ) 64 | s.Finish() 65 | 66 | rs := r.GetSpans()[0] 67 | logFields := rs.Logs[0].Fields 68 | 69 | assert.True(t, logFields[0].Key() == "intKey" && logFields[0].Value() == 37) 70 | assert.True(t, logFields[1].Key() == "boolKey" && logFields[1].Value() == true) 71 | assert.True(t, logFields[2].Key() == "stringKey" && logFields[2].Value() == "foo") 72 | } 73 | -------------------------------------------------------------------------------- /tracer/tag_injector_span_listener.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | type TagInjectorSpanListener struct { 4 | tags map[string]interface{} 5 | injectOnFinish bool 6 | } 7 | 8 | func (t *TagInjectorSpanListener) OnSpanStarted(span *spanImpl) { 9 | if !t.injectOnFinish { 10 | t.injectTags(span) 11 | } 12 | } 13 | 14 | func (t *TagInjectorSpanListener) OnSpanFinished(span *spanImpl) { 15 | if t.injectOnFinish { 16 | t.injectTags(span) 17 | } 18 | } 19 | 20 | func (t *TagInjectorSpanListener) injectTags(span *spanImpl) { 21 | if t.tags == nil { 22 | return 23 | } 24 | 25 | for k, v := range t.tags { 26 | span.SetTag(k, v) 27 | } 28 | } 29 | 30 | func (t *TagInjectorSpanListener) PanicOnError() bool { 31 | return false 32 | } 33 | 34 | func NewTagInjectorSpanListener(config map[string]interface{}) ThundraSpanListener { 35 | listener := &TagInjectorSpanListener{tags: map[string]interface{}{}, injectOnFinish: false} 36 | if injectOnFinish, ok := config["injectOnFinish"].(bool); ok { 37 | listener.injectOnFinish = injectOnFinish 38 | } 39 | if tags, ok := config["tags"].(map[string]interface{}); ok { 40 | listener.tags = tags 41 | } 42 | 43 | return listener 44 | } 45 | -------------------------------------------------------------------------------- /tracer/tracer.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | ot "github.com/opentracing/opentracing-go" 5 | "github.com/pkg/errors" 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/ext" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/plugin" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 10 | ) 11 | 12 | // New creates and returns a standard Tracer which defers completed Spans to 13 | // `recorder`. 14 | func New(recorder SpanRecorder) ot.Tracer { 15 | return &tracerImpl{ 16 | Recorder: recorder, 17 | } 18 | } 19 | 20 | type tracerImpl struct { 21 | Recorder SpanRecorder 22 | } 23 | 24 | // StartSpan starts a new span with options and returns it. 25 | func (t *tracerImpl) StartSpan(operationName string, opts ...ot.StartSpanOption) ot.Span { 26 | sso := ot.StartSpanOptions{} 27 | for _, o := range opts { 28 | o.Apply(&sso) 29 | } 30 | return t.StartSpanWithOptions(operationName, sso) 31 | } 32 | 33 | func (t *tracerImpl) StartSpanWithOptions(operationName string, opts ot.StartSpanOptions) ot.Span { 34 | newSpan := t.getSpan() 35 | 36 | newSpan.tracer = t 37 | newSpan.raw.Tags = opts.Tags 38 | newSpan.raw.Logs = []ot.LogRecord{} 39 | newSpan.raw.OperationName = operationName 40 | newSpan.raw.Context.TransactionID = plugin.TransactionID 41 | newSpan.raw.Context.TraceID = plugin.TraceID 42 | newSpan.raw.Context.SpanID = utils.GenerateNewID() 43 | 44 | for _, ref := range opts.References { 45 | if ref.Type == ot.ChildOfRef { 46 | parentCtx := ref.ReferencedContext.(SpanContext) 47 | newSpan.setParent(parentCtx) 48 | } 49 | } 50 | 51 | if opts.StartTime.IsZero() { 52 | newSpan.raw.StartTimestamp = utils.GetTimestamp() 53 | } else { 54 | newSpan.raw.StartTimestamp = utils.TimeToMs(opts.StartTime) 55 | } 56 | 57 | className, ok := opts.Tags[ext.ClassNameKey] 58 | if ok { 59 | classNameStr, ok := className.(string) 60 | if ok { 61 | newSpan.raw.ClassName = classNameStr 62 | } else { 63 | newSpan.raw.ClassName = constants.DefaultClassName 64 | } 65 | } else { 66 | newSpan.raw.ClassName = constants.DefaultClassName 67 | } 68 | 69 | domainName, ok := opts.Tags[ext.DomainNameKey] 70 | if ok { 71 | domainNameStr, ok := domainName.(string) 72 | if ok { 73 | newSpan.raw.DomainName = domainNameStr 74 | } else { 75 | newSpan.raw.DomainName = constants.DefaultDomainName 76 | } 77 | } else { 78 | newSpan.raw.DomainName = constants.DefaultDomainName 79 | } 80 | 81 | // Add to recorder 82 | t.Recorder.RecordSpan(&newSpan.raw) 83 | return newSpan 84 | } 85 | 86 | func (t *tracerImpl) getSpan() *spanImpl { 87 | return &spanImpl{} 88 | } 89 | 90 | // TODO Will be implemented 91 | func (t *tracerImpl) Inject(sc ot.SpanContext, format interface{}, carrier interface{}) error { 92 | return errors.New("Inject has not been supported yet") 93 | } 94 | 95 | // TODO Will be implemented 96 | func (t *tracerImpl) Extract(format interface{}, carrier interface{}) (ot.SpanContext, error) { 97 | return nil, errors.New("Extract has not been supported yet") 98 | } 99 | 100 | func (t *tracerImpl) AddSpanListener(listener ThundraSpanListener) { 101 | RegisterSpanListener(listener) 102 | } 103 | 104 | func (t *tracerImpl) GetSpanListeners() []ThundraSpanListener { 105 | return GetSpanListeners() 106 | } 107 | -------------------------------------------------------------------------------- /tracer/tracer_support.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/base64" 7 | "encoding/json" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "strings" 12 | 13 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 14 | ) 15 | 16 | var spanListeners = make([]ThundraSpanListener, 0) 17 | 18 | var SpanListenerConstructorMap = make(map[string]func(map[string]interface{}) ThundraSpanListener, 0) 19 | 20 | func GetSpanListeners() []ThundraSpanListener { 21 | return spanListeners 22 | } 23 | 24 | func RegisterSpanListener(listener ThundraSpanListener) { 25 | spanListeners = append(spanListeners, listener) 26 | } 27 | 28 | func ClearSpanListeners() { 29 | spanListeners = make([]ThundraSpanListener, 0) 30 | } 31 | 32 | func ParseSpanListeners() { 33 | ClearSpanListeners() 34 | 35 | for _, env := range os.Environ() { 36 | if strings.HasPrefix(env, constants.ThundraLambdaSpanListener) { 37 | config := make(map[string]interface{}) 38 | splits := strings.SplitN(env, "=", 2) 39 | 40 | if len(splits) < 2 { 41 | continue 42 | } 43 | 44 | var err error 45 | configStr := splits[1] 46 | 47 | if !strings.HasPrefix(configStr, "{") { 48 | configStr, err = decodeConfigStr(configStr) 49 | if err != nil { 50 | log.Println("Couldn't parse given span listener configuration:", err) 51 | continue 52 | } 53 | } 54 | 55 | if err := json.Unmarshal([]byte(configStr), &config); err != nil { 56 | log.Println("Given span listener configuration is not a valid JSON string:", err) 57 | continue 58 | } 59 | 60 | listenerName, ok := config["type"].(string) 61 | if !ok { 62 | log.Println("Given listener type is not a valid span listener") 63 | continue 64 | } 65 | 66 | listenerConfig, ok := config["config"].(map[string]interface{}) 67 | if !ok { 68 | log.Println("No config given for the span listener") 69 | } 70 | 71 | listenerConstructor, ok := SpanListenerConstructorMap[listenerName] 72 | if !ok { 73 | log.Println("Given listener type is not a valid span listener") 74 | continue 75 | } 76 | 77 | listener := listenerConstructor(listenerConfig) 78 | 79 | if listener != nil { 80 | RegisterSpanListener(listener) 81 | } 82 | } 83 | } 84 | } 85 | 86 | func decodeConfigStr(configStr string) (string, error) { 87 | z, err := base64.StdEncoding.DecodeString(configStr) 88 | if err != nil { 89 | return "", err 90 | } 91 | 92 | r, err := gzip.NewReader(bytes.NewReader(z)) 93 | if err != nil { 94 | return "", err 95 | } 96 | 97 | result, err := ioutil.ReadAll(r) 98 | if err != nil { 99 | return "", err 100 | } 101 | 102 | return string(result), nil 103 | } 104 | 105 | func init() { 106 | SpanListenerConstructorMap["ErrorInjectorSpanListener"] = NewErrorInjectorSpanListener 107 | SpanListenerConstructorMap["LatencyInjectorSpanListener"] = NewLatencyInjectorSpanListener 108 | SpanListenerConstructorMap["FilteringSpanListener"] = NewFilteringSpanListener 109 | SpanListenerConstructorMap["TagInjectorSpanListener"] = NewTagInjectorSpanListener 110 | SpanListenerConstructorMap["SecurityAwareSpanListener"] = NewSecurityAwareSpanListener 111 | ParseSpanListeners() 112 | } 113 | -------------------------------------------------------------------------------- /tracer/tracer_test.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/ext" 9 | 10 | opentracing "github.com/opentracing/opentracing-go" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | const ( 15 | duration = 10 16 | operationName = "creating-bubble" 17 | className = "Test Class" 18 | domainName = "Test Domain" 19 | ) 20 | 21 | func TestStartSpan(t *testing.T) { 22 | tracer, r := newTracerAndRecorder() 23 | 24 | f := func() { 25 | span := tracer.StartSpan(operationName) 26 | defer span.Finish() 27 | time.Sleep(time.Millisecond * duration) 28 | } 29 | 30 | f() 31 | 32 | spans := r.GetSpans() 33 | span := spans[0] 34 | 35 | assert.True(t, len(spans) == 1) 36 | assert.True(t, span.Duration() >= int64(duration)) 37 | assert.True(t, span.OperationName == operationName) 38 | assert.True(t, span.ClassName == constants.DefaultClassName) 39 | assert.True(t, span.DomainName == constants.DefaultDomainName) 40 | } 41 | 42 | func TestStartSpanWithOptions(t *testing.T) { 43 | tracer, r := newTracerAndRecorder() 44 | 45 | f := func() { 46 | span := tracer.StartSpan( 47 | operationName, 48 | ext.ClassName(className), 49 | ext.DomainName(domainName), 50 | opentracing.Tag{Key: "stage", Value: "testing"}, 51 | ) 52 | defer span.Finish() 53 | 54 | time.Sleep(time.Millisecond * duration) 55 | } 56 | 57 | f() 58 | spans := r.GetSpans() 59 | span := spans[0] 60 | 61 | assert.True(t, len(spans) == 1) 62 | assert.True(t, span.Duration() >= int64(duration)) 63 | assert.True(t, span.OperationName == operationName) 64 | assert.True(t, span.ClassName == className) 65 | assert.True(t, span.DomainName == domainName) 66 | assert.True(t, len(span.GetTags()) == 1) 67 | assert.True(t, span.GetTags()["stage"] == "testing") 68 | } 69 | 70 | func TestParentChildRelation(t *testing.T) { 71 | tracer, r := newTracerAndRecorder() 72 | 73 | f := func() { 74 | parentSpan := tracer.StartSpan("parentSpan") 75 | time.Sleep(time.Millisecond * duration) 76 | childSpan := tracer.StartSpan("childSpan", opentracing.ChildOf(parentSpan.Context())) 77 | time.Sleep(time.Millisecond * duration) 78 | childSpan.Finish() 79 | time.Sleep(time.Millisecond * duration) 80 | parentSpan.Finish() 81 | } 82 | 83 | f() 84 | 85 | spans := r.GetSpans() 86 | parentSpan, childSpan := spans[0], spans[1] 87 | 88 | assert.True(t, len(spans) == 2) 89 | assert.True(t, parentSpan.OperationName == "parentSpan") 90 | assert.True(t, childSpan.OperationName == "childSpan") 91 | assert.True(t, parentSpan.ParentSpanID == "") 92 | assert.True(t, childSpan.ParentSpanID == parentSpan.Context.SpanID) 93 | assert.True(t, childSpan.Context.TraceID == parentSpan.Context.TraceID) 94 | assert.True(t, childSpan.Duration() >= duration) 95 | assert.True(t, parentSpan.Duration() >= 3*duration) 96 | } 97 | 98 | func newTracerAndRecorder() (opentracing.Tracer, *InMemorySpanRecorder) { 99 | r := NewInMemoryRecorder() 100 | tracer := New(r) 101 | 102 | return tracer, r 103 | } 104 | -------------------------------------------------------------------------------- /wrappers/apexgateway/thundra_apex_gateway.go: -------------------------------------------------------------------------------- 1 | package apexgateway 2 | 3 | import ( 4 | "context" 5 | "github.com/apex/gateway" 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/thundra" 7 | "net/http" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | "github.com/aws/aws-lambda-go/lambda" 11 | ) 12 | 13 | func ListenAndServe(h http.Handler) error { 14 | if h == nil { 15 | h = http.DefaultServeMux 16 | } 17 | 18 | lambda.Start(thundra.Wrap(wrapper(h))) 19 | 20 | return nil 21 | } 22 | 23 | func wrapper(h http.Handler) func(ctx context.Context, e events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 24 | return func(ctx context.Context, e events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 25 | r, err := gateway.NewRequest(ctx, e) 26 | if err != nil { 27 | return events.APIGatewayProxyResponse{}, err 28 | } 29 | 30 | w := gateway.NewResponse() 31 | h.ServeHTTP(w, r) 32 | return w.End(), nil 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /wrappers/apexgatewayv2/thundra_apex_gateway_v2.go: -------------------------------------------------------------------------------- 1 | package apexgatewayv2 2 | 3 | import ( 4 | "context" 5 | "github.com/apex/gateway/v2" 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/thundra" 7 | "net/http" 8 | 9 | "github.com/aws/aws-lambda-go/events" 10 | "github.com/aws/aws-lambda-go/lambda" 11 | ) 12 | 13 | func ListenAndServe(h http.Handler) error { 14 | if h == nil { 15 | h = http.DefaultServeMux 16 | } 17 | 18 | lambda.Start(thundra.Wrap(wrapper(h))) 19 | 20 | return nil 21 | } 22 | 23 | func wrapper(h http.Handler) func(ctx context.Context, e events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 24 | return func(ctx context.Context, e events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { 25 | r, err := gateway.NewRequest(ctx, e) 26 | if err != nil { 27 | return events.APIGatewayV2HTTPResponse{}, err 28 | } 29 | 30 | w := gateway.NewResponse() 31 | h.ServeHTTP(w, r) 32 | 33 | return w.End(), nil 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /wrappers/aws/aws.go: -------------------------------------------------------------------------------- 1 | package thundraaws 2 | 3 | import ( 4 | opentracing "github.com/opentracing/opentracing-go" 5 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/aws/aws-sdk-go/aws/request" 12 | "github.com/aws/aws-sdk-go/aws/session" 13 | ) 14 | 15 | // Wrap wraps the given session object and adds necessary 16 | // handlers to create a span for the AWS call 17 | func Wrap(s *session.Session) *session.Session { 18 | if !config.AwsIntegrationDisabled && s != nil { 19 | s.Handlers.Validate.PushFrontNamed( 20 | request.NamedHandler{ 21 | Name: "github.com/thundra-io/thundra-lambda-agent-go/v2/wrappers/aws/aws.go/validateHandler", 22 | Fn: validateHandler, 23 | }, 24 | ) 25 | 26 | s.Handlers.Complete.PushFrontNamed( 27 | request.NamedHandler{ 28 | Name: "github.com/thundra-io/thundra-lambda-agent-go/v2/wrappers/aws/aws.go/completeHandler", 29 | Fn: completeHandler, 30 | }, 31 | ) 32 | } 33 | return s 34 | } 35 | 36 | func validateHandler(r *request.Request) { 37 | serviceID := r.ClientInfo.ServiceID 38 | i, ok := integrations[serviceID] 39 | if !ok { 40 | i = newAWSServiceIntegration(serviceID) 41 | } 42 | span, ctxWithSpan := opentracing.StartSpanFromContext(r.Context(), i.getOperationName(r)) 43 | r.SetContext(ctxWithSpan) 44 | rawSpan, ok := tracer.GetRaw(span) 45 | if !ok { 46 | return 47 | } 48 | i.beforeCall(r, rawSpan) 49 | tracer.OnSpanStarted(span) 50 | } 51 | 52 | func completeHandler(r *request.Request) { 53 | i, ok := integrations[r.ClientInfo.ServiceID] 54 | if !ok { 55 | return 56 | } 57 | span := opentracing.SpanFromContext(r.Context()) 58 | if span == nil { 59 | return 60 | } 61 | rawSpan, ok := tracer.GetRaw(span) 62 | if !ok { 63 | return 64 | } 65 | i.afterCall(r, rawSpan) 66 | if r.Error != nil { 67 | utils.SetSpanError(span, r.Error) 68 | } 69 | span.Finish() 70 | } 71 | 72 | func getOperationType(operationName string, className string) string { 73 | operationName = strings.Title(operationName) 74 | if class, ok := awsOperationTypesExclusions[className]; ok { 75 | if exclusion, ok := class[operationName]; ok { 76 | return exclusion 77 | } 78 | } 79 | 80 | for pattern := range awsOperationTypesPatterns { 81 | if r, ok := compiledTypes[pattern]; ok { 82 | if r.MatchString(operationName) { 83 | return awsOperationTypesPatterns[pattern] 84 | } 85 | } else { 86 | r, _ := regexp.Compile(pattern) 87 | compiledTypes[pattern] = *r 88 | if r.MatchString(operationName) { 89 | return awsOperationTypesPatterns[pattern] 90 | } 91 | } 92 | } 93 | 94 | return "" 95 | } 96 | -------------------------------------------------------------------------------- /wrappers/aws/default.go: -------------------------------------------------------------------------------- 1 | package thundraaws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/request" 5 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 7 | ) 8 | 9 | type defaultAWSIntegration struct { 10 | ServiceName string 11 | } 12 | 13 | func newAWSServiceIntegration(serviceName string) *defaultAWSIntegration { 14 | return &defaultAWSIntegration{ 15 | ServiceName: serviceName, 16 | } 17 | } 18 | 19 | func (i *defaultAWSIntegration) getOperationName(r *request.Request) string { 20 | return constants.AWSServiceRequest 21 | } 22 | 23 | func (i *defaultAWSIntegration) beforeCall(r *request.Request, span *tracer.RawSpan) { 24 | span.ClassName = constants.ClassNames["AWSSERVICE"] 25 | span.DomainName = constants.DomainNames["AWS"] 26 | 27 | tags := map[string]interface{}{ 28 | constants.AwsSDKTags["SERVICE_NAME"]: i.ServiceName, 29 | constants.AwsSDKTags["REQUEST_NAME"]: r.Operation.Name, 30 | } 31 | span.Tags = tags 32 | } 33 | 34 | func (i *defaultAWSIntegration) afterCall(r *request.Request, span *tracer.RawSpan) { 35 | 36 | } 37 | -------------------------------------------------------------------------------- /wrappers/aws/firehose.go: -------------------------------------------------------------------------------- 1 | package thundraaws 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "encoding/json" 7 | "reflect" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/aws/aws-sdk-go/aws/request" 12 | "github.com/aws/aws-sdk-go/service/firehose" 13 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 14 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 15 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 16 | ) 17 | 18 | type firehoseIntegration struct{} 19 | 20 | func (i *firehoseIntegration) getDeliveryStreamName(r *request.Request) string { 21 | fields := struct { 22 | DeliveryStreamName string 23 | }{} 24 | m, err := json.Marshal(r.Params) 25 | if err != nil { 26 | return "" 27 | } 28 | if err = json.Unmarshal(m, &fields); err != nil { 29 | return "" 30 | } 31 | if len(fields.DeliveryStreamName) > 0 { 32 | return fields.DeliveryStreamName 33 | } 34 | return "" 35 | } 36 | 37 | func (i *firehoseIntegration) getOperationName(r *request.Request) string { 38 | dsn := i.getDeliveryStreamName(r) 39 | if len(dsn) > 0 { 40 | return dsn 41 | } 42 | return constants.AWSServiceRequest 43 | } 44 | 45 | func (i *firehoseIntegration) beforeCall(r *request.Request, span *tracer.RawSpan) { 46 | span.ClassName = constants.ClassNames["FIREHOSE"] 47 | span.DomainName = constants.DomainNames["STREAM"] 48 | 49 | operationName := r.Operation.Name 50 | operationType := getOperationType(operationName, constants.ClassNames["FIREHOSE"]) 51 | 52 | tags := map[string]interface{}{ 53 | constants.AwsFirehoseTags["STREAM_NAME"]: i.getDeliveryStreamName(r), 54 | constants.SpanTags["OPERATION_TYPE"]: operationType, 55 | constants.AwsSDKTags["REQUEST_NAME"]: operationName, 56 | constants.SpanTags["TOPOLOGY_VERTEX"]: true, 57 | constants.SpanTags["TRIGGER_OPERATION_NAMES"]: []string{application.FunctionName}, 58 | constants.SpanTags["TRIGGER_DOMAIN_NAME"]: constants.AwsLambdaApplicationDomain, 59 | constants.SpanTags["TRIGGER_CLASS_NAME"]: constants.AwsLambdaApplicationClass, 60 | } 61 | 62 | span.Tags = tags 63 | } 64 | 65 | func (i *firehoseIntegration) afterCall(r *request.Request, span *tracer.RawSpan) { 66 | 67 | traceLinks := i.getTraceLinks(r.Operation.Name, r) 68 | if traceLinks != nil { 69 | span.Tags[constants.SpanTags["TRACE_LINKS"]] = traceLinks 70 | } 71 | } 72 | 73 | func getTimeStamp(dateStr string) int64 { 74 | date, err := time.Parse(time.RFC1123, dateStr) 75 | timestamp := time.Now().Unix() - 1 76 | if err == nil { 77 | timestamp = date.Unix() 78 | } 79 | return timestamp 80 | } 81 | 82 | func (i *firehoseIntegration) getTraceLinks(operationName string, r *request.Request) []string { 83 | requestValue := reflect.ValueOf(r.Params) 84 | if requestValue == (reflect.Value{}) { 85 | return nil 86 | } 87 | 88 | streamName := i.getDeliveryStreamName(r) 89 | region := "" 90 | dateStr := "" 91 | 92 | if r.Config.Region != nil { 93 | region = *r.Config.Region 94 | } 95 | if r.HTTPResponse != nil && r.HTTPResponse.Header != nil { 96 | dateStr = r.HTTPResponse.Header.Get("date") 97 | } 98 | 99 | if operationName == "PutRecord" { 100 | if recordInput, ok := requestValue.Elem().Interface().(firehose.PutRecordInput); ok { 101 | if recordInput.Record != nil { 102 | data := recordInput.Record.Data 103 | return i.generateTraceLinks(region, dateStr, data, streamName) 104 | } 105 | } 106 | } else if operationName == "PutRecordBatch" { 107 | records := requestValue.Elem().FieldByName("Records") 108 | if records != (reflect.Value{}) { 109 | var links []string 110 | for j := 0; j < records.Len(); j++ { 111 | if record, ok := records.Index(j).Elem().Interface().(firehose.Record); ok { 112 | linksForRecord := i.generateTraceLinks(region, dateStr, record.Data, streamName) 113 | for _, link := range linksForRecord { 114 | links = append(links, link) 115 | } 116 | } 117 | } 118 | return links 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | func (i *firehoseIntegration) generateTraceLinks(region string, dateStr string, data []byte, streamName string) []string { 125 | var traceLinks []string 126 | timestamp := getTimeStamp(dateStr) 127 | 128 | b := md5.Sum(data) 129 | dataMD5 := hex.EncodeToString(b[:]) 130 | 131 | for j := 0; j < 3; j++ { 132 | traceLinks = append(traceLinks, region+":"+streamName+":"+strconv.FormatInt(timestamp+int64(j), 10)+":"+dataMD5) 133 | } 134 | 135 | return traceLinks 136 | } 137 | 138 | func init() { 139 | integrations["Firehose"] = &firehoseIntegration{} 140 | } 141 | -------------------------------------------------------------------------------- /wrappers/aws/integration.go: -------------------------------------------------------------------------------- 1 | package thundraaws 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/request" 5 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 6 | ) 7 | 8 | type integration interface { 9 | beforeCall(r *request.Request, span *tracer.RawSpan) 10 | afterCall(r *request.Request, span *tracer.RawSpan) 11 | getOperationName(r *request.Request) string 12 | } 13 | 14 | var integrations = make(map[string]integration, 9) 15 | -------------------------------------------------------------------------------- /wrappers/aws/kinesis.go: -------------------------------------------------------------------------------- 1 | package thundraaws 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | 7 | "github.com/aws/aws-sdk-go/aws/request" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 12 | ) 13 | 14 | type kinesisIntegration struct{} 15 | 16 | func (i *kinesisIntegration) getStreamName(r *request.Request) string { 17 | fields := struct { 18 | StreamName string 19 | }{} 20 | m, err := json.Marshal(r.Params) 21 | if err != nil { 22 | return "" 23 | } 24 | if err = json.Unmarshal(m, &fields); err != nil { 25 | return "" 26 | } 27 | if len(fields.StreamName) > 0 { 28 | return fields.StreamName 29 | } 30 | return "" 31 | } 32 | 33 | func (i *kinesisIntegration) getOperationName(r *request.Request) string { 34 | streamName := i.getStreamName(r) 35 | if len(streamName) > 0 { 36 | return streamName 37 | } 38 | return constants.AWSServiceRequest 39 | } 40 | 41 | func (i *kinesisIntegration) beforeCall(r *request.Request, span *tracer.RawSpan) { 42 | span.ClassName = constants.ClassNames["KINESIS"] 43 | span.DomainName = constants.DomainNames["STREAM"] 44 | 45 | operationName := r.Operation.Name 46 | operationType := getOperationType(operationName, constants.ClassNames["KINESIS"]) 47 | 48 | tags := map[string]interface{}{ 49 | constants.AwsKinesisTags["STREAM_NAME"]: i.getStreamName(r), 50 | constants.SpanTags["OPERATION_TYPE"]: operationType, 51 | constants.AwsSDKTags["REQUEST_NAME"]: operationName, 52 | constants.SpanTags["TOPOLOGY_VERTEX"]: true, 53 | constants.SpanTags["TRIGGER_OPERATION_NAMES"]: []string{application.FunctionName}, 54 | constants.SpanTags["TRIGGER_DOMAIN_NAME"]: constants.AwsLambdaApplicationDomain, 55 | constants.SpanTags["TRIGGER_CLASS_NAME"]: constants.AwsLambdaApplicationClass, 56 | } 57 | 58 | span.Tags = tags 59 | } 60 | 61 | func (i *kinesisIntegration) afterCall(r *request.Request, span *tracer.RawSpan) { 62 | traceLinks := i.getTraceLinks(r) 63 | if traceLinks != nil { 64 | span.Tags[constants.SpanTags["TRACE_LINKS"]] = traceLinks 65 | } 66 | } 67 | 68 | func (i *kinesisIntegration) getTraceLinks(r *request.Request) []string { 69 | responseValue := reflect.ValueOf(r.Data) 70 | 71 | if responseValue == (reflect.Value{}) { 72 | return nil 73 | } 74 | 75 | records := responseValue.Elem().FieldByName("Records") 76 | region := "" 77 | streamName := i.getStreamName(r) 78 | 79 | if r.Config.Region != nil { 80 | region = *r.Config.Region 81 | } 82 | 83 | if records != (reflect.Value{}) { 84 | var links []string 85 | for j := 0; j < records.Len(); j++ { 86 | record := records.Index(j).Elem() 87 | if sequenceNumber, ok := utils.GetStringFieldFromValue(record, "SequenceNumber"); ok { 88 | if shardID, ok := utils.GetStringFieldFromValue(record, "ShardId"); ok { 89 | links = append(links, region+":"+streamName+":"+shardID+":"+sequenceNumber) 90 | } 91 | } 92 | } 93 | return links 94 | } 95 | if sequenceNumber, ok := utils.GetStringFieldFromValue(responseValue.Elem(), "SequenceNumber"); ok { 96 | if shardID, ok := utils.GetStringFieldFromValue(responseValue.Elem(), "ShardId"); ok { 97 | return []string{region + ":" + streamName + ":" + shardID + ":" + sequenceNumber} 98 | } 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func init() { 105 | integrations["Kinesis"] = &kinesisIntegration{} 106 | } 107 | -------------------------------------------------------------------------------- /wrappers/aws/lambda.go: -------------------------------------------------------------------------------- 1 | package thundraaws 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "strings" 7 | 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 11 | 12 | "github.com/aws/aws-lambda-go/lambdacontext" 13 | "github.com/aws/aws-sdk-go/aws/request" 14 | "github.com/aws/aws-sdk-go/service/lambda" 15 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 16 | ) 17 | 18 | type lambdaIntegration struct{} 19 | type lambdaParams struct { 20 | FunctionName string 21 | Qualifier string 22 | InvocationType string 23 | Payload string 24 | ClientContext string 25 | } 26 | 27 | func (i *lambdaIntegration) getLambdaInfo(r *request.Request) *lambdaParams { 28 | fields := &lambdaParams{} 29 | m, err := json.Marshal(r.Params) 30 | if err != nil { 31 | return &lambdaParams{} 32 | } 33 | if err = json.Unmarshal(m, fields); err != nil { 34 | return &lambdaParams{} 35 | } 36 | return fields 37 | } 38 | 39 | func (i *lambdaIntegration) getOperationName(r *request.Request) string { 40 | lambdaInfo := i.getLambdaInfo(r) 41 | if len(lambdaInfo.FunctionName) > 0 { 42 | return i.getFunctionName(lambdaInfo.FunctionName) 43 | } 44 | return constants.AWSServiceRequest 45 | } 46 | 47 | func (i *lambdaIntegration) getFunctionName(name string) string { 48 | functionName := name 49 | pos := strings.LastIndex(name, ":function:") 50 | if pos != -1 { 51 | posAfter := pos + len(":function:") 52 | if posAfter >= len(name) { 53 | functionName = "" 54 | } 55 | functionName = name[posAfter:len(name)] 56 | } 57 | 58 | // Strip version number if exists 59 | pos = strings.IndexByte(functionName, ':') 60 | if pos != -1 && pos < len(functionName) { 61 | functionName = functionName[:pos] 62 | } 63 | return functionName 64 | } 65 | 66 | func (i *lambdaIntegration) beforeCall(r *request.Request, span *tracer.RawSpan) { 67 | span.ClassName = constants.ClassNames["LAMBDA"] 68 | span.DomainName = constants.DomainNames["API"] 69 | 70 | operationName := r.Operation.Name 71 | operationType := getOperationType(operationName, constants.ClassNames["LAMBDA"]) 72 | 73 | lambdaInfo := i.getLambdaInfo(r) 74 | 75 | tags := map[string]interface{}{ 76 | constants.AwsLambdaTags["FUNCTION_NAME"]: i.getFunctionName(lambdaInfo.FunctionName), 77 | constants.SpanTags["OPERATION_TYPE"]: operationType, 78 | constants.AwsSDKTags["REQUEST_NAME"]: operationName, 79 | constants.SpanTags["TOPOLOGY_VERTEX"]: true, 80 | constants.SpanTags["TRIGGER_OPERATION_NAMES"]: []string{application.FunctionName}, 81 | constants.SpanTags["TRIGGER_DOMAIN_NAME"]: constants.AwsLambdaApplicationDomain, 82 | constants.SpanTags["TRIGGER_CLASS_NAME"]: constants.AwsLambdaApplicationClass, 83 | } 84 | 85 | if !config.MaskLambdaPayload && lambdaInfo.Payload != "" { 86 | tags[constants.AwsLambdaTags["INVOCATION_PAYLOAD"]] = lambdaInfo.Payload 87 | } 88 | if lambdaInfo.Qualifier != "" { 89 | tags[constants.AwsLambdaTags["FUNCTION_QUALIFIER"]] = lambdaInfo.Qualifier 90 | } 91 | if lambdaInfo.InvocationType != "" { 92 | tags[constants.AwsLambdaTags["INVOCATION_TYPE"]] = lambdaInfo.InvocationType 93 | } 94 | 95 | span.Tags = tags 96 | 97 | if !config.LambdaTraceInjectionDisabled { 98 | i.injectSpanIntoClientContext(r) 99 | } 100 | } 101 | 102 | func (i *lambdaIntegration) afterCall(r *request.Request, span *tracer.RawSpan) { 103 | xAmzRequestID := "" 104 | if r.HTTPResponse != nil && r.HTTPResponse.Header != nil { 105 | xAmzRequestID = r.HTTPResponse.Header.Get("X-Amzn-Requestid") 106 | } 107 | if xAmzRequestID != "" { 108 | span.Tags[constants.SpanTags["TRACE_LINKS"]] = []string{xAmzRequestID} 109 | } 110 | } 111 | 112 | func (i *lambdaIntegration) injectSpanIntoClientContext(r *request.Request) { 113 | input, ok := r.Params.(*lambda.InvokeInput) 114 | 115 | if !ok { 116 | return 117 | } 118 | clientContext := &lambdacontext.ClientContext{} 119 | if input.ClientContext != nil { 120 | data, err := base64.StdEncoding.DecodeString(*input.ClientContext) 121 | if err != nil { 122 | return 123 | } 124 | if err = json.Unmarshal(data, clientContext); err != nil { 125 | return 126 | } 127 | } 128 | if clientContext.Custom == nil { 129 | clientContext.Custom = make(map[string]string, 3) 130 | } 131 | clientContext.Custom[constants.AwsLambdaTriggerOperationName] = application.ApplicationName 132 | clientContext.Custom[constants.AwsLambdaTriggerDomainName] = application.ApplicationDomainName 133 | clientContext.Custom[constants.AwsLambdaTriggerClassName] = application.ApplicationClassName 134 | 135 | clientContextJSON, err := json.Marshal(clientContext) 136 | if err != nil { 137 | return 138 | } 139 | 140 | encodedClientContextJSON := base64.StdEncoding.EncodeToString(clientContextJSON) 141 | input.ClientContext = &encodedClientContextJSON 142 | } 143 | 144 | func init() { 145 | integrations["Lambda"] = &lambdaIntegration{} 146 | } 147 | -------------------------------------------------------------------------------- /wrappers/aws/s3.go: -------------------------------------------------------------------------------- 1 | package thundraaws 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 8 | 9 | "github.com/aws/aws-sdk-go/aws/request" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 11 | ) 12 | 13 | type s3Integration struct{} 14 | type s3Params struct { 15 | Bucket string 16 | Key string 17 | } 18 | 19 | func (i *s3Integration) getS3Info(r *request.Request) *s3Params { 20 | fields := &s3Params{} 21 | m, err := json.Marshal(r.Params) 22 | if err != nil { 23 | return &s3Params{} 24 | } 25 | if err = json.Unmarshal(m, &fields); err != nil { 26 | return &s3Params{} 27 | } 28 | return fields 29 | } 30 | 31 | func (i *s3Integration) getOperationName(r *request.Request) string { 32 | s3Info := i.getS3Info(r) 33 | if len(s3Info.Bucket) > 0 { 34 | return s3Info.Bucket 35 | } 36 | return constants.AWSServiceRequest 37 | } 38 | 39 | func (i *s3Integration) beforeCall(r *request.Request, span *tracer.RawSpan) { 40 | span.ClassName = constants.ClassNames["S3"] 41 | span.DomainName = constants.DomainNames["STORAGE"] 42 | 43 | operationName := r.Operation.Name 44 | operationType := getOperationType(operationName, constants.ClassNames["S3"]) 45 | 46 | s3Info := i.getS3Info(r) 47 | 48 | tags := map[string]interface{}{ 49 | constants.SpanTags["OPERATION_TYPE"]: operationType, 50 | constants.AwsSDKTags["REQUEST_NAME"]: operationName, 51 | constants.SpanTags["TOPOLOGY_VERTEX"]: true, 52 | constants.SpanTags["TRIGGER_OPERATION_NAMES"]: []string{application.FunctionName}, 53 | constants.SpanTags["TRIGGER_DOMAIN_NAME"]: constants.AwsLambdaApplicationDomain, 54 | constants.SpanTags["TRIGGER_CLASS_NAME"]: constants.AwsLambdaApplicationClass, 55 | } 56 | 57 | if s3Info.Bucket != "" { 58 | tags[constants.AwsS3Tags["BUCKET_NAME"]] = s3Info.Bucket 59 | } 60 | if s3Info.Key != "" { 61 | tags[constants.AwsS3Tags["OBJECT_NAME"]] = s3Info.Key 62 | } 63 | 64 | span.Tags = tags 65 | } 66 | 67 | func (i *s3Integration) afterCall(r *request.Request, span *tracer.RawSpan) { 68 | xAmzRequestID := "" 69 | if r.HTTPResponse != nil && r.HTTPResponse.Header != nil { 70 | xAmzRequestID = r.HTTPResponse.Header.Get("x-amz-request-id") 71 | } 72 | if xAmzRequestID != "" { 73 | span.Tags[constants.SpanTags["TRACE_LINKS"]] = []string{xAmzRequestID} 74 | } 75 | } 76 | 77 | func init() { 78 | integrations["S3"] = &s3Integration{} 79 | } 80 | -------------------------------------------------------------------------------- /wrappers/aws/sns.go: -------------------------------------------------------------------------------- 1 | package thundraaws 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 9 | 10 | "github.com/aws/aws-sdk-go/aws/request" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 12 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 13 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 14 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 15 | ) 16 | 17 | type snsIntegration struct{} 18 | 19 | func (i *snsIntegration) getSNSMessage(r *request.Request) string { 20 | inp := &struct { 21 | Message string 22 | }{} 23 | 24 | m, err := json.Marshal(r.Params) 25 | if err != nil { 26 | return "" 27 | } 28 | if err = json.Unmarshal(m, inp); err != nil { 29 | return "" 30 | } 31 | return inp.Message 32 | } 33 | 34 | func (i *snsIntegration) getTopicName(r *request.Request) string { 35 | fields := struct { 36 | Name string 37 | TopicArn string 38 | TargetArn string 39 | }{} 40 | m, err := json.Marshal(r.Params) 41 | if err != nil { 42 | return "" 43 | } 44 | if err = json.Unmarshal(m, &fields); err != nil { 45 | return "" 46 | } 47 | if len(fields.Name) > 0 { 48 | return fields.Name 49 | } else if len(fields.TopicArn) > 0 { 50 | arnParts := strings.Split(fields.TopicArn, ":") 51 | return arnParts[len(arnParts)-1] 52 | } else if len(fields.TargetArn) > 0 { 53 | arnParts := strings.Split(fields.TargetArn, ":") 54 | return arnParts[len(arnParts)-1] 55 | } 56 | return "" 57 | } 58 | 59 | func (i *snsIntegration) getOperationName(r *request.Request) string { 60 | topicName := i.getTopicName(r) 61 | if len(topicName) > 0 { 62 | return topicName 63 | } 64 | return constants.AWSServiceRequest 65 | } 66 | 67 | func (i *snsIntegration) beforeCall(r *request.Request, span *tracer.RawSpan) { 68 | span.ClassName = constants.ClassNames["SNS"] 69 | span.DomainName = constants.DomainNames["MESSAGING"] 70 | 71 | operationName := r.Operation.Name 72 | operationType := getOperationType(operationName, constants.ClassNames["SNS"]) 73 | 74 | tags := map[string]interface{}{ 75 | constants.AwsSNSTags["TOPIC_NAME"]: i.getTopicName(r), 76 | constants.SpanTags["OPERATION_TYPE"]: operationType, 77 | constants.AwsSDKTags["REQUEST_NAME"]: operationName, 78 | constants.SpanTags["TOPOLOGY_VERTEX"]: true, 79 | constants.SpanTags["TRIGGER_OPERATION_NAMES"]: []string{application.FunctionName}, 80 | constants.SpanTags["TRIGGER_DOMAIN_NAME"]: constants.AwsLambdaApplicationDomain, 81 | constants.SpanTags["TRIGGER_CLASS_NAME"]: constants.AwsLambdaApplicationClass, 82 | } 83 | 84 | message := i.getSNSMessage(r) 85 | 86 | if !config.MaskSNSMessage && message != "" { 87 | tags[constants.AwsSNSTags["MESSAGE"]] = message 88 | } 89 | 90 | span.Tags = tags 91 | } 92 | 93 | func (i *snsIntegration) afterCall(r *request.Request, span *tracer.RawSpan) { 94 | responseValue := reflect.ValueOf(r.Data) 95 | if responseValue == (reflect.Value{}) { 96 | return 97 | } 98 | messageID, _ := utils.GetStringFieldFromValue(responseValue.Elem(), "MessageId") 99 | 100 | if messageID != "" { 101 | span.Tags[constants.SpanTags["TRACE_LINKS"]] = []string{messageID} 102 | } 103 | } 104 | 105 | func init() { 106 | integrations["SNS"] = &snsIntegration{} 107 | } 108 | -------------------------------------------------------------------------------- /wrappers/aws/sqs.go: -------------------------------------------------------------------------------- 1 | package thundraaws 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 12 | 13 | "github.com/aws/aws-sdk-go/aws/request" 14 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 15 | ) 16 | 17 | type sqsIntegration struct{} 18 | 19 | func (i *sqsIntegration) getSQSMessage(r *request.Request) string { 20 | inp := &struct { 21 | MessageBody string 22 | }{} 23 | 24 | m, err := json.Marshal(r.Params) 25 | if err != nil { 26 | return "" 27 | } 28 | if err = json.Unmarshal(m, inp); err != nil { 29 | return "" 30 | } 31 | return inp.MessageBody 32 | } 33 | 34 | func (i *sqsIntegration) getQueueName(r *request.Request) string { 35 | fields := struct { 36 | QueueName string 37 | QueueURL string 38 | }{} 39 | m, err := json.Marshal(r.Params) 40 | if err != nil { 41 | return "" 42 | } 43 | if err = json.Unmarshal(m, &fields); err != nil { 44 | return "" 45 | } 46 | if len(fields.QueueName) > 0 { 47 | return fields.QueueName 48 | } else if len(fields.QueueURL) > 0 { 49 | urlParts := strings.Split(fields.QueueURL, "/") 50 | return urlParts[len(urlParts)-1] 51 | } 52 | return "" 53 | } 54 | 55 | func (i *sqsIntegration) getOperationName(r *request.Request) string { 56 | queueName := i.getQueueName(r) 57 | if len(queueName) > 0 { 58 | return queueName 59 | } 60 | return constants.AWSServiceRequest 61 | } 62 | 63 | func (i *sqsIntegration) beforeCall(r *request.Request, span *tracer.RawSpan) { 64 | span.ClassName = constants.ClassNames["SQS"] 65 | span.DomainName = constants.DomainNames["MESSAGING"] 66 | 67 | operationName := r.Operation.Name 68 | operationType := getOperationType(operationName, constants.ClassNames["SQS"]) 69 | 70 | tags := map[string]interface{}{ 71 | constants.AwsSQSTags["QUEUE_NAME"]: i.getQueueName(r), 72 | constants.SpanTags["OPERATION_TYPE"]: operationType, 73 | constants.AwsSDKTags["REQUEST_NAME"]: operationName, 74 | constants.SpanTags["TOPOLOGY_VERTEX"]: true, 75 | constants.SpanTags["TRIGGER_OPERATION_NAMES"]: []string{application.FunctionName}, 76 | constants.SpanTags["TRIGGER_DOMAIN_NAME"]: constants.AwsLambdaApplicationDomain, 77 | constants.SpanTags["TRIGGER_CLASS_NAME"]: constants.AwsLambdaApplicationClass, 78 | } 79 | 80 | message := i.getSQSMessage(r) 81 | 82 | if !config.MaskSQSMessage && message != "" { 83 | tags[constants.AwsSQSTags["MESSAGE"]] = message 84 | } 85 | 86 | span.Tags = tags 87 | } 88 | 89 | func (i *sqsIntegration) afterCall(r *request.Request, span *tracer.RawSpan) { 90 | links := i.getTraceLinks(r) 91 | if links != nil { 92 | span.Tags[constants.SpanTags["TRACE_LINKS"]] = links 93 | } 94 | } 95 | 96 | func (i *sqsIntegration) getTraceLinks(r *request.Request) []string { 97 | responseValue := reflect.ValueOf(r.Data) 98 | if responseValue == (reflect.Value{}) { 99 | return nil 100 | } 101 | responseValueElem := responseValue.Elem() 102 | operationName := r.Operation.Name 103 | if operationName == "SendMessage" { 104 | if messageID, ok := utils.GetStringFieldFromValue(responseValueElem, "MessageId"); ok { 105 | return []string{messageID} 106 | } 107 | 108 | } else if operationName == "SendMessageBatch" { 109 | successful := responseValueElem.FieldByName("Successful") 110 | if successful != (reflect.Value{}) && successful.Len() > 0 { 111 | var links []string 112 | for i := 0; i < successful.Len(); i++ { 113 | if messageID, ok := utils.GetStringFieldFromValue(successful.Index(i).Elem(), "MessageId"); ok { 114 | links = append(links, messageID) 115 | } 116 | } 117 | return links 118 | } 119 | } 120 | return nil 121 | } 122 | 123 | func init() { 124 | integrations["SQS"] = &sqsIntegration{} 125 | } 126 | -------------------------------------------------------------------------------- /wrappers/elastic/integration.go: -------------------------------------------------------------------------------- 1 | package elastic 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 8 | 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 12 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 13 | ) 14 | 15 | func BeforeCall(span *tracer.RawSpan, req *http.Request) { 16 | span.ClassName = constants.ClassNames["ELASTICSEARCH"] 17 | span.DomainName = constants.DomainNames["DB"] 18 | 19 | host := req.URL.Host 20 | method := req.Method 21 | esBody := "" 22 | 23 | // Set span tags 24 | tags := map[string]interface{}{ 25 | constants.SpanTags["OPERATION_TYPE"]: method, 26 | constants.EsTags["ES_HOSTS"]: []string{host}, 27 | constants.EsTags["ES_URI"]: req.URL.Path, 28 | constants.EsTags["ES_NORMALIZED_URI"]: GetNormalizedPath(req.URL.Path), 29 | constants.EsTags["ES_METHOD"]: method, 30 | constants.EsTags["ES_PARAMS"]: req.URL.Query().Encode(), 31 | constants.DBTags["DB_TYPE"]: "elasticsearch", 32 | constants.SpanTags["TRIGGER_DOMAIN_NAME"]: constants.AwsLambdaApplicationDomain, 33 | constants.SpanTags["TRIGGER_CLASS_NAME"]: constants.AwsLambdaApplicationClass, 34 | constants.SpanTags["TRIGGER_OPERATION_NAMES"]: []string{application.FunctionName}, 35 | constants.SpanTags["TOPOLOGY_VERTEX"]: true, 36 | } 37 | 38 | if req != nil && req.Body != nil && !config.MaskEsBody { 39 | esBody, req.Body = utils.ReadRequestBody(req.Body, int(req.ContentLength)) 40 | tags[constants.EsTags["ES_BODY"]] = esBody 41 | } 42 | 43 | span.Tags = tags 44 | } 45 | 46 | func AfterCall(span *tracer.RawSpan, resp *http.Response) { 47 | 48 | } 49 | 50 | func GetNormalizedPath(path string) string { 51 | depth := config.EsIntegrationUrlPathDepth 52 | if depth <= 0 { 53 | return "" 54 | } 55 | 56 | pathSlice := strings.Split(path, "/") 57 | 58 | //filter empty string 59 | n := 0 60 | for _, x := range pathSlice { 61 | if len(x) > 0 { 62 | pathSlice[n] = x 63 | n++ 64 | } 65 | } 66 | pathSlice = pathSlice[:n] 67 | 68 | // check out of bounds 69 | pathLength := len(pathSlice) 70 | if depth > pathLength { 71 | depth = pathLength 72 | } 73 | 74 | //slice till depth 75 | pathSlice = pathSlice[:depth] 76 | return "/" + strings.Join(pathSlice, "/") 77 | } 78 | -------------------------------------------------------------------------------- /wrappers/elastic/olivere/elastic.go: -------------------------------------------------------------------------------- 1 | package thundraelastic 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/opentracing/opentracing-go" 7 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 8 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/wrappers/elastic" 10 | ) 11 | 12 | type roundTripperWrapper struct { 13 | http.RoundTripper 14 | } 15 | 16 | // Wrap wraps the Transport of given http.Client to trace http requests 17 | func Wrap(c *http.Client) *http.Client { 18 | if c.Transport == nil { 19 | c.Transport = http.DefaultTransport 20 | } 21 | c.Transport = &roundTripperWrapper{c.Transport} 22 | return c 23 | } 24 | 25 | func (t *roundTripperWrapper) RoundTrip(req *http.Request) (resp *http.Response, err error) { 26 | normalizedPath := elastic.GetNormalizedPath(req.URL.Path) 27 | span, _ := opentracing.StartSpanFromContext( 28 | req.Context(), 29 | normalizedPath, 30 | ) 31 | defer span.Finish() 32 | rawSpan, ok := tracer.GetRaw(span) 33 | if ok { 34 | elastic.BeforeCall(rawSpan, req) 35 | } 36 | tracer.OnSpanStarted(span) 37 | resp, err = t.RoundTripper.RoundTrip(req) 38 | if err != nil { 39 | utils.SetSpanError(span, err) 40 | } else if ok { 41 | elastic.AfterCall(rawSpan, resp) 42 | } 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /wrappers/mongodb/mongodb.go: -------------------------------------------------------------------------------- 1 | package thundramongo 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "sync" 7 | 8 | opentracing "github.com/opentracing/opentracing-go" 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 12 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 13 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 14 | "go.mongodb.org/mongo-driver/event" 15 | ) 16 | 17 | type commandMonitor struct { 18 | sync.Mutex 19 | spans map[spanKey]opentracing.Span 20 | } 21 | 22 | type spanKey struct { 23 | ConnectionID string 24 | RequestID int64 25 | } 26 | 27 | // NewCommandMonitor returns a new event.CommandMonitor for tracing commands 28 | func NewCommandMonitor() *event.CommandMonitor { 29 | cm := commandMonitor{spans: make(map[spanKey]opentracing.Span)} 30 | return &event.CommandMonitor{Started: cm.started, Succeeded: cm.succeeded, Failed: cm.failed} 31 | } 32 | 33 | func (c *commandMonitor) started(ctx context.Context, event *event.CommandStartedEvent) { 34 | span, _ := opentracing.StartSpanFromContext(ctx, event.DatabaseName) 35 | rawSpan, ok := tracer.GetRaw(span) 36 | if !ok { 37 | return 38 | } 39 | 40 | // Store span to use it on command finished 41 | c.Lock() 42 | c.spans[spanKey{event.ConnectionID, event.RequestID}] = span 43 | c.Unlock() 44 | 45 | beforeCall(rawSpan, event) 46 | tracer.OnSpanStarted(span) 47 | } 48 | 49 | func (c *commandMonitor) succeeded(ctx context.Context, event *event.CommandSucceededEvent) { 50 | c.finished(ctx, &event.CommandFinishedEvent, "") 51 | } 52 | 53 | func (c *commandMonitor) failed(ctx context.Context, event *event.CommandFailedEvent) { 54 | c.finished(ctx, &event.CommandFinishedEvent, event.Failure) 55 | } 56 | 57 | func (c *commandMonitor) finished(ctx context.Context, event *event.CommandFinishedEvent, failure string) { 58 | key := spanKey{event.ConnectionID, event.RequestID} 59 | 60 | c.Lock() 61 | // Retrieve span set in command started 62 | span, ok := c.spans[key] 63 | if ok { 64 | delete(c.spans, key) 65 | } 66 | c.Unlock() 67 | if !ok { 68 | return 69 | } 70 | 71 | if failure != "" { 72 | utils.SetSpanError(span, failure) 73 | } 74 | span.Finish() 75 | } 76 | 77 | func beforeCall(span *tracer.RawSpan, event *event.CommandStartedEvent) { 78 | span.ClassName = constants.ClassNames["MONGODB"] 79 | span.DomainName = constants.DomainNames["DB"] 80 | 81 | host, port := "", "27017" 82 | if len(event.ConnectionID) > 0 { 83 | host = strings.Split(event.ConnectionID, "[")[0] 84 | 85 | if len(strings.Split(host, ":")) > 1 { 86 | port = strings.Split(host, ":")[1] 87 | host = strings.Split(host, ":")[0] 88 | } 89 | } 90 | 91 | collectionValue := event.Command.Lookup(event.CommandName) 92 | collectionName, _ := collectionValue.StringValueOK() 93 | 94 | // Set span tags 95 | tags := map[string]interface{}{ 96 | constants.SpanTags["OPERATION_TYPE"]: constants.MongoDBCommandTypes[strings.ToUpper(event.CommandName)], 97 | constants.DBTags["DB_TYPE"]: "mongodb", 98 | constants.DBTags["DB_HOST"]: host, 99 | constants.DBTags["DB_PORT"]: port, 100 | constants.DBTags["DB_INSTANCE"]: event.DatabaseName, 101 | constants.MongoDBTags["MONGODB_COMMAND_NAME"]: strings.ToUpper(event.CommandName), 102 | constants.MongoDBTags["MONGODB_COLLECTION"]: collectionName, 103 | constants.SpanTags["TRIGGER_DOMAIN_NAME"]: constants.AwsLambdaApplicationDomain, 104 | constants.SpanTags["TRIGGER_CLASS_NAME"]: constants.AwsLambdaApplicationClass, 105 | constants.SpanTags["TRIGGER_OPERATION_NAMES"]: []string{application.FunctionName}, 106 | constants.SpanTags["TOPOLOGY_VERTEX"]: true, 107 | } 108 | 109 | if !config.MaskMongoDBCommand { 110 | if event.Command != nil { 111 | command := event.Command.String() 112 | size := len(command) 113 | if size > constants.DefaultMongoDBSizeLimit { 114 | size = constants.DefaultMongoDBSizeLimit 115 | } 116 | 117 | tags[constants.MongoDBTags["MONGODB_COMMAND"]] = event.Command.String()[:size] 118 | } else { 119 | tags[constants.MongoDBTags["MONGODB_COMMAND"]] = "" 120 | } 121 | } 122 | 123 | span.Tags = tags 124 | } 125 | -------------------------------------------------------------------------------- /wrappers/rdb/mysql.go: -------------------------------------------------------------------------------- 1 | package thundrardb 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | 7 | "github.com/go-sql-driver/mysql" 8 | 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 12 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 13 | ) 14 | 15 | type mysqlIntegration struct{} 16 | 17 | func (i *mysqlIntegration) getOperationName(dsn string) string { 18 | dbName := "" 19 | 20 | cfg, err := mysql.ParseDSN(dsn) 21 | if err == nil { 22 | dbName = cfg.DBName 23 | } 24 | return dbName 25 | } 26 | 27 | func (i *mysqlIntegration) getDbQueryOperation(query string) string { 28 | querySplit := strings.Split(query, " ") 29 | operation := "" 30 | if len(querySplit) > 0 { 31 | operation = querySplit[0] 32 | } 33 | 34 | return operation 35 | } 36 | 37 | func (i *mysqlIntegration) beforeCall(query string, span *tracer.RawSpan, dsn string) { 38 | span.ClassName = constants.ClassNames["MYSQL"] 39 | span.DomainName = constants.DomainNames["DB"] 40 | 41 | operation := i.getDbQueryOperation(query) 42 | 43 | dbName := "" 44 | host := "" 45 | port := "" 46 | cfg, err := mysql.ParseDSN(dsn) 47 | 48 | if err == nil { 49 | dbName = cfg.DBName 50 | host, port, err = net.SplitHostPort(cfg.Addr) 51 | } 52 | 53 | // Set span tags 54 | tags := map[string]interface{}{ 55 | constants.SpanTags["OPERATION_TYPE"]: operationToType[strings.ToLower(operation)], 56 | constants.SpanTags["TRIGGER_OPERATION_NAMES"]: []string{application.FunctionName}, 57 | constants.SpanTags["TRIGGER_DOMAIN_NAME"]: constants.AwsLambdaApplicationDomain, 58 | constants.SpanTags["TRIGGER_CLASS_NAME"]: constants.AwsLambdaApplicationClass, 59 | constants.SpanTags["TOPOLOGY_VERTEX"]: true, 60 | constants.DBTags["DB_STATEMENT_TYPE"]: strings.ToUpper(operation), 61 | constants.DBTags["DB_TYPE"]: "mysql", 62 | constants.DBTags["DB_STATEMENT_TYPE"]: strings.ToUpper(operation), 63 | constants.DBTags["DB_INSTANCE"]: dbName, 64 | constants.DBTags["DB_HOST"]: host, 65 | constants.DBTags["DB_PORT"]: port, 66 | } 67 | 68 | if !config.MaskRDBStatement { 69 | tags[constants.DBTags["DB_STATEMENT"]] = query 70 | } 71 | 72 | span.Tags = tags 73 | } 74 | 75 | func (i *mysqlIntegration) afterCall(query string, span *tracer.RawSpan, dsn string) { 76 | return 77 | } 78 | -------------------------------------------------------------------------------- /wrappers/rdb/mysql_test.go: -------------------------------------------------------------------------------- 1 | package thundrardb 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/go-sql-driver/mysql" 10 | ) 11 | 12 | func setUpMysql(t *testing.T, dsn string) error { 13 | db, _ := sql.Open("mysql", dsn) 14 | err := db.Ping() 15 | if err != nil { 16 | return err 17 | } 18 | defer db.Close() 19 | 20 | _, err = db.Exec("CREATE table IF NOT EXISTS test(id int, type text)") 21 | assert.NoError(t, err) 22 | return nil 23 | } 24 | 25 | func TestMysqlIntegration(t *testing.T) { 26 | dsn := "user:userpass@tcp(localhost:3306)/db" 27 | err := setUpMysql(t, dsn) 28 | if err != nil { 29 | t.Skip() 30 | } 31 | s := newSuite(t, &mysql.MySQLDriver{}, dsn, "mysql") 32 | s.TestRdbIntegration(t, "SELECT * FROM test WHERE id = ?", "MYSQL", 1) 33 | } 34 | -------------------------------------------------------------------------------- /wrappers/rdb/postgresql_test.go: -------------------------------------------------------------------------------- 1 | package thundrardb 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com/lib/pq" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func setUpPostgresql(t *testing.T, dsn string) error { 12 | db, _ := sql.Open("postgres", dsn) 13 | err := db.Ping() 14 | if err != nil { 15 | return err 16 | } 17 | defer db.Close() 18 | 19 | _, err = db.Exec("CREATE table IF NOT EXISTS test(id int, type text)") 20 | assert.NoError(t, err) 21 | return nil 22 | } 23 | 24 | func TestPostgresqlIntegration(t *testing.T) { 25 | 26 | dsn := "postgres://user:userpass@localhost/db?sslmode=disable" 27 | err := setUpPostgresql(t, dsn) 28 | if err != nil { 29 | t.Skip() 30 | } 31 | s := newSuite(t, &pq.Driver{}, dsn, "postgresql") 32 | s.TestRdbIntegration(t, "SELECT * FROM test WHERE id = $1", "POSTGRESQL", 1) 33 | } 34 | -------------------------------------------------------------------------------- /wrappers/redis/integration.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/thundra-io/thundra-lambda-agent-go/v2/application" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/config" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/constants" 12 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 13 | ) 14 | 15 | func BeforeCall(span *tracer.RawSpan, host string, port string, commandName string, command string) { 16 | span.ClassName = constants.ClassNames["REDIS"] 17 | span.DomainName = constants.DomainNames["CACHE"] 18 | if len(commandName) == 0 { 19 | commandName = strings.Split(command, " ")[0] 20 | } 21 | commandName = strings.ToUpper(commandName) 22 | // Set span tags 23 | tags := map[string]interface{}{ 24 | constants.SpanTags["OPERATION_TYPE"]: constants.RedisCommandTypes[commandName], 25 | constants.DBTags["DB_INSTANCE"]: host, 26 | constants.DBTags["DB_STATEMENT_TYPE"]: commandName, 27 | constants.DBTags["DB_TYPE"]: "redis", 28 | constants.RedisTags["REDIS_HOST"]: host, 29 | constants.RedisTags["REDIS_COMMAND_TYPE"]: commandName, 30 | } 31 | 32 | tags[constants.SpanTags["TRIGGER_DOMAIN_NAME"]] = constants.AwsLambdaApplicationDomain 33 | tags[constants.SpanTags["TRIGGER_CLASS_NAME"]] = constants.AwsLambdaApplicationClass 34 | tags[constants.SpanTags["TRIGGER_OPERATION_NAMES"]] = []string{application.FunctionName} 35 | tags[constants.SpanTags["TOPOLOGY_VERTEX"]] = true 36 | 37 | if !config.MaskRedisCommand { 38 | tags[constants.DBTags["DB_STATEMENT"]] = command 39 | tags[constants.RedisTags["REDIS_COMMAND"]] = command 40 | } 41 | 42 | span.Tags = tags 43 | } 44 | 45 | func AfterCall(span *tracer.RawSpan, command string) { 46 | if !config.MaskRedisCommand { 47 | span.Tags[constants.DBTags["DB_STATEMENT"]] = command 48 | span.Tags[constants.RedisTags["REDIS_COMMAND"]] = command 49 | } 50 | } 51 | 52 | func GetRedisCommand(commandName string, args ...interface{}) string { 53 | var b bytes.Buffer 54 | b.WriteString(commandName) 55 | for _, arg := range args { 56 | b.WriteString(" ") 57 | switch arg := arg.(type) { 58 | case string: 59 | b.WriteString(arg) 60 | case int: 61 | b.WriteString(strconv.Itoa(arg)) 62 | case int32: 63 | b.WriteString(strconv.FormatInt(int64(arg), 10)) 64 | case int64: 65 | b.WriteString(strconv.FormatInt(arg, 10)) 66 | case fmt.Stringer: 67 | b.WriteString(arg.String()) 68 | } 69 | } 70 | return b.String() 71 | } 72 | -------------------------------------------------------------------------------- /wrappers/redis/redigo/redigo.go: -------------------------------------------------------------------------------- 1 | package thundraredigo 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/url" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | opentracing "github.com/opentracing/opentracing-go" 10 | "github.com/thundra-io/thundra-lambda-agent-go/v2/tracer" 11 | "github.com/thundra-io/thundra-lambda-agent-go/v2/utils" 12 | tredis "github.com/thundra-io/thundra-lambda-agent-go/v2/wrappers/redis" 13 | ) 14 | 15 | type connWrapper struct { 16 | redis.Conn 17 | host string 18 | port string 19 | } 20 | 21 | var emptyCtx = context.Background() 22 | 23 | // Dial wraps redis.Dial and returns a wrapped connection 24 | func Dial(network, address string, options ...redis.DialOption) (redis.Conn, error) { 25 | conn, err := redis.Dial(network, address, options...) 26 | if err != nil { 27 | return nil, err 28 | } 29 | host, port, err := net.SplitHostPort(address) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | return connWrapper{conn, host, port}, nil 35 | } 36 | 37 | // DialURL wraps redis.DialURL and returns a wrapped connection 38 | func DialURL(rawurl string, options ...redis.DialOption) (redis.Conn, error) { 39 | parsedURL, err := url.Parse(rawurl) 40 | if err != nil { 41 | return nil, err 42 | } 43 | host, port, err := net.SplitHostPort(parsedURL.Host) 44 | if err != nil { 45 | host = parsedURL.Host 46 | port = "6379" 47 | } 48 | if host == "" { 49 | host = "localhost" 50 | } 51 | conn, err := redis.DialURL(rawurl, options...) 52 | return connWrapper{conn, host, port}, err 53 | } 54 | 55 | // Do wraps the redis.Conn.Do and starts a new span. If context.Context is provided as last argument, 56 | // the newly created span will be a child of span with the passed context. Otherwise, new span will be 57 | // created with an empty context. 58 | func (c connWrapper) Do(commandName string, args ...interface{}) (interface{}, error) { 59 | ctx := emptyCtx 60 | if n := len(args); n > 0 { 61 | var ok bool 62 | if ctx, ok = args[n-1].(context.Context); ok { 63 | args = args[:n-1] 64 | } else { 65 | ctx = emptyCtx 66 | } 67 | } 68 | 69 | span, _ := opentracing.StartSpanFromContext( 70 | ctx, 71 | c.host, 72 | ) 73 | defer span.Finish() 74 | 75 | rawSpan, ok := tracer.GetRaw(span) 76 | if ok { 77 | tredis.BeforeCall(rawSpan, c.host, c.port, commandName, tredis.GetRedisCommand(commandName, args...)) 78 | } 79 | 80 | tracer.OnSpanStarted(span) 81 | reply, err := c.Conn.Do(commandName, args...) 82 | if err != nil { 83 | utils.SetSpanError(span, err) 84 | } 85 | return reply, err 86 | } 87 | --------------------------------------------------------------------------------