├── .github ├── CODEOWNERS └── workflows │ └── main.yml ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── epsagon ├── aws_sdk_v2_factories │ ├── common_utils.go │ ├── dynamodb_factories.go │ ├── s3_factory.go │ └── sts_factory.go ├── aws_v2_wrapper.go ├── aws_v2_wrapper_factories_test.go ├── common_utils.go ├── concurrent_generic_wrapper_test.go ├── custom_trace_data_test.go ├── generic_lambda_handler.go ├── generic_lambda_handler_test.go ├── generic_wrapper.go ├── generic_wrapper_test.go ├── lambda_trigger.go ├── lambda_trigger_test.go ├── lambda_wrapper.go ├── lambda_wrapper_test.go └── tracer_helpers.go ├── example ├── api_gateway │ ├── README.md │ ├── main.go │ ├── makefile │ └── serverless.yml ├── concurrent_generic_go_functions │ ├── main.go │ └── makefile ├── custom_error_example │ ├── main.go │ └── makefile ├── ddb_example │ ├── makefile │ ├── serverless.yml │ ├── trigger │ │ └── main.go │ ├── write │ │ └── main.go │ └── write_sdk_v2 │ │ └── main.go ├── exp_example │ ├── main.go │ └── serverless.yml ├── fiber_middleware │ ├── main.go │ └── makefile ├── generic_go_function │ ├── main.go │ └── makefile ├── gin_wrapper │ ├── main.go │ └── makefile ├── http_mux │ └── main.go ├── ignored_keys │ └── main.go ├── label_example │ ├── main.go │ └── makefile ├── mongo_example │ └── main.go ├── mux_error │ └── main.go ├── redis_wrapper │ └── main.go ├── s3_example │ ├── README.md │ ├── serverless.yml │ ├── trigger │ │ └── main.go │ ├── write │ │ └── main.go │ └── write_v2 │ │ └── main.go ├── simple_error │ ├── README.md │ ├── main.go │ ├── makefile │ └── serverless.yml ├── simple_lambda │ ├── README.md │ ├── main.go │ ├── makefile │ └── serverless.yml ├── sqs_trigger │ ├── hello │ │ └── main.go │ ├── makefile │ ├── serverless.yml │ └── triggered │ │ └── main.go └── sts_example │ └── main.go ├── go.mod ├── go.sum ├── internal └── fake_collector.go ├── protocol ├── error_code.pb.go ├── event.pb.go ├── exception.pb.go ├── timestamp.pb.go └── trace.pb.go ├── trace.png ├── tracer ├── common_utils.go ├── mask_ignored_keys.go ├── mask_ignored_keys_test.go ├── mocked_tracer.go ├── tracer.go ├── tracer_test.go └── version.go └── wrappers ├── aws └── aws-sdk-go │ └── aws │ ├── aws.go │ ├── aws_test.go │ ├── common_utils.go │ ├── dynamodb_factories.go │ ├── kinesis_factory.go │ ├── lambda_factory.go │ ├── s3_factory.go │ ├── ses_factory.go │ ├── sfn_factory.go │ ├── sns_factory.go │ └── sqs_factories.go ├── fiber ├── fiber.go └── fiber_test.go ├── gin ├── gin.go └── gin_test.go ├── mongo ├── common_utils.go ├── mongo.go └── mongo_test.go ├── net └── http │ ├── client.go │ ├── client_test.go │ ├── handler_wrapper.go │ ├── handler_wrapper_test.go │ └── response_writer.go └── redis ├── redis.go └── redis_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | * @epsagon/the-fabulous-team 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the main branch 5 | on: 6 | push: 7 | branches: 8 | - master 9 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 10 | jobs: 11 | # This workflow contains a single job called "build" 12 | build: 13 | # The type of runner that the job will run on 14 | runs-on: ubuntu-latest 15 | if: "!contains(github.event.head_commit.message, 'Increasing version')" 16 | # Steps represent a sequence of tasks that will be executed as part of the job 17 | steps: 18 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 19 | - uses: actions/checkout@v2 20 | with: 21 | token: ${{ secrets.INCREASE_VERSION_TOKEN }} 22 | # Runs a set of commands using the runners shell 23 | - name: Update version 24 | env: 25 | # This is necessary in order to push a commit to the repo 26 | GITHUB_TOKEN: ${{ secrets.INCREASE_VERSION_TOKEN }} 27 | run: | 28 | git remote add github "https://maorlx:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git" 29 | git pull github ${GITHUB_REF} --ff-only 30 | git config --global user.email "maor@epsagon.com" 31 | git config --global user.name "Maor Levi" 32 | version=`cat tracer/version.go | egrep "const VERSION = " | tr -s ' ' | cut -d ' ' -f 4` 33 | minor=`echo $version | cut -d. -f2` 34 | major=`echo $version | cut -d. -f1` 35 | patch=`echo $version | cut -d. -f3` 36 | new_minor=`echo "$((minor+1))"` 37 | new_version="${major}.${new_minor}.${patch}" 38 | echo $new_version 39 | sed -i "s/${version}/${new_version}/g" tracer/version.go 40 | git commit -m "Increasing version to $new_version" tracer/version.go 41 | git push github HEAD:${GITHUB_REF} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # GoLand IDE 2 | .idea 3 | 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | Gopkg.lock 17 | *.out 18 | 19 | # vendor directory are not needed here 20 | vendor 21 | example/simple_lambda/bin/hello 22 | example/simple_lambda/main 23 | example/sqs_trigger/hello/main 24 | example/sqs_trigger/triggered/main 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.13.x" 4 | - "tip" 5 | 6 | env: 7 | - GO111MODULE=on 8 | 9 | before_install: 10 | - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 11 | 12 | install: 13 | - go mod download 14 | - go get golang.org/x/lint/golint 15 | - go install golang.org/x/lint/golint 16 | - go mod tidy 17 | 18 | script: 19 | - go test ./... 20 | - go vet -v ./... 21 | - golint $(go list ./...) 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dev@epsagon.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Epsagon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /epsagon/aws_sdk_v2_factories/common_utils.go: -------------------------------------------------------------------------------- 1 | package epsagonawsv2factories 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/epsagon/epsagon-go/protocol" 8 | "github.com/epsagon/epsagon-go/tracer" 9 | "reflect" 10 | "strconv" 11 | ) 12 | 13 | type specificOperationHandler func( 14 | r *aws.Request, 15 | res *protocol.Resource, 16 | metadataOnly bool, 17 | currentTracer tracer.Tracer, 18 | ) 19 | 20 | func handleSpecificOperation( 21 | r *aws.Request, 22 | res *protocol.Resource, 23 | metadataOnly bool, 24 | handlers map[string]specificOperationHandler, 25 | defaultHandler specificOperationHandler, 26 | currentTracer tracer.Tracer, 27 | ) { 28 | handler := handlers[res.Operation] 29 | if handler == nil { 30 | handler = defaultHandler 31 | } 32 | if handler != nil { 33 | handler(r, res, metadataOnly, currentTracer) 34 | } 35 | } 36 | 37 | func getFieldStringPtr(value reflect.Value, fieldName string) (string, bool) { 38 | field := value.FieldByName(fieldName) 39 | if field == (reflect.Value{}) { 40 | return "", false 41 | } 42 | return field.Elem().String(), true 43 | } 44 | 45 | func updateMetadataField(data reflect.Value, key string, res *protocol.Resource) { 46 | value, ok := getFieldStringPtr(data, key) 47 | if ok { 48 | res.Metadata[key] = value 49 | } 50 | } 51 | 52 | func updateMetadataFromBytes( 53 | value reflect.Value, fieldName string, targetKey string, metadata map[string]string) { 54 | field := value.FieldByName(fieldName) 55 | if field == (reflect.Value{}) { 56 | return 57 | } 58 | metadata[targetKey] = string(field.Bytes()) 59 | } 60 | 61 | func updateMetadataFromValue( 62 | value reflect.Value, fieldName string, targetKey string, metadata map[string]string) { 63 | fieldValue, ok := getFieldStringPtr(value, fieldName) 64 | if ok { 65 | metadata[targetKey] = fieldValue 66 | } 67 | } 68 | 69 | func updateMetadataFromInt64( 70 | value reflect.Value, fieldName string, targetKey string, metadata map[string]string) { 71 | field := value.FieldByName(fieldName) 72 | if field == (reflect.Value{}) { 73 | return 74 | } 75 | metadata[targetKey] = strconv.FormatInt(field.Elem().Int(), 10) 76 | } 77 | 78 | func updateMetadataWithFieldToJSON( 79 | value reflect.Value, 80 | fieldName string, 81 | targetKey string, 82 | metadata map[string]string, 83 | currentTracer tracer.Tracer, 84 | ) { 85 | field := value.FieldByName(fieldName) 86 | if field == (reflect.Value{}) { 87 | return 88 | } 89 | stream, err := json.Marshal(field.Interface()) 90 | if err != nil { 91 | currentTracer.AddExceptionTypeAndMessage("aws-sdk-go", fmt.Sprintf("%v", err)) 92 | return 93 | } 94 | metadata[targetKey] = string(stream) 95 | } 96 | 97 | func getResourceNameFromField(res *protocol.Resource, value reflect.Value, fieldName string) { 98 | fieldValue, ok := getFieldStringPtr(value, fieldName) 99 | if ok { 100 | res.Name = fieldValue 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /epsagon/aws_sdk_v2_factories/dynamodb_factories.go: -------------------------------------------------------------------------------- 1 | package epsagonawsv2factories 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 10 | "github.com/epsagon/epsagon-go/protocol" 11 | "github.com/epsagon/epsagon-go/tracer" 12 | "reflect" 13 | ) 14 | 15 | // DynamodbEventDataFactory to create epsagon Resource from aws.Request to DynamoDB 16 | func DynamodbEventDataFactory( 17 | r *aws.Request, 18 | res *protocol.Resource, 19 | metadataOnly bool, 20 | currentTracer tracer.Tracer, 21 | ) { 22 | inputValue := reflect.ValueOf(r.Params).Elem() 23 | tableName, ok := getFieldStringPtr(inputValue, "TableName") 24 | if ok { 25 | res.Name = tableName 26 | } 27 | handleSpecificOperations := map[string]specificOperationHandler{ 28 | "PutItem": handleDynamoDBPutItem, 29 | "GetItem": handleDynamoDBGetItem, 30 | "DeleteItem": handleDynamoDBDeleteItem, 31 | "UpdateItem": handleDynamoDBUpdateItem, 32 | "Scan": handleDynamoDBScan, 33 | "BatchWriteItem": handleDynamoDBBatchWriteItem, 34 | } 35 | handleSpecificOperation(r, res, metadataOnly, handleSpecificOperations, nil, currentTracer) 36 | } 37 | 38 | func deserializeAttributeMap(inputField reflect.Value) map[string]string { 39 | formattedItem := make(map[string]string) 40 | input := inputField.Interface().(map[string]dynamodb.AttributeValue) 41 | for k, v := range input { 42 | formattedItem[k] = v.String() 43 | } 44 | return formattedItem 45 | } 46 | 47 | func jsonAttributeMap(inputField reflect.Value, currentTracer tracer.Tracer) string { 48 | if inputField == (reflect.Value{}) { 49 | return "" 50 | } 51 | formattedMap := deserializeAttributeMap(inputField) 52 | stream, err := json.Marshal(formattedMap) 53 | if err != nil { 54 | currentTracer.AddExceptionTypeAndMessage("aws-sdk-go", fmt.Sprintf("%v", err)) 55 | return "" 56 | } 57 | return string(stream) 58 | } 59 | 60 | func handleDynamoDBPutItem( 61 | r *aws.Request, 62 | res *protocol.Resource, 63 | metadataOnly bool, 64 | _ tracer.Tracer, 65 | ) { 66 | inputValue := reflect.ValueOf(r.Params).Elem() 67 | itemField := inputValue.FieldByName("Item") 68 | if itemField == (reflect.Value{}) { 69 | return 70 | } 71 | formattedItem := deserializeAttributeMap(itemField) 72 | formattedItemStream, err := json.Marshal(formattedItem) 73 | if err != nil { 74 | // TODO send tracer exception? 75 | return 76 | } 77 | if !metadataOnly { 78 | res.Metadata["Item"] = string(formattedItemStream) 79 | } 80 | h := md5.New() 81 | h.Write(formattedItemStream) 82 | res.Metadata["item_hash"] = hex.EncodeToString(h.Sum(nil)) 83 | } 84 | 85 | func handleDynamoDBGetItem( 86 | r *aws.Request, 87 | res *protocol.Resource, 88 | metadataOnly bool, 89 | currentTracer tracer.Tracer, 90 | ) { 91 | inputValue := reflect.ValueOf(r.Params).Elem() 92 | jsonKeyField := jsonAttributeMap(inputValue.FieldByName("Key"), currentTracer) 93 | res.Metadata["Key"] = jsonKeyField 94 | 95 | if !metadataOnly { 96 | outputValue := reflect.ValueOf(r.Data).Elem() 97 | jsonItemField := jsonAttributeMap(outputValue.FieldByName("Item"), currentTracer) 98 | res.Metadata["Item"] = jsonItemField 99 | } 100 | } 101 | 102 | func handleDynamoDBDeleteItem( 103 | r *aws.Request, 104 | res *protocol.Resource, 105 | metadataOnly bool, 106 | currentTracer tracer.Tracer, 107 | ) { 108 | inputValue := reflect.ValueOf(r.Params).Elem() 109 | jsonKeyField := jsonAttributeMap(inputValue.FieldByName("Key"), currentTracer) 110 | res.Metadata["Key"] = jsonKeyField 111 | } 112 | 113 | func handleDynamoDBUpdateItem( 114 | r *aws.Request, 115 | res *protocol.Resource, 116 | metadataOnly bool, 117 | currentTracer tracer.Tracer, 118 | ) { 119 | inputValue := reflect.ValueOf(r.Params).Elem() 120 | eavField := inputValue.FieldByName("ExpressionAttributeValues") 121 | eav := deserializeAttributeMap(eavField) 122 | eavStream, err := json.Marshal(eav) 123 | if err != nil { 124 | return 125 | } 126 | updateParameters := map[string]string{ 127 | "Expression Attribute Values": string(eavStream), 128 | } 129 | jsonKeyField := jsonAttributeMap(inputValue.FieldByName("Key"), currentTracer) 130 | updateParameters["Key"] = jsonKeyField 131 | updateMetadataFromValue(inputValue, 132 | "UpdateExpression", "UpdateExpression", updateParameters) 133 | updateParamsStream, err := json.Marshal(updateParameters) 134 | if err != nil { 135 | return 136 | } 137 | res.Metadata["Update Parameters"] = string(updateParamsStream) 138 | } 139 | 140 | func deserializeItems(itemsField reflect.Value, currentTracer tracer.Tracer) string { 141 | if itemsField == (reflect.Value{}) { 142 | return "" 143 | } 144 | formattedItems := make([]map[string]string, itemsField.Len()) 145 | for ind := 0; ind < itemsField.Len(); ind++ { 146 | formattedItems = append(formattedItems, 147 | deserializeAttributeMap(itemsField.Index(ind))) 148 | } 149 | formattedItemsStream, err := json.Marshal(formattedItems) 150 | if err != nil { 151 | currentTracer.AddExceptionTypeAndMessage("aws-sdk-go", 152 | fmt.Sprintf("sederializeItems: %v", err)) 153 | } 154 | return string(formattedItemsStream) 155 | } 156 | 157 | func handleDynamoDBScan( 158 | r *aws.Request, 159 | res *protocol.Resource, 160 | metadataOnly bool, 161 | currentTracer tracer.Tracer, 162 | ) { 163 | outputValue := reflect.ValueOf(r.Params).Elem() 164 | updateMetadataFromInt64(outputValue, "Count", "Items Count", res.Metadata) 165 | updateMetadataFromInt64(outputValue, "ScannedCount", "Scanned Items Count", res.Metadata) 166 | itemsField := outputValue.FieldByName("Items") 167 | if !metadataOnly { 168 | res.Metadata["Items"] = deserializeItems(itemsField, currentTracer) 169 | } 170 | } 171 | 172 | func handleDynamoDBBatchWriteItem( 173 | r *aws.Request, 174 | res *protocol.Resource, 175 | metadataOnly bool, 176 | currentTracer tracer.Tracer, 177 | ) { 178 | inputValue := reflect.ValueOf(r.Params).Elem() 179 | requestItemsField := inputValue.FieldByName("RequestItems") 180 | if requestItemsField != (reflect.Value{}) { 181 | var tableName string 182 | requestItems, ok := requestItemsField.Interface().(map[string][]*dynamodb.WriteRequest) 183 | if !ok { 184 | currentTracer.AddExceptionTypeAndMessage("aws-sdk-go", 185 | "handleDynamoDBBatchWriteItem: Failed to cast RequestItems") 186 | return 187 | } 188 | for k := range requestItems { 189 | tableName = k 190 | break 191 | } 192 | res.Name = tableName 193 | // TODO not ignore other tables 194 | if !metadataOnly { 195 | items := make([]map[string]dynamodb.AttributeValue, len(requestItems)) 196 | for _, writeRequest := range requestItems[tableName] { 197 | items = append(items, writeRequest.PutRequest.Item) 198 | } 199 | itemsValue := reflect.ValueOf(items) 200 | res.Metadata["Items"] = deserializeItems(itemsValue, currentTracer) 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /epsagon/aws_sdk_v2_factories/s3_factory.go: -------------------------------------------------------------------------------- 1 | package epsagonawsv2factories 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aws/aws-sdk-go-v2/aws" 6 | "github.com/epsagon/epsagon-go/protocol" 7 | "github.com/epsagon/epsagon-go/tracer" 8 | "reflect" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // S3EventDataFactory creats an Epsagon Resource from aws.Request to S3 14 | func S3EventDataFactory( 15 | r *aws.Request, 16 | res *protocol.Resource, 17 | metadataOnly bool, 18 | currentTracer tracer.Tracer, 19 | ) { 20 | inputValue := reflect.ValueOf(r.Params).Elem() 21 | getResourceNameFromField(res, inputValue, "Bucket") 22 | handleSpecificOperations := map[string]specificOperationHandler{ 23 | "HeadObject": handleS3GetOrHeadObject, 24 | "GetObject": handleS3GetOrHeadObject, 25 | "PutObject": handleS3PutObject, 26 | "ListObjects": handleS3ListObject, 27 | } 28 | handleSpecificOperation(r, res, metadataOnly, handleSpecificOperations, nil, currentTracer) 29 | } 30 | 31 | func commonS3OpertionHandler(r *aws.Request, res *protocol.Resource, metadataOnly bool) { 32 | inputValue := reflect.ValueOf(r.Params).Elem() 33 | updateMetadataFromValue(inputValue, "Key", "key", res.Metadata) 34 | outputValue := reflect.ValueOf(r.Data).Elem() 35 | etag, ok := getFieldStringPtr(outputValue, "ETag") 36 | if ok { 37 | etag = strings.Trim(etag, "\"") 38 | res.Metadata["etag"] = etag 39 | } 40 | } 41 | 42 | func handleS3GetOrHeadObject( 43 | r *aws.Request, 44 | res *protocol.Resource, 45 | metadataOnly bool, 46 | _ tracer.Tracer, 47 | ) { 48 | commonS3OpertionHandler(r, res, metadataOnly) 49 | outputValue := reflect.ValueOf(r.Data).Elem() 50 | updateMetadataFromValue(outputValue, "ContentLength", "file_size", res.Metadata) 51 | 52 | lastModifiedField := outputValue.FieldByName("LastModified") 53 | if lastModifiedField == (reflect.Value{}) { 54 | return 55 | } 56 | lastModified := lastModifiedField.Elem().Interface().(time.Time) 57 | res.Metadata["last_modified"] = lastModified.String() 58 | } 59 | 60 | func handleS3PutObject( 61 | r *aws.Request, 62 | res *protocol.Resource, 63 | metadataOnly bool, 64 | _ tracer.Tracer, 65 | ) { 66 | commonS3OpertionHandler(r, res, metadataOnly) 67 | } 68 | 69 | type s3File struct { 70 | key string 71 | size int64 72 | etag string 73 | } 74 | 75 | func handleS3ListObject( 76 | r *aws.Request, 77 | res *protocol.Resource, 78 | metadataOnly bool, 79 | _ tracer.Tracer, 80 | ) { 81 | if metadataOnly { 82 | return 83 | } 84 | 85 | outputValue := reflect.ValueOf(r.Data).Elem() 86 | contentsField := outputValue.FieldByName("Contents") 87 | if contentsField == (reflect.Value{}) { 88 | return 89 | } 90 | length := contentsField.Len() 91 | files := make([]s3File, length) 92 | for i := 0; i < length; i++ { 93 | var key, etag string 94 | var size int64 95 | fileObject := contentsField.Index(i).Elem() 96 | etag = fileObject.FieldByName("ETag").Elem().String() 97 | key = fileObject.FieldByName("Key").Elem().String() 98 | size = fileObject.FieldByName("Size").Elem().Int() 99 | 100 | files = append(files, s3File{key, size, etag}) 101 | } 102 | res.Metadata["files"] = fmt.Sprintf("%+v", files) 103 | } 104 | -------------------------------------------------------------------------------- /epsagon/aws_sdk_v2_factories/sts_factory.go: -------------------------------------------------------------------------------- 1 | package epsagonawsv2factories 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/aws" 5 | "github.com/epsagon/epsagon-go/protocol" 6 | "github.com/epsagon/epsagon-go/tracer" 7 | "reflect" 8 | ) 9 | 10 | // STSEventDataFactory to create epsagon Resource from aws.Request to STS 11 | func StsDataFactory( 12 | r *aws.Request, 13 | res *protocol.Resource, 14 | metadataOnly bool, 15 | currentTracer tracer.Tracer, 16 | ) { 17 | handleSpecificOperations := map[string]specificOperationHandler{ 18 | "GetCallerIdentity": handleStsGetCallerIdentityRequest, 19 | } 20 | handleSpecificOperation(r, res, metadataOnly, handleSpecificOperations, nil, currentTracer) 21 | } 22 | 23 | func handleStsGetCallerIdentityRequest( 24 | r *aws.Request, 25 | res *protocol.Resource, 26 | metadataOnly bool, 27 | _ tracer.Tracer, 28 | ) { 29 | if !metadataOnly { 30 | outputValue := reflect.ValueOf(r.Data).Elem() 31 | for _, key := range []string{"Account", "Arn", "UserId"} { 32 | updateMetadataField(outputValue, key, res) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /epsagon/aws_v2_wrapper.go: -------------------------------------------------------------------------------- 1 | package epsagon 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/epsagon/epsagon-go/epsagon/aws_sdk_v2_factories" 9 | "github.com/epsagon/epsagon-go/protocol" 10 | "github.com/epsagon/epsagon-go/tracer" 11 | "log" 12 | "reflect" 13 | "time" 14 | ) 15 | 16 | // WrapAwsV2Service wrap aws service with epsgon 17 | // svc := epsagon.WrapAwsV2Service(dynamodb.New(cfg)).(*dynamodb.Client) 18 | func WrapAwsV2Service(svcClient interface{}, args ...context.Context) interface{} { 19 | awsClient := reflect.ValueOf(svcClient).Elem().FieldByName("Client").Interface().(*aws.Client) 20 | awsClient.Handlers.Complete.PushFrontNamed( 21 | aws.NamedHandler{ 22 | Name: "epsagon-aws-sdk-v2", 23 | Fn: func(r *aws.Request) { 24 | currentTracer := ExtractTracer(args) 25 | completeEventData(r, currentTracer) 26 | }, 27 | }, 28 | ) 29 | return svcClient 30 | } 31 | 32 | func getTimeStampFromRequest(r *aws.Request) float64 { 33 | return float64(r.Time.UTC().UnixNano()) / float64(time.Millisecond) / float64(time.Nanosecond) / 1000.0 34 | } 35 | 36 | func completeEventData(r *aws.Request, currentTracer tracer.Tracer) { 37 | defer GeneralEpsagonRecover("aws-sdk-go wrapper", "", currentTracer) 38 | if currentTracer.GetConfig().Debug { 39 | log.Printf("EPSAGON DEBUG OnComplete current tracer: %+v\n", currentTracer) 40 | log.Printf("EPSAGON DEBUG OnComplete request response: %+v\n", r.HTTPResponse) 41 | log.Printf("EPSAGON DEBUG OnComplete request Operation: %+v\n", r.Operation) 42 | log.Printf("EPSAGON DEBUG OnComplete request Endpoint: %+v\n", r.Endpoint) 43 | log.Printf("EPSAGON DEBUG OnComplete request Params: %+v\n", r.Params) 44 | log.Printf("EPSAGON DEBUG OnComplete request Data: %+v\n", r.Data) 45 | } 46 | 47 | endTime := tracer.GetTimestamp() 48 | event := protocol.Event{ 49 | Id: r.RequestID, 50 | StartTime: getTimeStampFromRequest(r), 51 | Origin: "aws-sdk", 52 | Resource: extractResourceInformation(r, currentTracer), 53 | } 54 | event.Duration = endTime - event.StartTime 55 | currentTracer.AddEvent(&event) 56 | } 57 | 58 | type factory func(*aws.Request, *protocol.Resource, bool, tracer.Tracer) 59 | 60 | var awsResourceEventFactories = map[string]factory{ 61 | "s3": epsagonawsv2factories.S3EventDataFactory, 62 | "dynamodb": epsagonawsv2factories.DynamodbEventDataFactory, 63 | "sts": epsagonawsv2factories.StsDataFactory, 64 | } 65 | 66 | func extractResourceInformation(r *aws.Request, currentTracer tracer.Tracer) *protocol.Resource { 67 | res := protocol.Resource{ 68 | Type: r.Endpoint.SigningName, 69 | Operation: r.Operation.Name, 70 | Metadata: make(map[string]string), 71 | } 72 | factory := awsResourceEventFactories[res.Type] 73 | if factory != nil { 74 | factory(r, &res, currentTracer.GetConfig().MetadataOnly, currentTracer) 75 | } else { 76 | defaultFactory(r, &res, currentTracer.GetConfig().MetadataOnly, currentTracer) 77 | } 78 | return &res 79 | } 80 | 81 | func defaultFactory(r *aws.Request, res *protocol.Resource, metadataOnly bool, currentTracer tracer.Tracer) { 82 | if currentTracer.GetConfig().Debug { 83 | log.Println("EPSAGON DEBUG:: entering defaultFactory") 84 | } 85 | if !metadataOnly { 86 | extractInterfaceToMetadata(r.Data, res) 87 | extractInterfaceToMetadata(r.Params, res) 88 | } 89 | } 90 | 91 | func extractInterfaceToMetadata(input interface{}, res *protocol.Resource) { 92 | var data map[string]interface{} 93 | rawJSON, err := json.Marshal(input) 94 | if err != nil { 95 | log.Printf("EPSAGON DEBUG: Failed to marshal input: %+v\n", input) 96 | return 97 | } 98 | err = json.Unmarshal(rawJSON, &data) 99 | if err != nil { 100 | log.Printf("EPSAGON DEBUG: Failed to unmarshal input: %+v\n", rawJSON) 101 | return 102 | } 103 | for key, value := range data { 104 | res.Metadata[key] = fmt.Sprintf("%v", value) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /epsagon/aws_v2_wrapper_factories_test.go: -------------------------------------------------------------------------------- 1 | package epsagon 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go-v2/aws" 5 | epsagonawsv2factories "github.com/epsagon/epsagon-go/epsagon/aws_sdk_v2_factories" 6 | "github.com/epsagon/epsagon-go/protocol" 7 | "github.com/epsagon/epsagon-go/tracer" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | const TEST_ACCOUNT = "test_account" 13 | const TEST_USER_ID = "test_user_id" 14 | const TEST_ARN = "test_arn" 15 | 16 | type CallerIdentityMock struct { 17 | Account *string 18 | Arn *string 19 | UserId *string 20 | } 21 | 22 | var _ = Describe("aws_sdk_v2_factories", func() { 23 | Describe("sts_factory", func() { 24 | Context("Happy Flows", func() { 25 | var ( 26 | request *aws.Request 27 | resource *protocol.Resource 28 | ) 29 | BeforeEach(func() { 30 | request = &aws.Request{} 31 | resource = &protocol.Resource{ 32 | Metadata: map[string]string{}, 33 | Operation: "GetCallerIdentity", 34 | } 35 | }) 36 | It("Metadata Only is false, partial data", func() { 37 | account_data := TEST_ACCOUNT 38 | request.Data = &CallerIdentityMock{ 39 | Account: &account_data, 40 | } 41 | epsagonawsv2factories.StsDataFactory(request, resource, false, tracer.GlobalTracer) 42 | Expect(resource.Metadata["Account"]).To(Equal(TEST_ACCOUNT)) 43 | }) 44 | It("Metadata Only is false, full data", func() { 45 | account_data := TEST_ACCOUNT 46 | user_id_data := TEST_USER_ID 47 | arn_data := TEST_ARN 48 | request.Data = &CallerIdentityMock{ 49 | Account: &account_data, 50 | Arn: &arn_data, 51 | UserId: &user_id_data, 52 | } 53 | epsagonawsv2factories.StsDataFactory(request, resource, false, tracer.GlobalTracer) 54 | Expect(len(resource.Metadata)).To(Equal(3)) 55 | Expect(resource.Metadata["Account"]).To(Equal(TEST_ACCOUNT)) 56 | Expect(resource.Metadata["Arn"]).To(Equal(TEST_ARN)) 57 | Expect(resource.Metadata["UserId"]).To(Equal(TEST_USER_ID)) 58 | }) 59 | It("Metadata Only is true", func() { 60 | account_data := TEST_ACCOUNT 61 | user_id_data := TEST_USER_ID 62 | arn_data := TEST_ARN 63 | request.Data = &CallerIdentityMock{ 64 | Account: &account_data, 65 | Arn: &arn_data, 66 | UserId: &user_id_data, 67 | } 68 | epsagonawsv2factories.StsDataFactory(request, resource, true, tracer.GlobalTracer) 69 | Expect(len(resource.Metadata)).To(Equal(0)) 70 | }) 71 | }) 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /epsagon/common_utils.go: -------------------------------------------------------------------------------- 1 | package epsagon 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "reflect" 13 | "strings" 14 | 15 | "github.com/epsagon/epsagon-go/tracer" 16 | "github.com/onsi/gomega/types" 17 | ) 18 | 19 | // DefaultErrorType Default custom error type 20 | const DefaultErrorType = "Error" 21 | 22 | // MaxMetadataSize Maximum size of event metadata 23 | const MaxMetadataSize = 10 * 1024 24 | 25 | // HTTP Content types to ignore 26 | var ignoredContentTypes = [...]string{ 27 | "image", 28 | "audio", 29 | "video", 30 | "font", 31 | "zip", 32 | "css", 33 | } 34 | 35 | // HTTP request file types to ignore 36 | var ignoredFileTypes = [...]string{ 37 | ".js", 38 | ".jsx", 39 | ".woff", 40 | ".woff2", 41 | ".ttf", 42 | ".eot", 43 | ".ico", 44 | } 45 | 46 | // Config is the configuration for Epsagon's tracer 47 | type Config struct { 48 | tracer.Config 49 | } 50 | 51 | // GeneralEpsagonRecover recover function that will send exception to epsagon 52 | // exceptionType, msg are strings that will be added to the exception 53 | func GeneralEpsagonRecover(exceptionType, msg string, currentTracer tracer.Tracer) { 54 | if r := recover(); r != nil && currentTracer != nil { 55 | currentTracer.AddExceptionTypeAndMessage(exceptionType, fmt.Sprintf("%s:%+v", msg, r)) 56 | } 57 | } 58 | 59 | // NewTracerConfig creates a new tracer Config 60 | func NewTracerConfig(applicationName, token string) *Config { 61 | return &Config{ 62 | Config: tracer.Config{ 63 | ApplicationName: applicationName, 64 | Token: token, 65 | MetadataOnly: true, 66 | Debug: false, 67 | SendTimeout: "1s", 68 | MaxTraceSize: tracer.DefaultMaxTraceSize, 69 | }, 70 | } 71 | } 72 | 73 | // Label adds a label to the sent trace 74 | func Label(key string, value interface{}, args ...context.Context) { 75 | currentTracer := ExtractTracer(args) 76 | if currentTracer != nil { 77 | currentTracer.AddLabel(key, value) 78 | } 79 | } 80 | 81 | // Error adds an error to the sent trace 82 | func Error(value interface{}, args ...context.Context) { 83 | currentTracer := ExtractTracer(args) 84 | if currentTracer != nil { 85 | currentTracer.AddError(DefaultErrorType, value) 86 | } 87 | } 88 | 89 | // TypeError adds an error to the sent trace with specific error type 90 | func TypeError(value interface{}, errorType string, args ...context.Context) { 91 | currentTracer := ExtractTracer(args) 92 | if currentTracer != nil { 93 | currentTracer.AddError(errorType, value) 94 | } 95 | } 96 | 97 | // FormatHeaders format HTTP headers to string - using first header value, ignoring the rest 98 | func FormatHeaders(headers http.Header) (string, error) { 99 | headersToFormat := make(map[string]string) 100 | for headerKey, headerValues := range headers { 101 | if len(headerValues) > 0 { 102 | headersToFormat[headerKey] = headerValues[0] 103 | } 104 | } 105 | headersJSON, err := json.Marshal(headersToFormat) 106 | if err != nil { 107 | return "", err 108 | } 109 | return string(headersJSON), nil 110 | } 111 | 112 | // ExtractRequestData extracts headers and body from http.Request 113 | func ExtractRequestData(req *http.Request) (headers string, body string) { 114 | headers, err := FormatHeaders(req.Header) 115 | if err != nil { 116 | headers = "" 117 | } 118 | 119 | if req.Body == nil { 120 | return 121 | } 122 | 123 | buf, err := ioutil.ReadAll(req.Body) 124 | req.Body = NewReadCloser(buf, err) 125 | if err != nil { 126 | return 127 | } 128 | // truncates request body to the first 64KB 129 | trimmed := buf 130 | if len(buf) > MaxMetadataSize { 131 | trimmed = buf[0:MaxMetadataSize] 132 | } 133 | body = string(trimmed) 134 | return 135 | } 136 | 137 | // ShouldIgnoreRequest checks whether HTTP request should be ignored according 138 | // to given content type and request path 139 | func ShouldIgnoreRequest(contentType string, path string) bool { 140 | if len(contentType) > 0 { 141 | for _, ignoredContentType := range ignoredContentTypes { 142 | if strings.Contains(contentType, ignoredContentType) { 143 | return true 144 | } 145 | } 146 | } 147 | if len(path) > 0 { 148 | for _, ignoredFileSuffix := range ignoredFileTypes { 149 | if strings.HasSuffix(path, ignoredFileSuffix) { 150 | return true 151 | } 152 | } 153 | } 154 | return false 155 | } 156 | 157 | // NewReadCloser returns an io.ReadCloser 158 | // will mimick read from body depending on given error 159 | func NewReadCloser(body []byte, err error) io.ReadCloser { 160 | if err != nil { 161 | return &errorReader{err: err} 162 | } 163 | return ioutil.NopCloser(bytes.NewReader(body)) 164 | } 165 | 166 | // DebugLog logs helpful debugging messages 167 | 168 | func DebugLog(debugMode bool, args ...interface{}) { 169 | if debugMode { 170 | log.Println("[EPSAGON]", args) 171 | } 172 | } 173 | 174 | type errorReader struct { 175 | err error 176 | } 177 | 178 | func (er *errorReader) Read([]byte) (int, error) { 179 | return 0, er.err 180 | } 181 | func (er *errorReader) Close() error { 182 | return er.err 183 | } 184 | 185 | type matchUserError struct { 186 | exception interface{} 187 | } 188 | 189 | func (matcher *matchUserError) Match(actual interface{}) (bool, error) { 190 | uErr, ok := actual.(userError) 191 | if !ok { 192 | return false, fmt.Errorf("excpects userError, got %v", actual) 193 | } 194 | if !reflect.DeepEqual(uErr.exception, matcher.exception) { 195 | return false, fmt.Errorf("expected\n\t%v\nexception, got\n\t%v", matcher.exception, uErr.exception) 196 | } 197 | 198 | return true, nil 199 | } 200 | 201 | func (matcher *matchUserError) FailureMessage(actual interface{}) string { 202 | return fmt.Sprintf("Expected\n\t%#v\nto be userError with exception\n\t%#v", actual, matcher.exception) 203 | } 204 | 205 | func (matcher *matchUserError) NegatedFailureMessage(actual interface{}) string { 206 | return fmt.Sprintf("NegatedFailureMessage") 207 | } 208 | 209 | // MatchUserError matches epsagon exceptions 210 | func MatchUserError(exception interface{}) types.GomegaMatcher { 211 | return &matchUserError{ 212 | exception: exception, 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /epsagon/concurrent_generic_wrapper_test.go: -------------------------------------------------------------------------------- 1 | package epsagon_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "math/rand" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "strconv" 13 | "sync" 14 | "testing" 15 | "time" 16 | 17 | "github.com/epsagon/epsagon-go/epsagon" 18 | "github.com/epsagon/epsagon-go/protocol" 19 | "github.com/epsagon/epsagon-go/tracer" 20 | epsagonhttp "github.com/epsagon/epsagon-go/wrappers/net/http" 21 | . "github.com/onsi/ginkgo" 22 | . "github.com/onsi/gomega" 23 | ) 24 | 25 | const RunnerLabelKey = "test_identifier" 26 | 27 | func TestEpsagonConcurrentWrapper(t *testing.T) { 28 | RegisterFailHandler(Fail) 29 | RunSpecs(t, "Multiple Traces") 30 | } 31 | 32 | func sendRequest(wg *sync.WaitGroup, path string, testServer *httptest.Server) { 33 | defer wg.Done() 34 | time.Sleep(time.Duration(rand.Intn(500)) * time.Microsecond) 35 | client := http.Client{} 36 | response, err := client.Get(testServer.URL + path) 37 | Expect(err).To(BeNil()) 38 | defer response.Body.Close() 39 | responseData, err := ioutil.ReadAll(response.Body) 40 | Expect(err).To(BeNil()) 41 | responseString := string(responseData) 42 | Expect(responseString).To(Equal(path)) 43 | } 44 | 45 | func convertRequestUriToInt(requestUri string) (identifier int) { 46 | identifier, err := strconv.Atoi(requestUri[1:]) 47 | if err != nil { 48 | panic("failed to parse request uri - bad trace") 49 | } 50 | return 51 | } 52 | 53 | func parseEventID(event *protocol.Event) (identifier int) { 54 | resourceName := event.Resource.GetName() 55 | resourceURL, err := url.Parse(resourceName) 56 | if err != nil { 57 | panic("failed to parse event URL - bad trace") 58 | } 59 | identifier = convertRequestUriToInt(resourceURL.RequestURI()) 60 | return 61 | } 62 | 63 | func validateAgainstRunnerEvent(runnerEvent *protocol.Event, identifier int) { 64 | labels, ok := runnerEvent.Resource.Metadata[tracer.LabelsKey] 65 | if !ok { 66 | panic("no labels in runner event!") 67 | } 68 | var labelsMap map[string]interface{} 69 | err := json.Unmarshal([]byte(labels), &labelsMap) 70 | if err != nil { 71 | panic("bad labels map in runner event") 72 | } 73 | labelValue, ok := labelsMap[RunnerLabelKey] 74 | if !ok { 75 | panic("no identifier in runner event labels") 76 | } 77 | runnerIdentifier := convertRequestUriToInt(labelValue.(string)) 78 | Expect(runnerIdentifier).To(Equal(identifier)) 79 | } 80 | 81 | func waitForTraces(start int, end int, traceChannel chan *protocol.Trace, resourceName string, wg *sync.WaitGroup) { 82 | defer wg.Done() 83 | var trace *protocol.Trace 84 | receivedTraces := map[int]bool{} 85 | for i := start; i < end; i++ { 86 | receivedTraces[i] = false 87 | } 88 | ticker := time.NewTicker(8 * time.Second) 89 | for len(receivedTraces) > 0 { 90 | select { 91 | case trace = <-traceChannel: 92 | func() { 93 | Expect(len(trace.Events)).To(Equal(2)) 94 | if len(resourceName) > 0 { 95 | Expect(trace.Events[1].Resource.Name).To(Equal(resourceName)) 96 | } 97 | identifier := parseEventID(trace.Events[0]) 98 | if identifier < start || identifier >= end { 99 | panic("received unexpected event") 100 | } 101 | _, exists := receivedTraces[identifier] 102 | if !exists { 103 | panic("received duplicated event") 104 | } 105 | validateAgainstRunnerEvent(trace.Events[1], identifier) 106 | delete(receivedTraces, identifier) 107 | }() 108 | case <-ticker.C: 109 | panic("timeout while receiving traces") 110 | } 111 | } 112 | } 113 | 114 | type HandlerFunc func(res http.ResponseWriter, req *http.Request) 115 | 116 | func handleResponse(ctx context.Context, res http.ResponseWriter, req *http.Request) { 117 | client := http.Client{Transport: epsagonhttp.NewTracingTransport(ctx)} 118 | client.Get(fmt.Sprintf("https://www.google.com%s", req.RequestURI)) 119 | epsagon.Label(RunnerLabelKey, req.RequestURI, ctx) 120 | res.Write([]byte(req.RequestURI)) 121 | } 122 | 123 | func createTestHTTPServer(config *epsagon.Config, resourceName string) *httptest.Server { 124 | var concurrentWrapper epsagon.GenericFunction 125 | if len(resourceName) > 0 { 126 | concurrentWrapper = epsagon.ConcurrentGoWrapper( 127 | config, 128 | handleResponse, 129 | resourceName, 130 | ) 131 | } else { 132 | concurrentWrapper = epsagon.ConcurrentGoWrapper( 133 | config, 134 | handleResponse, 135 | ) 136 | } 137 | return httptest.NewServer(http.HandlerFunc( 138 | func(res http.ResponseWriter, req *http.Request) { 139 | concurrentWrapper(res, req) 140 | }, 141 | )) 142 | } 143 | 144 | var _ = Describe("multiple_traces", func() { 145 | Describe("http_server_tests", func() { 146 | Context("Happy Flows", func() { 147 | var ( 148 | traceCollectorServer *httptest.Server 149 | testServer *httptest.Server 150 | config *epsagon.Config 151 | traceChannel chan *protocol.Trace 152 | ) 153 | BeforeEach(func() { 154 | traceChannel = make(chan *protocol.Trace) 155 | traceCollectorServer = httptest.NewServer(http.HandlerFunc( 156 | func(res http.ResponseWriter, req *http.Request) { 157 | buf, err := ioutil.ReadAll(req.Body) 158 | if err != nil { 159 | panic(err) 160 | } 161 | var receivedTrace protocol.Trace 162 | err = json.Unmarshal(buf, &receivedTrace) 163 | if err != nil { 164 | panic(err) 165 | } 166 | traceChannel <- &receivedTrace 167 | res.Write([]byte("")) 168 | }, 169 | )) 170 | config = epsagon.NewTracerConfig("test", "test token") 171 | config.CollectorURL = traceCollectorServer.URL 172 | }) 173 | AfterEach(func() { 174 | testServer.Close() 175 | traceCollectorServer.Close() 176 | }) 177 | It("Multiple requests to test server", func() { 178 | resourceName := "" 179 | testServer = createTestHTTPServer(config, "") 180 | var wg sync.WaitGroup 181 | go waitForTraces(0, 50, traceChannel, resourceName, &wg) 182 | wg.Add(1) 183 | for i := 0; i < 50; i++ { 184 | wg.Add(1) 185 | go sendRequest(&wg, fmt.Sprintf("/%d", i), testServer) 186 | 187 | } 188 | wg.Wait() 189 | go waitForTraces(51, 100, traceChannel, resourceName, &wg) 190 | wg.Add(1) 191 | for i := 51; i < 100; i++ { 192 | wg.Add(1) 193 | go sendRequest(&wg, fmt.Sprintf("/%d", i), testServer) 194 | } 195 | wg.Wait() 196 | }) 197 | It("Custom runner resource name", func() { 198 | resourceName := "test-resource-name" 199 | testServer = createTestHTTPServer(config, resourceName) 200 | var wg sync.WaitGroup 201 | go waitForTraces(0, 1, traceChannel, resourceName, &wg) 202 | wg.Add(1) 203 | for i := 0; i < 1; i++ { 204 | wg.Add(1) 205 | go sendRequest(&wg, fmt.Sprintf("/%d", i), testServer) 206 | 207 | } 208 | wg.Wait() 209 | }) 210 | 211 | }) 212 | }) 213 | }) 214 | -------------------------------------------------------------------------------- /epsagon/generic_lambda_handler.go: -------------------------------------------------------------------------------- 1 | package epsagon 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/epsagon/epsagon-go/protocol" 8 | "github.com/epsagon/epsagon-go/tracer" 9 | "reflect" 10 | ) 11 | 12 | func errorHandler(e error) genericLambdaHandler { 13 | return func(ctx context.Context, payload json.RawMessage) (interface{}, error) { 14 | tracer.AddException(&protocol.Exception{ 15 | Type: "wrapper", 16 | Message: fmt.Sprintf("Error in wrapper: %v", e), 17 | Time: tracer.GetTimestamp(), 18 | }) 19 | return nil, e 20 | } 21 | } 22 | 23 | // validateArguments returns an error if the handler's arguments are 24 | // not compatible with aws lambda handlers 25 | // the boolean return value is wether or not the handler accepts context.Context 26 | // in its first argument. 27 | func validateArguments(handler reflect.Type) (bool, error) { 28 | handlerTakesContext := false 29 | if handler.NumIn() > 2 { 30 | return false, fmt.Errorf("handlers may not take more than two arguments, but handler takes %d", handler.NumIn()) 31 | } else if handler.NumIn() > 0 { 32 | contextType := reflect.TypeOf((*context.Context)(nil)).Elem() 33 | argumentType := handler.In(0) 34 | handlerTakesContext = argumentType.Implements(contextType) 35 | if handler.NumIn() > 1 && !handlerTakesContext { 36 | return false, fmt.Errorf("handler takes two arguments, but the first is not Context. got %s", argumentType.Kind()) 37 | } 38 | } 39 | 40 | return handlerTakesContext, nil 41 | } 42 | 43 | func validateReturns(handler reflect.Type) error { 44 | errorType := reflect.TypeOf((*error)(nil)).Elem() 45 | if handler.NumOut() > 2 { 46 | return fmt.Errorf("handler may not return more than two values") 47 | } else if handler.NumOut() > 1 { 48 | if !handler.Out(1).Implements(errorType) { 49 | return fmt.Errorf("handler returns two values, but the second does not implement error") 50 | } 51 | } else if handler.NumOut() == 1 { 52 | if !handler.Out(0).Implements(errorType) { 53 | return fmt.Errorf("handler returns a single value, but it does not implement error") 54 | } 55 | } 56 | return nil 57 | } 58 | 59 | func makeGenericHandler(handlerSymbol interface{}) genericLambdaHandler { 60 | if handlerSymbol == nil { 61 | return errorHandler(fmt.Errorf("handler is nil")) 62 | } 63 | handler := reflect.ValueOf(handlerSymbol) 64 | handlerType := reflect.TypeOf(handlerSymbol) 65 | if handlerType.Kind() != reflect.Func { 66 | return errorHandler(fmt.Errorf("handler kind %s is not %s", handlerType.Kind(), reflect.Func)) 67 | } 68 | 69 | takesContext, err := validateArguments(handlerType) 70 | if err != nil { 71 | return errorHandler(err) 72 | } 73 | 74 | if err := validateReturns(handlerType); err != nil { 75 | return errorHandler(err) 76 | } 77 | 78 | return func(ctx context.Context, payload json.RawMessage) (interface{}, error) { 79 | // construct arguments 80 | var args []reflect.Value 81 | if takesContext { 82 | args = append(args, reflect.ValueOf(ctx)) 83 | } 84 | if (handlerType.NumIn() == 1 && !takesContext) || handlerType.NumIn() == 2 { 85 | argType := handlerType.In(handlerType.NumIn() - 1) 86 | arg := reflect.New(argType) 87 | 88 | if err := json.Unmarshal(payload, arg.Interface()); err != nil { 89 | tracer.AddException(&protocol.Exception{ 90 | Type: "wrapper", 91 | Message: fmt.Sprintf("Error in wrapper: failed to convert arguments: %v", err), 92 | Time: tracer.GetTimestamp(), 93 | }) 94 | return nil, err 95 | } 96 | 97 | args = append(args, arg.Elem()) 98 | } 99 | 100 | response := handler.Call(args) 101 | 102 | // convert return values into (interface{}, error) 103 | var err error 104 | if len(response) > 0 { 105 | if errVal, ok := response[len(response)-1].Interface().(error); ok { 106 | err = errVal 107 | } 108 | } 109 | var val interface{} 110 | if len(response) > 1 { 111 | val = response[0].Interface() 112 | } 113 | 114 | return val, err 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /epsagon/generic_lambda_handler_test.go: -------------------------------------------------------------------------------- 1 | package epsagon 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | protocol "github.com/epsagon/epsagon-go/protocol" 8 | "github.com/epsagon/epsagon-go/tracer" 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | "reflect" 12 | ) 13 | 14 | var _ = Describe("GenericHandler suite", func() { 15 | Describe("validateArguments", func() { 16 | Context("Bad Handlers", func() { 17 | It("fails on too many arguments", func() { 18 | badHandler := func(ctx context.Context, a, b, c string) error { 19 | return nil 20 | } 21 | _, err := validateArguments(reflect.TypeOf(badHandler)) 22 | Expect(err).NotTo(Equal(nil)) 23 | }) 24 | It("fails if two arguments but first is not context", func() { 25 | badHandler := func(a string, ctx context.Context) error { 26 | return nil 27 | } 28 | _, err := validateArguments(reflect.TypeOf(badHandler)) 29 | Expect(err).NotTo(Equal(nil)) 30 | }) 31 | }) 32 | Context("Happy Handlers", func() { 33 | It("accepts no arguments", func() { 34 | goodHandler := func() error { 35 | return nil 36 | } 37 | hasCtx, err := validateArguments(reflect.TypeOf(goodHandler)) 38 | Expect(err).To(BeNil()) 39 | Expect(hasCtx).To(Equal(false)) 40 | }) 41 | It("accepts one argument not context", func() { 42 | goodHandler := func(a string) error { 43 | return nil 44 | } 45 | hasCtx, err := validateArguments(reflect.TypeOf(goodHandler)) 46 | Expect(err).To(BeNil()) 47 | Expect(hasCtx).To(Equal(false)) 48 | }) 49 | It("accepts two arguments when the first is context", func() { 50 | goodHandler := func(ctx context.Context, a string) error { 51 | return nil 52 | } 53 | hasCtx, err := validateArguments(reflect.TypeOf(goodHandler)) 54 | Expect(err).To(BeNil()) 55 | Expect(hasCtx).To(Equal(true)) 56 | }) 57 | }) 58 | }) 59 | Describe("validateReturns", func() { 60 | Context("Bad Handlers", func() { 61 | It("fails on too many returns", func() { 62 | badHandler := func() (string, error, string) { 63 | return "", nil, "" 64 | } 65 | err := validateReturns(reflect.TypeOf(badHandler)) 66 | Expect(err).NotTo(Equal(nil)) 67 | }) 68 | It("fails if last return is not error", func() { 69 | badHandler := func() (string, string) { 70 | return "", "" 71 | } 72 | err := validateReturns(reflect.TypeOf(badHandler)) 73 | Expect(err).NotTo(Equal(nil)) 74 | }) 75 | It("fails if retuns one thing that is not error", func() { 76 | badHandler := func() string { 77 | return "" 78 | } 79 | err := validateReturns(reflect.TypeOf(badHandler)) 80 | Expect(err).NotTo(Equal(nil)) 81 | }) 82 | }) 83 | Context("Good Handlers", func() { 84 | It("succeeds if no returns", func() { 85 | goodHandler := func() { 86 | return 87 | } 88 | err := validateReturns(reflect.TypeOf(goodHandler)) 89 | Expect(err).To(BeNil()) 90 | }) 91 | It("suceeds if only returns error", func() { 92 | goodHandler := func() error { 93 | return nil 94 | } 95 | err := validateReturns(reflect.TypeOf(goodHandler)) 96 | Expect(err).To(BeNil()) 97 | }) 98 | It("succeeds if returns something and error", func() { 99 | goodHandler := func() (string, error) { 100 | return "", nil 101 | } 102 | err := validateReturns(reflect.TypeOf(goodHandler)) 103 | Expect(err).To(BeNil()) 104 | }) 105 | }) 106 | }) 107 | 108 | Describe("makeGenericHandler", func() { 109 | var ( 110 | events []*protocol.Event 111 | exceptions []*protocol.Exception 112 | ) 113 | BeforeEach(func() { 114 | events = make([]*protocol.Event, 0) 115 | exceptions = make([]*protocol.Exception, 0) 116 | tracer.GlobalTracer = &tracer.MockedEpsagonTracer{ 117 | Events: &events, 118 | Exceptions: &exceptions, 119 | } 120 | }) 121 | 122 | Context("Bad Handlers", func() { 123 | It("fails if handlers return types are bad", func() { 124 | badHandler := func() (string, string) { 125 | return "", "" 126 | } 127 | generic := makeGenericHandler(badHandler) 128 | bg := context.Background() 129 | _, err := generic(bg, []byte{}) 130 | 131 | Expect(err).NotTo(BeNil()) 132 | Expect(len(exceptions)).To(BeNumerically("==", 1)) 133 | }) 134 | It("fails if handler is not a function", func() { 135 | badHandler := "not a function" 136 | generic := makeGenericHandler(badHandler) 137 | bg := context.Background() 138 | _, err := generic(bg, []byte{}) 139 | 140 | Expect(err).NotTo(BeNil()) 141 | Expect(len(exceptions)).To(BeNumerically("==", 1)) 142 | }) 143 | It("fails if handler is nil", func() { 144 | generic := makeGenericHandler(nil) 145 | bg := context.Background() 146 | _, err := generic(bg, []byte{}) 147 | 148 | Expect(err).NotTo(BeNil()) 149 | Expect(len(exceptions)).To(BeNumerically("==", 1)) 150 | }) 151 | It("fails if handlers arguments are bad", func() { 152 | badHandler := func(string, string) {} 153 | generic := makeGenericHandler(badHandler) 154 | bg := context.Background() 155 | _, err := generic(bg, []byte{}) 156 | 157 | Expect(err).NotTo(BeNil()) 158 | Expect(len(exceptions)).To(BeNumerically("==", 1)) 159 | }) 160 | }) 161 | Context("Handler arguments are not json compatible to input", func() { 162 | type t1 struct { 163 | a1 string 164 | b1 string 165 | } 166 | type t2 struct { 167 | a2 int 168 | } 169 | It("fails when target type is not JSONable", func() { 170 | msg, err := json.Marshal(t1{a1: "hello", b1: "world"}) 171 | if err != nil { 172 | Fail(fmt.Sprintf("Failed to marshal json %v", err)) 173 | } 174 | type myFuncType func(int, int) error 175 | generic := makeGenericHandler(func(f myFuncType) { f(1, 2) }) 176 | bg := context.Background() 177 | _, err = generic(bg, msg) 178 | Expect(err).NotTo(BeNil()) 179 | Expect(len(exceptions)).To(BeNumerically("==", 1)) 180 | }) 181 | It("constructs available fields when not everything is available in json", func() { 182 | msg, err := json.Marshal(t1{b1: "world"}) 183 | if err != nil { 184 | Fail(fmt.Sprintf("Failed to marshal json %v", err)) 185 | } 186 | generic := makeGenericHandler(func(x t1) {}) 187 | bg := context.Background() 188 | _, err = generic(bg, msg) 189 | Expect(err).To(BeNil()) 190 | Expect(len(exceptions)).To(BeNumerically("==", 0)) 191 | }) 192 | }) 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /epsagon/generic_wrapper.go: -------------------------------------------------------------------------------- 1 | package epsagon 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "runtime" 7 | "runtime/debug" 8 | 9 | "github.com/epsagon/epsagon-go/protocol" 10 | "github.com/epsagon/epsagon-go/tracer" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | type userError struct { 15 | exception interface{} 16 | stack string 17 | } 18 | 19 | // GenericWrapper is a generic lambda function type 20 | type GenericWrapper struct { 21 | handler reflect.Value 22 | config *Config 23 | tracer tracer.Tracer 24 | runner *protocol.Event 25 | thrownError interface{} 26 | resourceName string 27 | invoked bool 28 | invoking bool 29 | dontAddRunner bool 30 | injectContext bool 31 | } 32 | 33 | // WrapGenericFunction return an epsagon wrapper for a generic function 34 | func WrapGenericFunction( 35 | handler interface{}, config *Config, tracer tracer.Tracer, injectContext bool, resourceName string) *GenericWrapper { 36 | return &GenericWrapper{ 37 | config: config, 38 | handler: reflect.ValueOf(handler), 39 | tracer: tracer, 40 | injectContext: injectContext, 41 | resourceName: resourceName, 42 | } 43 | } 44 | 45 | // GetRunnerEvent returns the wrapper runner event 46 | func (wrapper *GenericWrapper) GetRunnerEvent() *protocol.Event { 47 | return wrapper.runner 48 | } 49 | 50 | // createRunner creates a runner event but does not add it to the tracer 51 | // the runner is saved for further manipulations at wrapper.runner 52 | func (wrapper *GenericWrapper) createRunner() { 53 | resourceName := wrapper.resourceName 54 | if len(resourceName) == 0 { 55 | resourceName = runtime.FuncForPC(wrapper.handler.Pointer()).Name() 56 | } 57 | wrapper.runner = &protocol.Event{ 58 | Id: uuid.New().String(), 59 | Origin: "runner", 60 | StartTime: tracer.GetTimestamp(), 61 | Resource: &protocol.Resource{ 62 | Name: resourceName, 63 | Type: "go-function", 64 | Operation: "invoke", 65 | Metadata: make(map[string]string), 66 | }, 67 | ErrorCode: protocol.ErrorCode_OK, 68 | } 69 | } 70 | 71 | // For instances when you want to add event but can't risk exception 72 | func (wrapper *GenericWrapper) safeAddRunnerEvent() { 73 | defer func() { 74 | recover() 75 | }() 76 | wrapper.addRunnerEvent() 77 | } 78 | 79 | func (wrapper *GenericWrapper) addRunnerEvent() { 80 | if wrapper.dontAddRunner { 81 | return 82 | } 83 | endTime := tracer.GetTimestamp() 84 | wrapper.runner.Duration = endTime - wrapper.runner.StartTime 85 | wrapper.tracer.AddEvent(wrapper.runner) 86 | } 87 | 88 | // Change the arguments from interface{} to reflect.Value array 89 | func (wrapper *GenericWrapper) transformArguments(args ...interface{}) []reflect.Value { 90 | actualLength := len(args) 91 | if wrapper.injectContext { 92 | actualLength += 1 93 | } 94 | if wrapper.handler.Type().NumIn() != actualLength { 95 | msg := fmt.Sprintf( 96 | "Wrong number of args: %d, expected: %d", 97 | actualLength, wrapper.handler.Type().NumIn()) 98 | wrapper.createRunner() 99 | wrapper.runner.Exception = &protocol.Exception{ 100 | Type: "Runtime Error", 101 | Message: fmt.Sprintf("%v", msg), 102 | Time: tracer.GetTimestamp(), 103 | } 104 | wrapper.safeAddRunnerEvent() 105 | panic(msg) 106 | } 107 | // add new context to inputs 108 | inputs := make([]reflect.Value, actualLength) 109 | argsInputs := inputs 110 | if wrapper.injectContext { 111 | inputs[0] = reflect.ValueOf(ContextWithTracer(wrapper.tracer)) 112 | argsInputs = argsInputs[1:] 113 | } 114 | for k, in := range args { 115 | argsInputs[k] = reflect.ValueOf(in) 116 | } 117 | return inputs 118 | } 119 | 120 | // Call the wrapped function 121 | func (wrapper *GenericWrapper) Call(args ...interface{}) (results []reflect.Value) { 122 | inputs := wrapper.transformArguments(args...) 123 | defer func() { 124 | wrapper.thrownError = recover() 125 | if wrapper.thrownError != nil { 126 | exception := &protocol.Exception{ 127 | Type: "Runtime Error", 128 | Message: fmt.Sprintf("%v", wrapper.thrownError), 129 | Traceback: string(debug.Stack()), 130 | Time: tracer.GetTimestamp(), 131 | } 132 | if wrapper.invoking { 133 | wrapper.runner.Exception = exception 134 | wrapper.runner.ErrorCode = protocol.ErrorCode_EXCEPTION 135 | wrapper.safeAddRunnerEvent() 136 | panic(userError{ 137 | exception: wrapper.thrownError, 138 | stack: exception.Traceback, 139 | }) 140 | } else { 141 | exception.Type = "GenericEpsagonWrapper" 142 | wrapper.tracer.AddException(exception) 143 | if !wrapper.invoked { // attempt to run the user's function untraced 144 | results = wrapper.handler.Call(inputs) 145 | } 146 | } 147 | } 148 | }() 149 | 150 | wrapper.createRunner() 151 | wrapper.invoking = true 152 | wrapper.invoked = true 153 | results = wrapper.handler.Call(inputs) 154 | wrapper.invoking = false 155 | wrapper.addRunnerEvent() 156 | return results 157 | } 158 | 159 | // GenericFunction type 160 | type GenericFunction func(args ...interface{}) []reflect.Value 161 | 162 | func getResourceName(args []string) (resourceName string) { 163 | if len(args) > 0 { 164 | resourceName = args[0] 165 | } 166 | return 167 | } 168 | 169 | // GoWrapper wraps the function with epsagon's tracer 170 | func GoWrapper(config *Config, wrappedFunction interface{}, args ...string) GenericFunction { 171 | resourceName := getResourceName(args) 172 | return func(args ...interface{}) []reflect.Value { 173 | if config == nil { 174 | config = &Config{} 175 | } 176 | wrapperTracer := tracer.CreateGlobalTracer(&config.Config) 177 | wrapperTracer.Start() 178 | defer wrapperTracer.Stop() 179 | 180 | wrapper := &GenericWrapper{ 181 | config: config, 182 | handler: reflect.ValueOf(wrappedFunction), 183 | tracer: wrapperTracer, 184 | resourceName: resourceName, 185 | } 186 | return wrapper.Call(args...) 187 | } 188 | } 189 | 190 | // ConcurrentGoWrapper wraps the function with epsagon's tracer 191 | func ConcurrentGoWrapper(config *Config, wrappedFunction interface{}, args ...string) GenericFunction { 192 | resourceName := getResourceName(args) 193 | return func(args ...interface{}) []reflect.Value { 194 | if config == nil { 195 | config = &Config{} 196 | } 197 | wrapperTracer := tracer.CreateTracer(&config.Config) 198 | wrapperTracer.Start() 199 | defer wrapperTracer.Stop() 200 | 201 | wrapper := &GenericWrapper{ 202 | config: config, 203 | handler: reflect.ValueOf(wrappedFunction), 204 | tracer: wrapperTracer, 205 | injectContext: true, 206 | resourceName: resourceName, 207 | } 208 | return wrapper.Call(args...) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /epsagon/generic_wrapper_test.go: -------------------------------------------------------------------------------- 1 | package epsagon 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/epsagon/epsagon-go/protocol" 7 | "github.com/epsagon/epsagon-go/tracer" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("generic_wrapper", func() { 13 | Describe("GoWrapper", func() { 14 | Context("called with nil config", func() { 15 | It("Returns a valid function", func() { 16 | wrapper := GoWrapper(nil, func() {}) 17 | wrapperType := reflect.TypeOf(wrapper) 18 | Expect(wrapperType.Kind()).To(Equal(reflect.Func)) 19 | }) 20 | }) 21 | }) 22 | Describe("GenericWrapper", func() { 23 | Context("Happy Flows", func() { 24 | var ( 25 | events []*protocol.Event 26 | exceptions []*protocol.Exception 27 | ) 28 | BeforeEach(func() { 29 | events = make([]*protocol.Event, 0) 30 | exceptions = make([]*protocol.Exception, 0) 31 | tracer.GlobalTracer = &tracer.MockedEpsagonTracer{ 32 | Events: &events, 33 | Exceptions: &exceptions, 34 | } 35 | }) 36 | It("Calls the user function", func() { 37 | called := false 38 | wrapper := &GenericWrapper{ 39 | config: &Config{}, 40 | handler: reflect.ValueOf(func() { called = true }), 41 | tracer: tracer.GlobalTracer, 42 | } 43 | wrapper.Call() 44 | Expect(called).To(Equal(true)) 45 | Expect(len(events)).To(Equal(1)) 46 | }) 47 | It("Calls the user function with custom resource name", func() { 48 | called := false 49 | resourceName := "test-resource-name" 50 | wrapper := &GenericWrapper{ 51 | config: &Config{}, 52 | handler: reflect.ValueOf(func() { called = true }), 53 | tracer: tracer.GlobalTracer, 54 | resourceName: resourceName, 55 | } 56 | wrapper.Call() 57 | Expect(called).To(Equal(true)) 58 | Expect(len(events)).To(Equal(1)) 59 | Expect(events[0].Resource.Name).To(Equal(resourceName)) 60 | }) 61 | It("Retuns and accepts arguments", func() { 62 | called := false 63 | result := false 64 | wrapper := &GenericWrapper{ 65 | config: &Config{}, 66 | handler: reflect.ValueOf( 67 | func(x bool) bool { 68 | called = x 69 | return x 70 | }), 71 | tracer: tracer.GlobalTracer, 72 | } 73 | result = wrapper.Call(true)[0].Bool() 74 | Expect(called).To(Equal(true)) 75 | Expect(result).To(Equal(true)) 76 | Expect(len(events)).To(Equal(1)) 77 | }) 78 | }) 79 | Context("Error Flows", func() { 80 | var ( 81 | events []*protocol.Event 82 | exceptions []*protocol.Exception 83 | ) 84 | BeforeEach(func() { 85 | events = make([]*protocol.Event, 0) 86 | exceptions = make([]*protocol.Exception, 0) 87 | tracer.GlobalTracer = &tracer.MockedEpsagonTracer{ 88 | Events: &events, 89 | Exceptions: &exceptions, 90 | } 91 | }) 92 | It("Panics for wrong number of arguments", func() { 93 | called := false 94 | wrapper := &GenericWrapper{ 95 | config: &Config{}, 96 | handler: reflect.ValueOf(func(x bool) { called = x }), 97 | tracer: tracer.GlobalTracer, 98 | } 99 | Expect(func() { wrapper.Call() }).To(Panic()) 100 | Expect(called).To(Equal(false)) 101 | Expect(len(events)).To(Equal(1)) 102 | Expect(events[0].Exception).NotTo(Equal(nil)) 103 | }) 104 | It("Failed to add event", func() { 105 | tracer.GlobalTracer.(*tracer.MockedEpsagonTracer).PanicAddEvent = true 106 | called := false 107 | wrapper := &GenericWrapper{ 108 | config: &Config{}, 109 | handler: reflect.ValueOf(func() { called = true }), 110 | tracer: tracer.GlobalTracer, 111 | } 112 | wrapper.Call() 113 | Expect(called).To(Equal(true)) 114 | Expect(len(exceptions)).To(Equal(1)) 115 | }) 116 | It("User function panics", func() { 117 | wrapper := &GenericWrapper{ 118 | config: &Config{}, 119 | handler: reflect.ValueOf(func() { panic("boom") }), 120 | tracer: tracer.GlobalTracer, 121 | } 122 | Expect(func() { wrapper.Call() }).To( 123 | PanicWith(MatchUserError("boom"))) 124 | Expect(len(events)).To(Equal(1)) 125 | Expect(events[0].Exception).NotTo(Equal(nil)) 126 | }) 127 | }) 128 | }) 129 | }) 130 | -------------------------------------------------------------------------------- /epsagon/lambda_trigger_test.go: -------------------------------------------------------------------------------- 1 | package epsagon 2 | 3 | import ( 4 | // "fmt" 5 | "encoding/json" 6 | "time" 7 | 8 | lambdaEvents "github.com/aws/aws-lambda-go/events" 9 | "github.com/epsagon/epsagon-go/protocol" 10 | "github.com/epsagon/epsagon-go/tracer" 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | ) 14 | 15 | type inventedEvent struct { 16 | Name string 17 | Job string 18 | DateOfBirth time.Time 19 | } 20 | 21 | var ( 22 | exampleAPIGateWay = lambdaEvents.APIGatewayProxyRequest{ 23 | Resource: "test-resource", 24 | Path: "/hello", 25 | HTTPMethod: "GET", 26 | Body: "hello world", 27 | IsBase64Encoded: false, 28 | Headers: map[string]string{ 29 | "hello": "world", 30 | }, 31 | StageVariables: map[string]string{ 32 | "hello": "world", 33 | }, 34 | PathParameters: map[string]string{ 35 | "hello": "world", 36 | }, 37 | QueryStringParameters: map[string]string{ 38 | "hello": "world", 39 | }, 40 | } 41 | exampleAPIGatewayV2HTTP = lambdaEvents.APIGatewayV2HTTPRequest{ 42 | RawPath: "/hello", 43 | RequestContext: lambdaEvents.APIGatewayV2HTTPRequestContext{ 44 | APIID: "test-api", 45 | HTTP: lambdaEvents.APIGatewayV2HTTPRequestContextHTTPDescription{ 46 | Method: "GET", 47 | }, 48 | }, 49 | Body: "hello world", 50 | IsBase64Encoded: false, 51 | Headers: map[string]string{ 52 | "hello": "world", 53 | }, 54 | StageVariables: map[string]string{ 55 | "hello": "world", 56 | }, 57 | PathParameters: map[string]string{ 58 | "hello": "world", 59 | }, 60 | QueryStringParameters: map[string]string{ 61 | "hello": "world", 62 | }, 63 | } 64 | exampleDDB = lambdaEvents.DynamoDBEvent{ 65 | Records: []lambdaEvents.DynamoDBEventRecord{ 66 | lambdaEvents.DynamoDBEventRecord{ 67 | AWSRegion: "us-east-1", 68 | EventSourceArn: "test/1/2", 69 | EventSource: "aws:dynamodb", 70 | EventName: "PutItem", 71 | Change: lambdaEvents.DynamoDBStreamRecord{ 72 | SequenceNumber: "test_sequence_number", 73 | NewImage: map[string]lambdaEvents.DynamoDBAttributeValue{ 74 | "test2": lambdaEvents.NewStringAttribute("2"), 75 | }, 76 | OldImage: map[string]lambdaEvents.DynamoDBAttributeValue{ 77 | "test1": lambdaEvents.NewStringAttribute("1"), 78 | }, 79 | }, 80 | }, 81 | }, 82 | } 83 | exampleInventedEvent = inventedEvent{ 84 | Name: "Erez Freiberger", 85 | Job: "Software Engineer", 86 | DateOfBirth: time.Now(), 87 | } 88 | ) 89 | 90 | func verifyLabelValue(key string, value string, labelsMap map[string]string) { 91 | labelValue, ok := labelsMap[key] 92 | Expect(ok).To(BeTrue()) 93 | Expect(labelValue).To(Equal(value)) 94 | } 95 | 96 | var _ = Describe("epsagon trigger suite", func() { 97 | Describe("addLambdaTrigger", func() { 98 | var ( 99 | events []*protocol.Event 100 | exceptions []*protocol.Exception 101 | ) 102 | BeforeEach(func() { 103 | events = make([]*protocol.Event, 0) 104 | exceptions = make([]*protocol.Exception, 0) 105 | tracer.GlobalTracer = &tracer.MockedEpsagonTracer{ 106 | Events: &events, 107 | Exceptions: &exceptions, 108 | } 109 | }) 110 | 111 | Context("Handling of known trigger - API Gateway", func() { 112 | It("Identifies the first known handler, API Gateway - REST", func() { 113 | exampleJSON, err := json.Marshal(exampleAPIGateWay) 114 | if err != nil { 115 | Fail("Failed to marshal json") 116 | } 117 | addLambdaTrigger(json.RawMessage(exampleJSON), false, triggerFactories, tracer.GlobalTracer) 118 | Expect(len(events)).To(BeNumerically("==", 1)) 119 | Expect(events[0].Resource.Type).To(Equal("api_gateway")) 120 | }) 121 | 122 | It("Identifies the first known handler, API Gateway - HTTP", func() { 123 | exampleJSON, err := json.Marshal(exampleAPIGatewayV2HTTP) 124 | if err != nil { 125 | Fail("Failed to marshal json") 126 | } 127 | addLambdaTrigger(json.RawMessage(exampleJSON), false, triggerFactories, tracer.GlobalTracer) 128 | Expect(len(events)).To(BeNumerically("==", 1)) 129 | Expect(events[0].Resource.Type).To(Equal("api_gateway")) 130 | }) 131 | }) 132 | Context("Handling of known trigger - DynamoDB", func() { 133 | It("Identifies the first known handler, DynamoDB", func() { 134 | exampleJSON, err := json.Marshal(exampleDDB) 135 | if err != nil { 136 | Fail("Failed to marshal json") 137 | } 138 | addLambdaTrigger(json.RawMessage(exampleJSON), false, triggerFactories, tracer.GlobalTracer) 139 | Expect(len(events)).To(BeNumerically("==", 1)) 140 | Expect(events[0].Resource.Type).To(Equal("dynamodb")) 141 | verifyLabelValue("region", "us-east-1", events[0].Resource.Metadata) 142 | verifyLabelValue( 143 | "New Image", 144 | "{\"test2\":\"{\\n S: \\\"2\\\"\\n}\"}", 145 | events[0].Resource.Metadata, 146 | ) 147 | verifyLabelValue( 148 | "Old Image", 149 | "{\"test1\":\"{\\n S: \\\"1\\\"\\n}\"}", 150 | events[0].Resource.Metadata, 151 | ) 152 | }) 153 | }) 154 | Context("Handling of known trigger with extra fields", func() { 155 | It("Identifies the first known handler", func() { 156 | exampleJSON, err := json.Marshal(exampleAPIGateWay) 157 | if err != nil { 158 | Fail("Failed to marshal json") 159 | } 160 | var rawEvent map[string]interface{} 161 | err = json.Unmarshal(exampleJSON, &rawEvent) 162 | if err != nil { 163 | Fail("Failed to unmarshal json into rawEvent map[string]interface{}") 164 | } 165 | rawEvent["ExtraFields"] = "BOOOM" 166 | exampleJSON, err = json.Marshal(rawEvent) 167 | if err != nil { 168 | Fail("Failed to marshal json from rawEvent") 169 | } 170 | addLambdaTrigger(json.RawMessage(exampleJSON), false, triggerFactories, tracer.GlobalTracer) 171 | Expect(len(events)).To(BeNumerically("==", 1)) 172 | Expect(events[0].Resource.Type).To(Equal("api_gateway")) 173 | }) 174 | }) 175 | 176 | Context("Handling of unknown trigger", func() { 177 | It("Adds a JSON Event", func() { 178 | exampleJSON, err := json.Marshal(exampleInventedEvent) 179 | if err != nil { 180 | Fail("Failed to marshal json") 181 | } 182 | addLambdaTrigger(json.RawMessage(exampleJSON), false, triggerFactories, tracer.GlobalTracer) 183 | Expect(len(events)).To(BeNumerically("==", 1)) 184 | Expect(events[0].Resource.Type).To(Equal("json")) 185 | }) 186 | }) 187 | }) 188 | }) 189 | -------------------------------------------------------------------------------- /epsagon/lambda_wrapper.go: -------------------------------------------------------------------------------- 1 | package epsagon 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "runtime/debug" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/aws/aws-lambda-go/lambdacontext" 14 | "github.com/epsagon/epsagon-go/protocol" 15 | "github.com/epsagon/epsagon-go/tracer" 16 | ) 17 | 18 | var ( 19 | coldStart = true 20 | ) 21 | 22 | const TimeoutErrorCode protocol.ErrorCode = 3 23 | 24 | type genericLambdaHandler func(context.Context, json.RawMessage) (interface{}, error) 25 | 26 | // epsagonLambdaWrapper is a generic lambda function type 27 | type epsagonLambdaWrapper struct { 28 | handler genericLambdaHandler 29 | config *Config 30 | tracer tracer.Tracer 31 | invoked bool 32 | invoking bool 33 | timeout bool 34 | } 35 | 36 | type preInvokeData struct { 37 | InvocationMetadata map[string]string 38 | LambdaContext *lambdacontext.LambdaContext 39 | StartTime float64 40 | } 41 | 42 | type invocationData struct { 43 | ExceptionInfo *protocol.Exception 44 | errorStatus protocol.ErrorCode 45 | result interface{} 46 | err error 47 | thrownError interface{} 48 | } 49 | 50 | func getAWSAccount(lc *lambdacontext.LambdaContext) string { 51 | arnParts := strings.Split(lc.InvokedFunctionArn, ":") 52 | if len(arnParts) >= 4 { 53 | return arnParts[4] 54 | } 55 | return "" 56 | } 57 | 58 | func createLambdaEvent(preInvokeInfo *preInvokeData) *protocol.Event { 59 | endTime := tracer.GetTimestamp() 60 | duration := endTime - preInvokeInfo.StartTime 61 | 62 | return &protocol.Event{ 63 | Id: preInvokeInfo.LambdaContext.AwsRequestID, 64 | StartTime: preInvokeInfo.StartTime, 65 | Resource: &protocol.Resource{ 66 | Name: lambdacontext.FunctionName, 67 | Type: "lambda", 68 | Operation: "invoke", 69 | Metadata: preInvokeInfo.InvocationMetadata, 70 | }, 71 | Origin: "runner", 72 | Duration: duration, 73 | } 74 | } 75 | 76 | func (wrapper *epsagonLambdaWrapper) preInvokeOps( 77 | ctx context.Context, payload json.RawMessage) (info *preInvokeData) { 78 | startTime := tracer.GetTimestamp() 79 | metadata := map[string]string{} 80 | lc, ok := lambdacontext.FromContext(ctx) 81 | if !ok { 82 | lc = &lambdacontext.LambdaContext{} 83 | } 84 | defer func() { 85 | if r := recover(); r != nil { 86 | wrapper.tracer.AddExceptionTypeAndMessage("LambdaWrapper", 87 | fmt.Sprintf("preInvokeOps:%+v", r)) 88 | info = &preInvokeData{ 89 | LambdaContext: lc, 90 | StartTime: startTime, 91 | InvocationMetadata: metadata, 92 | } 93 | } 94 | }() 95 | 96 | metadata = map[string]string{ 97 | "log_stream_name": lambdacontext.LogStreamName, 98 | "log_group_name": lambdacontext.LogGroupName, 99 | "function_version": lambdacontext.FunctionVersion, 100 | "memory": strconv.Itoa(lambdacontext.MemoryLimitInMB), 101 | "cold_start": strconv.FormatBool(coldStart), 102 | "aws_account": getAWSAccount(lc), 103 | "region": os.Getenv("AWS_REGION"), 104 | } 105 | coldStart = false 106 | 107 | addLambdaTrigger(payload, wrapper.config.MetadataOnly, triggerFactories, wrapper.tracer) 108 | 109 | return &preInvokeData{ 110 | InvocationMetadata: metadata, 111 | LambdaContext: lc, 112 | StartTime: startTime, 113 | } 114 | } 115 | 116 | func (wrapper *epsagonLambdaWrapper) postInvokeOps( 117 | preInvokeInfo *preInvokeData, 118 | invokeInfo *invocationData) { 119 | defer func() { 120 | if r := recover(); r != nil { 121 | wrapper.tracer.AddExceptionTypeAndMessage("LambdaWrapper", fmt.Sprintf("postInvokeOps:%+v", r)) 122 | } 123 | }() 124 | 125 | lambdaEvent := createLambdaEvent(preInvokeInfo) 126 | lambdaEvent.ErrorCode = invokeInfo.errorStatus 127 | lambdaEvent.Exception = invokeInfo.ExceptionInfo 128 | 129 | if !wrapper.config.MetadataOnly { 130 | result, err := json.Marshal(invokeInfo.result) 131 | if err == nil { 132 | lambdaEvent.Resource.Metadata["return_value"] = string(result) 133 | } else { 134 | lambdaEvent.Resource.Metadata["return_value"] = fmt.Sprintf("%+v", invokeInfo.result) 135 | } 136 | } 137 | 138 | wrapper.tracer.AddEvent(lambdaEvent) 139 | } 140 | 141 | // Invoke calls the wrapper, and creates a tracer for that duration. 142 | func (wrapper *epsagonLambdaWrapper) Invoke(ctx context.Context, payload json.RawMessage) (result interface{}, err error) { 143 | invokeInfo := &invocationData{} 144 | wrapper.invoked = false 145 | wrapper.invoking = false 146 | defer func() { 147 | if !wrapper.invoking { 148 | recover() 149 | } 150 | if !wrapper.invoked { 151 | result, err = wrapper.handler(ctx, payload) 152 | } 153 | if invokeInfo.thrownError != nil { 154 | panic(userError{ 155 | exception: invokeInfo.thrownError, 156 | stack: invokeInfo.ExceptionInfo.Traceback, 157 | }) 158 | } 159 | }() 160 | 161 | preInvokeInfo := wrapper.preInvokeOps(ctx, payload) 162 | go wrapper.trackTimeout(ctx, preInvokeInfo) 163 | wrapper.InvokeClientLambda(ctx, payload, invokeInfo) 164 | if !wrapper.timeout { 165 | wrapper.postInvokeOps(preInvokeInfo, invokeInfo) 166 | } 167 | 168 | return invokeInfo.result, invokeInfo.err 169 | } 170 | 171 | func (wrapper *epsagonLambdaWrapper) trackTimeout(ctx context.Context, preInvokeInfo *preInvokeData) { 172 | deadline, isDeadlineSet := ctx.Deadline() 173 | if isDeadlineSet { 174 | thresholdDuration := time.Duration(tracer.GetLambdaTimeoutThresholdMs()) 175 | deadline = deadline.Add(-thresholdDuration * time.Millisecond) 176 | timeoutChannel := time.After(time.Until(deadline)) 177 | 178 | for range timeoutChannel { 179 | if wrapper.invoking { 180 | wrapper.timeout = true 181 | 182 | lambdaEvent := createLambdaEvent(preInvokeInfo) 183 | lambdaEvent.ErrorCode = TimeoutErrorCode 184 | 185 | wrapper.tracer.AddEvent(lambdaEvent) 186 | wrapper.tracer.Stop() 187 | } 188 | } 189 | } 190 | } 191 | 192 | func (wrapper *epsagonLambdaWrapper) InvokeClientLambda( 193 | ctx context.Context, payload json.RawMessage, invokeInfo *invocationData) { 194 | defer func() { 195 | invokeInfo.thrownError = recover() 196 | if invokeInfo.thrownError != nil { 197 | invokeInfo.ExceptionInfo = &protocol.Exception{ 198 | Type: "Runtime Error", 199 | Message: fmt.Sprintf("%v", invokeInfo.thrownError), 200 | Traceback: string(debug.Stack()), 201 | Time: tracer.GetTimestamp(), 202 | } 203 | invokeInfo.errorStatus = protocol.ErrorCode_EXCEPTION 204 | } 205 | }() 206 | 207 | invokeInfo.errorStatus = protocol.ErrorCode_OK 208 | // calling the actual function: 209 | wrapper.invoked = true 210 | wrapper.invoking = true 211 | result, err := wrapper.handler(ctx, payload) 212 | wrapper.invoking = false 213 | if err != nil { 214 | invokeInfo.errorStatus = protocol.ErrorCode_ERROR 215 | invokeInfo.ExceptionInfo = &protocol.Exception{ 216 | Type: "Error Result", 217 | Message: err.Error(), 218 | Traceback: "", 219 | Time: tracer.GetTimestamp(), 220 | } 221 | } 222 | invokeInfo.result = result 223 | invokeInfo.err = err 224 | } 225 | 226 | // WrapLambdaHandler wraps a generic wrapper for lambda function with epsagon tracing 227 | func WrapLambdaHandler(config *Config, handler interface{}) interface{} { 228 | return func(ctx context.Context, payload json.RawMessage) (interface{}, error) { 229 | wrapperTracer := tracer.CreateGlobalTracer(&config.Config) 230 | wrapperTracer.Start() 231 | 232 | wrapper := &epsagonLambdaWrapper{ 233 | config: config, 234 | handler: makeGenericHandler(handler), 235 | tracer: wrapperTracer, 236 | } 237 | 238 | defer func() { 239 | if !wrapper.timeout { 240 | wrapperTracer.Stop() 241 | } 242 | }() 243 | 244 | return wrapper.Invoke(ctx, payload) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /epsagon/tracer_helpers.go: -------------------------------------------------------------------------------- 1 | package epsagon 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/epsagon/epsagon-go/tracer" 7 | ) 8 | 9 | type tracerKey string 10 | 11 | const tracerKeyValue tracerKey = "tracer" 12 | 13 | // ContextWithTracer creates a context with given tracer 14 | func ContextWithTracer(t tracer.Tracer, ctx ...context.Context) context.Context { 15 | if len(ctx) == 1 { 16 | return context.WithValue(ctx[0], tracerKeyValue, t) 17 | } 18 | return context.WithValue(context.Background(), tracerKeyValue, t) 19 | } 20 | 21 | // ExtractTracer Extracts the tracer from given contexts (using first context), 22 | // returns Global tracer if no context is given and GlobalTracer is valid (= non nil, not stopped) 23 | func ExtractTracer(ctx []context.Context) tracer.Tracer { 24 | if len(ctx) == 0 { 25 | if tracer.GlobalTracer == nil || tracer.GlobalTracer.Stopped() { 26 | return nil 27 | } 28 | return tracer.GlobalTracer 29 | } 30 | rawValue := ctx[0].Value(tracerKeyValue) 31 | if rawValue == nil { 32 | panic("Invalid context, see Epsagon Concurrent Generic GO function example") 33 | } 34 | tracerValue, ok := rawValue.(tracer.Tracer) 35 | if !ok { 36 | panic("Invalid context value, see Epsagon Concurrent Generic GO function example") 37 | } 38 | if tracerValue == nil || tracerValue.Stopped() { 39 | return nil 40 | } 41 | return tracerValue 42 | } 43 | -------------------------------------------------------------------------------- /example/api_gateway/README.md: -------------------------------------------------------------------------------- 1 | GOOS=linux go build main.go 2 | -------------------------------------------------------------------------------- /example/api_gateway/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/aws/aws-lambda-go/events" 9 | "github.com/aws/aws-lambda-go/lambda" 10 | "github.com/epsagon/epsagon-go/epsagon" 11 | epsagonhttp "github.com/epsagon/epsagon-go/wrappers/net/http" 12 | ) 13 | 14 | // Response is an API gateway response type 15 | type Response events.APIGatewayProxyResponse 16 | 17 | func myHandler(request events.APIGatewayProxyRequest) (Response, error) { 18 | log.Println("In myHandler, received body: ", request.Body) 19 | client := http.Client{Transport: epsagonhttp.NewTracingTransport()} 20 | resp, err := client.Get("https://api.randomuser.me") 21 | var body string 22 | if err == nil { 23 | defer resp.Body.Close() 24 | bodyBytes, err := ioutil.ReadAll(resp.Body) 25 | if err == nil { 26 | body = string(bodyBytes) 27 | } 28 | } else { 29 | log.Printf("Error in getting response: %+v\n", err) 30 | } 31 | return Response{Body: "yes: random user: " + body, StatusCode: 200}, nil 32 | } 33 | 34 | func main() { 35 | log.Println("enter main") 36 | config := epsagon.NewTracerConfig("epsagon-test-go", "") 37 | lambda.Start(epsagon.WrapLambdaHandler(config, myHandler)) 38 | } 39 | -------------------------------------------------------------------------------- /example/api_gateway/makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | deploy: build 4 | sls deploy 5 | 6 | build: main 7 | 8 | main: main.go 9 | go build -o main main.go 10 | 11 | .PHONY: all build main 12 | -------------------------------------------------------------------------------- /example/api_gateway/serverless.yml: -------------------------------------------------------------------------------- 1 | service: example-go-app-2 2 | 3 | provider: 4 | name: aws 5 | runtime: go1.x 6 | region: eu-west-1 7 | environment: 8 | EPSAGON_TOKEN: ${env:EPSAGON_TOKEN} 9 | EPSAGON_COLLECTOR_URL: ${env:EPSAGON_COLLECTOR_URL} 10 | EPSAGON_METADATA: FALSE 11 | EPSAGON_DEBUG: TRUE 12 | 13 | functions: 14 | hello: 15 | handler: main 16 | events: 17 | - http: 18 | path: hello 19 | method: post 20 | -------------------------------------------------------------------------------- /example/concurrent_generic_go_functions/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "sync" 9 | "time" 10 | 11 | "github.com/epsagon/epsagon-go/epsagon" 12 | epsagonhttp "github.com/epsagon/epsagon-go/wrappers/net/http" 13 | ) 14 | 15 | func doTask(ctx context.Context, a int, b string, wg *sync.WaitGroup) (int, error) { 16 | defer wg.Done() 17 | log.Printf("inside doTask: b = %s", b) 18 | client := http.Client{Transport: epsagonhttp.NewTracingTransport(ctx)} 19 | client.Get("https://epsagon.com/") 20 | return a + 1, fmt.Errorf("boom") 21 | } 22 | 23 | func main() { 24 | config := epsagon.NewTracerConfig("generic-go-wrapper", "") 25 | config.Debug = true 26 | var wg sync.WaitGroup 27 | for i := 0; i < 5; i++ { 28 | go epsagon.ConcurrentGoWrapper(config, doTask)(i, "hello", &wg) 29 | } 30 | wg.Wait() 31 | time.Sleep(2 * time.Second) 32 | } 33 | -------------------------------------------------------------------------------- /example/concurrent_generic_go_functions/makefile: -------------------------------------------------------------------------------- 1 | build: main 2 | 3 | main: main.go 4 | go build -o main main.go 5 | 6 | .PHONY: all build main 7 | -------------------------------------------------------------------------------- /example/custom_error_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/epsagon/epsagon-go/epsagon" 8 | ) 9 | 10 | func doTask(a int, b string) (int, error) { 11 | log.Printf("inside doTask: b = %s", b) 12 | if a < 10 { 13 | // This error with its type will be viewable in Epsagon dashboard 14 | epsagon.TypeError("value must be bigger than 10", "InputError") 15 | } 16 | 17 | return a + 1, fmt.Errorf("boom") 18 | } 19 | 20 | func main() { 21 | // Normal call 22 | res, err := doTask(3, "world") 23 | if err != nil { 24 | log.Printf("First result is %d", res) 25 | } else { 26 | log.Printf("error was: %v", err) 27 | } 28 | 29 | // With Epsagon instrumentation 30 | config := epsagon.NewTracerConfig("generic-go-wrapper", "") 31 | config.Debug = true 32 | response := epsagon.GoWrapper(config, doTask)(5, "hello") 33 | res2 := response[0].Int() 34 | errInterface := response[1].Interface() 35 | if errInterface == nil { 36 | log.Printf("Result was %d", res2) 37 | } else { 38 | err := errInterface.(error) 39 | log.Printf("error was: %v", err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/custom_error_example/makefile: -------------------------------------------------------------------------------- 1 | build: main 2 | 3 | main: main.go 4 | go build -o main main.go 5 | 6 | .PHONY: all build main 7 | -------------------------------------------------------------------------------- /example/ddb_example/makefile: -------------------------------------------------------------------------------- 1 | deploy: build 2 | sls deploy 3 | 4 | build: trigger/main write/main write_sdk_v2/main 5 | 6 | local: trigger/local_main write/local_main 7 | 8 | trigger/main: trigger/main.go 9 | GOOS=linux go build -o trigger/main trigger/main.go 10 | 11 | write/main: write/main.go 12 | GOOS=linux go build -o write/main write/main.go 13 | 14 | write_sdk_v2/main: write_sdk_v2/main.go 15 | GOOS=linux go build -o write_sdk_v2/main write_sdk_v2/main.go 16 | 17 | trigger/local_main: trigger/main.go 18 | GOOS=darwin go build -o trigger/local_main trigger/main.go 19 | 20 | write/local_main: write/main.go 21 | GOOS=darwin go build -o write/local_main write/main.go 22 | 23 | .PHONY: all build trigger write 24 | -------------------------------------------------------------------------------- /example/ddb_example/serverless.yml: -------------------------------------------------------------------------------- 1 | service: example-go-app-ddb 2 | 3 | custom: 4 | TableName: golang-test-table 5 | 6 | provider: 7 | name: aws 8 | runtime: go1.x 9 | region: eu-west-1 10 | environment: 11 | EPSAGON_TOKEN: ${env:EPSAGON_TOKEN} 12 | EPSAGON_COLLECTOR_URL: ${env:EPSAGON_COLLECTOR_URL} 13 | 14 | iamRoleStatements: 15 | - Effect: "Allow" 16 | Action: 17 | - "*" 18 | Resource: "arn:aws:dynamodb:eu-west-1:*:table/golang-test-table" 19 | 20 | functions: 21 | write: 22 | handler: write/main 23 | events: 24 | - http: 25 | path: write 26 | method: post 27 | environment: 28 | TABLE_NAME: ${self:custom.TableName} 29 | write-v2: 30 | handler: write_sdk_v2/main 31 | events: 32 | - http: 33 | path: write_v2 34 | method: post 35 | environment: 36 | TABLE_NAME: ${self:custom.TableName} 37 | triggered: 38 | handler: trigger/main 39 | events: 40 | - stream: 41 | type: dynamodb 42 | batchSize: 20 43 | startingPosition: TRIM_HORIZON 44 | arn: 45 | Fn::GetAtt: 46 | - GoLangTestTable 47 | - StreamArn 48 | 49 | resources: 50 | Resources: 51 | GoLangTestTable: 52 | Type: "AWS::DynamoDB::Table" 53 | DeletionPolicy: Delete 54 | Properties: 55 | AttributeDefinitions: 56 | - 57 | AttributeName: item 58 | AttributeType: S 59 | KeySchema: 60 | - 61 | AttributeName: item 62 | KeyType: HASH 63 | ProvisionedThroughput: 64 | ReadCapacityUnits: 1 65 | WriteCapacityUnits: 1 66 | StreamSpecification: 67 | StreamViewType: NEW_IMAGE 68 | TableName: ${self:custom.TableName} 69 | -------------------------------------------------------------------------------- /example/ddb_example/trigger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/aws/aws-lambda-go/events" 5 | "github.com/aws/aws-lambda-go/lambda" 6 | "github.com/epsagon/epsagon-go/epsagon" 7 | "log" 8 | ) 9 | 10 | func ddbHandler(ddbEvent events.DynamoDBEvent) error { 11 | log.Println("In ddbHandler, received body: ", ddbEvent) 12 | return nil 13 | } 14 | 15 | func main() { 16 | log.Println("enter main") 17 | config := epsagon.NewTracerConfig("ddb-test-go", "") 18 | config.Debug = true 19 | lambda.Start(epsagon.WrapLambdaHandler(config, ddbHandler)) 20 | log.Println("exit main") 21 | } 22 | -------------------------------------------------------------------------------- /example/ddb_example/write/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aws/aws-lambda-go/events" 6 | "github.com/aws/aws-lambda-go/lambda" 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/dynamodb" 10 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 11 | "github.com/epsagon/epsagon-go/epsagon" 12 | "github.com/epsagon/epsagon-go/wrappers/aws/aws-sdk-go/aws" 13 | "github.com/google/uuid" 14 | "log" 15 | "os" 16 | ) 17 | 18 | // Item example 19 | type Item struct { 20 | Item string `json:"item"` 21 | } 22 | 23 | func ddbHandler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 24 | log.Println("In ddbHandler, received body: ", request.Body) 25 | 26 | session := epsagonawswrapper.WrapSession(session.Must(session.NewSession(&aws.Config{ 27 | Region: aws.String("eu-west-1")}, 28 | ))) 29 | 30 | svc := dynamodb.New(session) 31 | 32 | item := Item{ 33 | Item: uuid.New().String(), 34 | } 35 | 36 | av, marshalErr := dynamodbattribute.MarshalMap(item) 37 | 38 | if marshalErr != nil { 39 | marshErrMsg := fmt.Sprintf("Failed to marshal table: %v", marshalErr) 40 | log.Println(marshErrMsg) 41 | return events.APIGatewayProxyResponse{Body: marshErrMsg, StatusCode: 500}, marshalErr 42 | } 43 | 44 | input := &dynamodb.PutItemInput{ 45 | Item: av, 46 | TableName: aws.String(os.Getenv("TABLE_NAME")), 47 | } 48 | 49 | _, err := svc.PutItem(input) 50 | 51 | if err != nil { 52 | fmt.Println("Failed calling PutItem:") 53 | errMsg := fmt.Sprintf("Failed calling PutItem: %v", err.Error()) 54 | return events.APIGatewayProxyResponse{Body: errMsg, StatusCode: 500}, err 55 | } 56 | 57 | fmt.Println("Successfully written item to table") 58 | return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil 59 | } 60 | 61 | func main() { 62 | log.Println("enter main") 63 | config := epsagon.NewTracerConfig("ddb-test-go-v2", "") 64 | config.Debug = true 65 | lambda.Start(epsagon.WrapLambdaHandler(config, ddbHandler)) 66 | log.Println("exit main") 67 | } 68 | -------------------------------------------------------------------------------- /example/ddb_example/write_sdk_v2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/aws/aws-lambda-go/events" 7 | "github.com/aws/aws-lambda-go/lambda" 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/aws/external" 10 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 11 | "github.com/epsagon/epsagon-go/epsagon" 12 | "github.com/google/uuid" 13 | "log" 14 | "os" 15 | ) 16 | 17 | func ddbHandler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 18 | cfg, err := external.LoadDefaultAWSConfig() 19 | if err != nil { 20 | panic("Failed to load default aws config") 21 | } 22 | cfg.Region = "eu-west-1" 23 | svc := epsagon.WrapAwsV2Service(dynamodb.New(cfg)).(*dynamodb.Client) 24 | putItemInput := dynamodb.PutItemInput{ 25 | Item: map[string]dynamodb.AttributeValue{ 26 | "item": {S: aws.String(uuid.New().String())}, 27 | "request": {S: &request.Body}, 28 | }, 29 | TableName: aws.String(os.Getenv("TABLE_NAME")), 30 | } 31 | req := svc.PutItemRequest(&putItemInput) 32 | 33 | resp, err := req.Send(context.Background()) 34 | if err != nil { 35 | return events.APIGatewayProxyResponse{ 36 | Body: fmt.Sprintf("PutItem Failed: %s\n%s", 37 | resp.String(), err.Error()), 38 | StatusCode: 500, 39 | }, nil 40 | } 41 | 42 | log.Println("Successfully written item to table") 43 | return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil 44 | } 45 | 46 | func main() { 47 | log.Println("enter main") 48 | config := epsagon.NewTracerConfig("ddb-test-go-v2", "") 49 | config.Debug = true 50 | lambda.Start(epsagon.WrapLambdaHandler(config, ddbHandler)) 51 | log.Println("exit main") 52 | } 53 | -------------------------------------------------------------------------------- /example/exp_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/aws/aws-lambda-go/events" 5 | "github.com/aws/aws-lambda-go/lambda" 6 | "github.com/epsagon/epsagon-go/epsagon" 7 | "log" 8 | ) 9 | 10 | func expHandler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 11 | log.Println("In expHandler, received body: ", request.Body) 12 | zero := 0 13 | _ = 1 / zero 14 | return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil 15 | } 16 | 17 | func main() { 18 | log.Println("enter main") 19 | config := epsagon.NewTracerConfig("exp-test-go", "") 20 | lambda.Start(epsagon.WrapLambdaHandler(config, expHandler)) 21 | log.Println("exit main") 22 | } 23 | -------------------------------------------------------------------------------- /example/exp_example/serverless.yml: -------------------------------------------------------------------------------- 1 | service: example-go-app-exp 2 | 3 | provider: 4 | name: aws 5 | runtime: go1.x 6 | region: eu-west-1 7 | environment: 8 | EPSAGON_TOKEN: ${env:EPSAGON_TOKEN} 9 | EPSAGON_COLLECTOR_URL: ${env:EPSAGON_COLLECTOR_URL} 10 | 11 | functions: 12 | write: 13 | handler: main 14 | events: 15 | - http: 16 | path: exp 17 | method: post 18 | -------------------------------------------------------------------------------- /example/fiber_middleware/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/epsagon/epsagon-go/epsagon" 8 | epsagonfiber "github.com/epsagon/epsagon-go/wrappers/fiber" 9 | epsagonhttp "github.com/epsagon/epsagon-go/wrappers/net/http" 10 | "github.com/gofiber/fiber/v2" 11 | ) 12 | 13 | func main() { 14 | config := epsagon.NewTracerConfig( 15 | "fiber-example", "", 16 | ) 17 | config.MetadataOnly = false 18 | app := fiber.New() 19 | // Match all routes 20 | epsagonMiddleware := &epsagonfiber.FiberEpsagonMiddleware{ 21 | Config: config, 22 | } 23 | app.Use(epsagonMiddleware.HandlerFunc()) 24 | app.Post("/", func(c *fiber.Ctx) error { 25 | // any call wrapped by Epsagon should receive the user context from the fiber context 26 | client := http.Client{Transport: epsagonhttp.NewTracingTransport(c.UserContext())} 27 | client.Get(fmt.Sprintf("https://epsagon.com/")) 28 | return c.SendString("Hello, World 👋!") 29 | }) 30 | 31 | app.Listen("0.0.0.0:3000") 32 | } 33 | -------------------------------------------------------------------------------- /example/fiber_middleware/makefile: -------------------------------------------------------------------------------- 1 | build: main 2 | 3 | main: main.go 4 | go build -o main main.go 5 | 6 | .PHONY: all build main 7 | -------------------------------------------------------------------------------- /example/generic_go_function/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/epsagon/epsagon-go/epsagon" 8 | ) 9 | 10 | func doTask(a int, b string) (int, error) { 11 | log.Printf("inside doTask: b = %s", b) 12 | return a + 1, fmt.Errorf("boom") 13 | } 14 | 15 | func main() { 16 | // Normal call 17 | res, err := doTask(3, "world") 18 | if err != nil { 19 | log.Printf("First result is %d", res) 20 | } else { 21 | log.Printf("error was: %v", err) 22 | } 23 | 24 | // With Epsagon instrumentation 25 | config := epsagon.NewTracerConfig("generic-go-wrapper", "") 26 | config.Debug = true 27 | response := epsagon.GoWrapper(config, doTask)(5, "hello") 28 | res2 := response[0].Int() 29 | errInterface := response[1].Interface() 30 | if errInterface == nil { 31 | log.Printf("Result was %d", res2) 32 | } else { 33 | err := errInterface.(error) 34 | log.Printf("error was: %v", err) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /example/generic_go_function/makefile: -------------------------------------------------------------------------------- 1 | build: main 2 | 3 | main: main.go 4 | go build -o main main.go 5 | 6 | .PHONY: all build main 7 | -------------------------------------------------------------------------------- /example/gin_wrapper/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/epsagon/epsagon-go/epsagon" 10 | epsagongin "github.com/epsagon/epsagon-go/wrappers/gin" 11 | epsagonhttp "github.com/epsagon/epsagon-go/wrappers/net/http" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func main() { 16 | config := epsagon.NewTracerConfig( 17 | "gin-wrapper-test", "", 18 | ) 19 | config.MetadataOnly = false 20 | r := epsagongin.GinRouterWrapper{ 21 | IRouter: gin.Default(), 22 | Hostname: "my-site", 23 | Config: config, 24 | } 25 | 26 | r.GET("/ping", func(c *gin.Context) { 27 | time.Sleep(time.Second * 1) 28 | fmt.Println("hello world") 29 | c.JSON(200, gin.H{ 30 | "message": "pong", 31 | }) 32 | }) 33 | r.POST("/ping", func(c *gin.Context) { 34 | time.Sleep(time.Second * 1) 35 | body, err := ioutil.ReadAll(c.Request.Body) 36 | if err == nil { 37 | fmt.Println("Recieved body: ", string(body)) 38 | } else { 39 | fmt.Println("Error reading body: ", err) 40 | } 41 | 42 | client := http.Client{ 43 | Transport: epsagonhttp.NewTracingTransport(epsagongin.EpsagonContext(c))} 44 | resp, err := client.Get("http://www.example.com") 45 | 46 | if err == nil { 47 | respBody, err := ioutil.ReadAll(resp.Body) 48 | if err == nil { 49 | fmt.Println("First 1000 bytes recieved: ", string(respBody[:1000])) 50 | } 51 | } 52 | 53 | c.JSON(200, gin.H{ 54 | "message": "pong", 55 | }) 56 | }) 57 | 58 | /* listen and serve on 0.0.0.0: */ 59 | /* for windows - localhost: */ 60 | /* Port arg format: ":" "*/ 61 | err := r.Run(":3001") 62 | 63 | if err != nil { 64 | fmt.Println(err) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /example/gin_wrapper/makefile: -------------------------------------------------------------------------------- 1 | build: main 2 | 3 | main: main.go 4 | go build -o main main.go 5 | 6 | .PHONY: all build main 7 | -------------------------------------------------------------------------------- /example/http_mux/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net/http" 8 | 9 | "github.com/epsagon/epsagon-go/epsagon" 10 | epsagonhttp "github.com/epsagon/epsagon-go/wrappers/net/http" 11 | ) 12 | 13 | func main() { 14 | mux := http.NewServeMux() 15 | mux.HandleFunc("/ping", epsagonhttp.WrapHandleFunc( 16 | epsagon.NewTracerConfig("test-http-mux", ""), 17 | func(w http.ResponseWriter, req *http.Request) { 18 | 19 | client := http.Client{ 20 | Transport: epsagonhttp.NewTracingTransport(req.Context())} 21 | resp, err := client.Get("http://example.com") 22 | 23 | if err == nil { 24 | respBody, err := ioutil.ReadAll(resp.Body) 25 | if err == nil { 26 | fmt.Println("First 1000 bytes recieved: ", string(respBody[:1000])) 27 | } 28 | } 29 | io.WriteString(w, "pong\n") 30 | }, 31 | "my-test-function", 32 | "test.hostname.com", 33 | )) 34 | 35 | http.ListenAndServe(":8080", mux) 36 | } 37 | -------------------------------------------------------------------------------- /example/ignored_keys/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | "github.com/epsagon/epsagon-go/epsagon" 11 | epsagonhttp "github.com/epsagon/epsagon-go/wrappers/net/http" 12 | ) 13 | 14 | func doTask(ctx context.Context) { 15 | client := http.Client{Transport: epsagonhttp.NewTracingTransport(ctx)} 16 | // This password will be masked in the sent trace: 17 | decodedJSON, err := json.Marshal(map[string]string{"password": "abcde", "animal": "lion"}) 18 | if err != nil { 19 | epsagon.Label("animal", "lion", ctx) 20 | epsagon.TypeError(err, "json decoding error", ctx) 21 | } 22 | resp, err := client.Post("http://example.com/upload", "application/json", bytes.NewReader(decodedJSON)) 23 | if err != nil { 24 | epsagon.Label("animal", "lion", ctx) 25 | epsagon.TypeError(err, "post", ctx) 26 | } 27 | if resp.StatusCode != 200 { 28 | body, _ := ioutil.ReadAll(resp.Body) 29 | if err == nil { 30 | epsagon.TypeError(string(body), "post status code", ctx) 31 | } else { 32 | epsagon.TypeError(err, "post status code", ctx) 33 | } 34 | epsagon.Label("animal", "lion", ctx) 35 | } 36 | } 37 | 38 | func main() { 39 | // With Epsagon instrumentation 40 | config := epsagon.NewTracerConfig("test-ignored-keys", "") 41 | config.Debug = true 42 | config.MetadataOnly = false 43 | config.IgnoredKeys = []string{"password"} 44 | epsagon.ConcurrentGoWrapper(config, doTask)() 45 | } 46 | -------------------------------------------------------------------------------- /example/label_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/epsagon/epsagon-go/epsagon" 8 | ) 9 | 10 | func doTask(a int, b string) (int, error) { 11 | log.Printf("inside doTask: b = %s", b) 12 | 13 | // This label will be viewable in Epsagon dashboard 14 | epsagon.Label("my_key", 100.12) 15 | return a + 1, fmt.Errorf("boom") 16 | } 17 | 18 | func main() { 19 | // Normal call 20 | res, err := doTask(3, "world") 21 | if err != nil { 22 | log.Printf("First result is %d", res) 23 | } else { 24 | log.Printf("error was: %v", err) 25 | } 26 | 27 | // With Epsagon instrumentation 28 | config := epsagon.NewTracerConfig("generic-go-wrapper", "") 29 | config.Debug = true 30 | response := epsagon.GoWrapper(config, doTask)(5, "hello") 31 | res2 := response[0].Int() 32 | errInterface := response[1].Interface() 33 | if errInterface == nil { 34 | log.Printf("Result was %d", res2) 35 | } else { 36 | err := errInterface.(error) 37 | log.Printf("error was: %v", err) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/label_example/makefile: -------------------------------------------------------------------------------- 1 | build: main 2 | 3 | main: main.go 4 | go build -o main main.go 5 | 6 | .PHONY: all build main 7 | -------------------------------------------------------------------------------- /example/mongo_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/epsagon/epsagon-go/epsagon" 7 | epsagonmongo "github.com/epsagon/epsagon-go/wrappers/mongo" 8 | "time" 9 | 10 | "go.mongodb.org/mongo-driver/bson" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "go.mongodb.org/mongo-driver/mongo/options" 13 | "go.mongodb.org/mongo-driver/mongo/readpref" 14 | ) 15 | 16 | func dbAPI() { 17 | 18 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 19 | defer cancel() 20 | 21 | client, err := mongo.Connect( 22 | ctx, 23 | options.Client().ApplyURI("mongodb://localhost:27017"), 24 | ) 25 | 26 | defer func() { 27 | if err = client.Disconnect(ctx); err != nil { 28 | panic(err) 29 | } 30 | }() 31 | 32 | err = client.Ping(ctx, readpref.Primary()) 33 | 34 | db := client.Database("DB") 35 | 36 | // WRAP THE MONGO COLLECTION with WrapMongoCollection() 37 | coll := epsagonmongo.WrapMongoCollection( 38 | db.Collection("COLL"), 39 | ) 40 | 41 | type doc struct { 42 | Name string 43 | } 44 | var res interface{} 45 | 46 | fmt.Println("##InsertOne") 47 | _, err = coll.InsertOne( 48 | context.Background(), 49 | doc{Name: "bon"}, 50 | ) 51 | if err != nil { 52 | panic(err) 53 | } 54 | 55 | fmt.Println("##InsertMany") 56 | _, err = coll.InsertMany( 57 | context.Background(), 58 | []interface{}{ 59 | bson.D{ 60 | {Key: "name", Value: "hello"}, 61 | {Key: "age", Value: "33"}, 62 | }, 63 | bson.D{ 64 | {Key: "name", Value: "world"}, 65 | {Key: "age", Value: "44"}, 66 | }, 67 | }, 68 | ) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | fmt.Println("##FindOne") 74 | coll.FindOne( 75 | context.Background(), 76 | bson.D{{Key: "name", Value: "bon"}}, 77 | ) 78 | 79 | fmt.Println("##Find") 80 | coll.Find(context.Background(), bson.M{}) 81 | 82 | fmt.Println("##Aggregate") 83 | res, err = coll.Aggregate( 84 | context.Background(), 85 | mongo.Pipeline{ 86 | bson.D{{Key: "$match", Value: bson.D{{Key: "name", Value: "bon"}}}}, 87 | }, 88 | ) 89 | if err != nil || err == mongo.ErrNoDocuments { 90 | panic(err) 91 | } 92 | 93 | fmt.Println("##CountDocuments") 94 | res, err = coll.CountDocuments( 95 | context.Background(), 96 | bson.D{{Key: "name", Value: "bon"}}, 97 | ) 98 | fmt.Println(res) 99 | if err != nil || err == mongo.ErrNoDocuments { 100 | panic(err) 101 | } 102 | 103 | fmt.Println("##DeleteOne") 104 | res, err = coll.DeleteOne( 105 | context.Background(), 106 | bson.D{{Key: "name", Value: "bon"}}, 107 | ) 108 | fmt.Println(res) 109 | if err != nil || err == mongo.ErrNoDocuments { 110 | panic(err) 111 | } 112 | 113 | fmt.Println("##UpdateOne") 114 | res, err = coll.UpdateOne( 115 | context.Background(), 116 | bson.D{{Key: "name", Value: "bon"}}, 117 | bson.D{{Key: "$set", Value: bson.D{{Key: "name", Value: "son"}}}}, 118 | ) 119 | fmt.Println(res) 120 | if err != nil || err == mongo.ErrNoDocuments { 121 | panic(err) 122 | } 123 | } 124 | 125 | func main() { 126 | 127 | config := epsagon.NewTracerConfig( 128 | "skate-shop", 129 | "token", 130 | ) 131 | config.MetadataOnly = false 132 | config.Debug = true 133 | 134 | epsagon.GoWrapper( 135 | config, 136 | dbAPI, 137 | )() 138 | 139 | } 140 | -------------------------------------------------------------------------------- /example/mux_error/main.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import ( 5 | "github.com/epsagon/epsagon-go/epsagon" 6 | "github.com/epsagon/epsagon-go/wrappers/net/http" 7 | "net/http" 8 | ) 9 | 10 | 11 | func SetEpsagonConfig() *epsagon.Config { 12 | appName := "simple-error-go" 13 | token := "" 14 | config := epsagon.NewTracerConfig(appName, token) 15 | config.Debug = true 16 | config.MetadataOnly = false 17 | config.SendTimeout = "10s" 18 | 19 | return config 20 | } 21 | 22 | 23 | func handler(res http.ResponseWriter, req *http.Request) { 24 | println("/test pinged") 25 | epsagon.Error("Unknown timezone", req.Context()) 26 | res.Write([]byte("Pong.\n")) 27 | } 28 | 29 | func main() { 30 | config := SetEpsagonConfig() 31 | serveMux := http.NewServeMux() 32 | serveMux.HandleFunc( 33 | "/test", 34 | epsagonhttp.WrapHandleFunc(config, handler)) 35 | server := http.Server{ 36 | Addr: "localhost:8082", 37 | Handler: serveMux, 38 | } 39 | err := server.ListenAndServe() 40 | if err != nil { 41 | panic(err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /example/redis_wrapper/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/epsagon/epsagon-go/epsagon" 9 | epsagonhttp "github.com/epsagon/epsagon-go/wrappers/net/http" 10 | epsagonredis "github.com/epsagon/epsagon-go/wrappers/redis" 11 | "github.com/go-redis/redis/v8" 12 | ) 13 | 14 | func main() { 15 | config := epsagon.NewTracerConfig("redis-wrapper-test", "") 16 | config.MetadataOnly = false 17 | 18 | mux := http.NewServeMux() 19 | mux.HandleFunc("/ping", epsagonhttp.WrapHandleFunc( 20 | config, 21 | func(w http.ResponseWriter, req *http.Request) { 22 | // initialize the redis client as usual 23 | // make sure to pass in the epsagon tracer context 24 | rdb := epsagonredis.NewClient(&redis.Options{ 25 | Addr: "localhost:6379", 26 | Password: "", 27 | DB: 0, 28 | }, req.Context()) 29 | 30 | ctx := context.Background() 31 | 32 | // pipeline operations 33 | pipe := rdb.Pipeline() 34 | pipe.Set(ctx, "somekey", "somevalue", 0) 35 | pipe.Get(ctx, "somekey") 36 | pipe.Exec(ctx) 37 | 38 | // single operation 39 | value, _ := rdb.Get(ctx, "somekey").Result() 40 | 41 | io.WriteString(w, value) 42 | }), 43 | ) 44 | 45 | http.ListenAndServe(":8080", mux) 46 | } 47 | -------------------------------------------------------------------------------- /example/s3_example/README.md: -------------------------------------------------------------------------------- 1 | GOOS=linux go build main.go 2 | -------------------------------------------------------------------------------- /example/s3_example/serverless.yml: -------------------------------------------------------------------------------- 1 | service: s3-example-go-app 2 | 3 | custom: 4 | BucketName: s3-golang-test-bucket-epsagon 5 | 6 | provider: 7 | name: aws 8 | runtime: go1.x 9 | region: eu-west-1 10 | environment: 11 | EPSAGON_TOKEN: ${env:EPSAGON_TOKEN} 12 | EPSAGON_COLLECTOR_URL: ${env:EPSAGON_COLLECTOR_URL} 13 | BUCKET_NAME: ${self:custom.BucketName} 14 | 15 | iamRoleStatements: 16 | - Effect: Allow 17 | Action: 18 | - s3:* 19 | Resource: "arn:aws:s3::*:${self:custom.BucketName}/*" 20 | - Effect: Allow 21 | Action: 22 | - s3:* 23 | Resource: "arn:aws:s3::*:${self:custom.BucketName}" 24 | 25 | functions: 26 | write: 27 | handler: write/main 28 | events: 29 | - http: 30 | path: write 31 | method: post 32 | write_v2: 33 | handler: write_v2/main 34 | events: 35 | - http: 36 | path: write_v2 37 | method: post 38 | trigger: 39 | handler: trigger/main 40 | events: 41 | - s3: 42 | bucket: ${self:custom.BucketName} 43 | event: s3:ObjectCreated:* 44 | -------------------------------------------------------------------------------- /example/s3_example/trigger/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aws/aws-lambda-go/events" 6 | "github.com/aws/aws-lambda-go/lambda" 7 | "github.com/epsagon/epsagon-go/epsagon" 8 | "log" 9 | ) 10 | 11 | func s3Handler(s3Event events.S3Event) { 12 | for _, record := range s3Event.Records { 13 | s3 := record.S3 14 | fmt.Printf("[%s - %s] Bucket = %s, Key = %s \n", 15 | record.EventSource, record.EventTime, s3.Bucket.Name, s3.Object.Key) 16 | } 17 | } 18 | 19 | func main() { 20 | log.Println("enter main") 21 | config := epsagon.NewTracerConfig("s3-test-go", "") 22 | config.Debug = true 23 | lambda.Start(epsagon.WrapLambdaHandler(config, s3Handler)) 24 | log.Println("exit main") 25 | } 26 | -------------------------------------------------------------------------------- /example/s3_example/write/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/aws/aws-lambda-go/events" 7 | "github.com/aws/aws-lambda-go/lambda" 8 | "github.com/aws/aws-sdk-go/aws" 9 | "github.com/aws/aws-sdk-go/aws/session" 10 | "github.com/aws/aws-sdk-go/service/s3/s3manager" 11 | "github.com/epsagon/epsagon-go/epsagon" 12 | "github.com/epsagon/epsagon-go/wrappers/aws/aws-sdk-go/aws" 13 | "github.com/google/uuid" 14 | "log" 15 | "os" 16 | ) 17 | 18 | func s3WriteHandler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 19 | log.Println("In s3WriteHandler, received body: ", request.Body) 20 | 21 | session := epsagonawswrapper.WrapSession(session.Must(session.NewSession(&aws.Config{ 22 | Region: aws.String("eu-west-1")}, 23 | ))) 24 | 25 | uploader := s3manager.NewUploader(session) 26 | 27 | byteData := []byte("Hello World") 28 | myBucket := os.Getenv("BUCKET_NAME") 29 | key := uuid.New().String() 30 | 31 | if len(myBucket) == 0 { 32 | errMsg := "ERROR: You need to assign BUCKET_NAME env" 33 | log.Println(errMsg) 34 | return events.APIGatewayProxyResponse{Body: errMsg, StatusCode: 500}, nil 35 | } 36 | 37 | result, err := uploader.Upload(&s3manager.UploadInput{ 38 | Bucket: aws.String(myBucket), 39 | Key: aws.String(key), 40 | Body: bytes.NewReader(byteData), 41 | }) 42 | 43 | if err != nil { 44 | errMsg := fmt.Sprintf("Failed calling Upload: %v", err.Error()) 45 | log.Println(errMsg) 46 | return events.APIGatewayProxyResponse{Body: errMsg, StatusCode: 500}, err 47 | } 48 | 49 | log.Printf("Succesfully uploaded file to %s\n", result.Location) 50 | return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil 51 | } 52 | 53 | func main() { 54 | log.Println("enter main") 55 | config := epsagon.NewTracerConfig("s3-test-go", "") 56 | config.Debug = true 57 | lambda.Start(epsagon.WrapLambdaHandler(config, s3WriteHandler)) 58 | log.Println("exit main") 59 | } 60 | -------------------------------------------------------------------------------- /example/s3_example/write_v2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "strings" 10 | "time" 11 | 12 | "github.com/aws/aws-lambda-go/events" 13 | "github.com/aws/aws-lambda-go/lambda" 14 | "github.com/aws/aws-sdk-go-v2/aws" 15 | "github.com/aws/aws-sdk-go-v2/aws/external" 16 | "github.com/aws/aws-sdk-go-v2/service/s3" 17 | "github.com/epsagon/epsagon-go/epsagon" 18 | "github.com/google/uuid" 19 | ) 20 | 21 | func s3WriteHandler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 22 | log.Println("In s3WriteHandler, received body: ", request.Body) 23 | 24 | cfg, err := external.LoadDefaultAWSConfig() 25 | if err != nil { 26 | panic("Failed to load default aws config") 27 | } 28 | cfg.Region = "eu-west-1" 29 | svc := epsagon.WrapAwsV2Service(s3.New(cfg)).(*s3.Client) 30 | 31 | // Create a context with a timeout that will abort the upload if it takes 32 | // more than the passed in timeout. 33 | ctx := context.Background() 34 | var cancelFn func() 35 | timeout := time.Minute * 2 36 | if timeout > 0 { 37 | ctx, cancelFn = context.WithTimeout(ctx, timeout) 38 | } 39 | // Ensure the context is canceled to prevent leaking. 40 | // See context package for more information, https://golang.org/pkg/context/ 41 | defer cancelFn() 42 | 43 | data := "Hello World" 44 | myBucket := os.Getenv("BUCKET_NAME") 45 | key := uuid.New().String() 46 | 47 | if len(myBucket) == 0 { 48 | errMsg := "ERROR: You need to assign BUCKET_NAME env" 49 | log.Println(errMsg) 50 | return events.APIGatewayProxyResponse{Body: errMsg, StatusCode: 500}, nil 51 | } 52 | 53 | req := svc.PutObjectRequest(&s3.PutObjectInput{ 54 | Bucket: &myBucket, 55 | Key: &key, 56 | Body: strings.NewReader(data), 57 | }) 58 | 59 | if _, err := req.Send(ctx); err != nil { 60 | var cerr *aws.RequestCanceledError 61 | var errMsg string 62 | if errors.As(err, &cerr) { 63 | errMsg = fmt.Sprintf("upload caceled due to timeout, %v", err) 64 | } else { 65 | errMsg = fmt.Sprintf("Failed to upload object: %v", err) 66 | } 67 | log.Println(errMsg) 68 | return events.APIGatewayProxyResponse{Body: errMsg, StatusCode: 500}, err 69 | } 70 | 71 | log.Printf("Succesfully uploaded file.") 72 | return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil 73 | } 74 | 75 | func main() { 76 | log.Println("enter main") 77 | config := epsagon.NewTracerConfig("s3-test-go", "") 78 | config.Debug = true 79 | lambda.Start(epsagon.WrapLambdaHandler(config, s3WriteHandler)) 80 | log.Println("exit main") 81 | } 82 | -------------------------------------------------------------------------------- /example/simple_error/README.md: -------------------------------------------------------------------------------- 1 | GOOS=linux go build main.go 2 | -------------------------------------------------------------------------------- /example/simple_error/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aws/aws-lambda-go/events" 6 | "github.com/aws/aws-lambda-go/lambda" 7 | "github.com/epsagon/epsagon-go/epsagon" 8 | "log" 9 | ) 10 | 11 | // Response is an API gateway response type 12 | type Response events.APIGatewayProxyResponse 13 | 14 | func myHandler(request events.APIGatewayProxyRequest) (Response, error) { 15 | log.Println("In myHandler, received body: ", request.Body) 16 | return Response{StatusCode: 404, Body: "error"}, fmt.Errorf("example error") 17 | } 18 | 19 | func main() { 20 | log.Println("enter main") 21 | config := epsagon.NewTracerConfig("simple-error-go", "") 22 | lambda.Start(epsagon.WrapLambdaHandler(config, myHandler)) 23 | } 24 | -------------------------------------------------------------------------------- /example/simple_error/makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | deploy: build 4 | sls deploy 5 | 6 | build: main 7 | 8 | main: main.go 9 | go build -o main main.go 10 | 11 | .PHONY: all build main 12 | -------------------------------------------------------------------------------- /example/simple_error/serverless.yml: -------------------------------------------------------------------------------- 1 | service: example-go-simple-error 2 | 3 | provider: 4 | name: aws 5 | runtime: go1.x 6 | region: eu-west-1 7 | environment: 8 | EPSAGON_TOKEN: ${env:EPSAGON_TOKEN} 9 | EPSAGON_COLLECTOR_URL: ${env:EPSAGON_COLLECTOR_URL} 10 | EPSAGON_DEBUG: "TRUE" 11 | 12 | functions: 13 | simple_error: 14 | handler: main 15 | events: 16 | - http: 17 | path: hello 18 | method: post 19 | -------------------------------------------------------------------------------- /example/simple_lambda/README.md: -------------------------------------------------------------------------------- 1 | GOOS=linux go build main.go 2 | -------------------------------------------------------------------------------- /example/simple_lambda/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/aws/aws-lambda-go/events" 5 | "github.com/aws/aws-lambda-go/lambda" 6 | "github.com/epsagon/epsagon-go/epsagon" 7 | "log" 8 | ) 9 | 10 | // Response is an API gateway response type 11 | type Response events.APIGatewayProxyResponse 12 | 13 | func myHandler(request events.APIGatewayProxyRequest) (Response, error) { 14 | log.Println("In myHandler, received body: ", request.Body) 15 | return Response{Body: "yes", StatusCode: 200}, nil 16 | } 17 | 18 | func main() { 19 | log.Println("enter main") 20 | config := epsagon.NewTracerConfig("epsagon-test-go", "") 21 | lambda.Start(epsagon.WrapLambdaHandler(config, myHandler)) 22 | } 23 | -------------------------------------------------------------------------------- /example/simple_lambda/makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | deploy: build 4 | sls deploy 5 | 6 | build: main 7 | 8 | main: main.go 9 | go build -o main main.go 10 | 11 | .PHONY: all build main 12 | -------------------------------------------------------------------------------- /example/simple_lambda/serverless.yml: -------------------------------------------------------------------------------- 1 | service: example-go-app 2 | 3 | provider: 4 | name: aws 5 | runtime: go1.x 6 | region: eu-west-1 7 | environment: 8 | EPSAGON_TOKEN: ${env:EPSAGON_TOKEN} 9 | EPSAGON_COLLECTOR_URL: ${env:EPSAGON_COLLECTOR_URL} 10 | 11 | functions: 12 | hello: 13 | handler: main 14 | events: 15 | - http: 16 | path: hello 17 | method: post 18 | -------------------------------------------------------------------------------- /example/sqs_trigger/hello/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/aws/aws-lambda-go/events" 9 | "github.com/aws/aws-lambda-go/lambda" 10 | "github.com/aws/aws-sdk-go/aws/session" 11 | "github.com/aws/aws-sdk-go/service/sqs" 12 | 13 | "github.com/epsagon/epsagon-go/epsagon" 14 | "github.com/epsagon/epsagon-go/wrappers/aws/aws-sdk-go/aws" 15 | ) 16 | 17 | func myHandler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 18 | log.Println("In myHandler, received body: ", request.Body) 19 | 20 | // sess := session.Must(session.NewSession()) 21 | // epsagon wrapper for aws-sdk-go 22 | sess := epsagonawswrapper.WrapSession(session.Must(session.NewSession())) 23 | 24 | svcSQS := sqs.New(sess) 25 | 26 | sqsQueueName := os.Getenv("SQS_NAME") 27 | queueURL, err := svcSQS.GetQueueUrl(&sqs.GetQueueUrlInput{QueueName: &sqsQueueName}) 28 | if err != nil { 29 | errMsg := fmt.Sprintf("Failed to get SQS Queue Url: %v", err) 30 | log.Println(errMsg) 31 | return events.APIGatewayProxyResponse{Body: errMsg, StatusCode: 500}, err 32 | } 33 | _, err = svcSQS.SendMessage(&sqs.SendMessageInput{ 34 | MessageBody: &request.Body, 35 | QueueUrl: queueURL.QueueUrl, 36 | }) 37 | if err != nil { 38 | errMsg := fmt.Sprintf("Failed to send SQS message: %v", err) 39 | log.Println(errMsg) 40 | return events.APIGatewayProxyResponse{Body: errMsg, StatusCode: 500}, err 41 | } 42 | return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil 43 | } 44 | 45 | func main() { 46 | log.Println("enter main") 47 | config := epsagon.NewTracerConfig("sqs-test-go", "") 48 | config.Debug = true 49 | lambda.Start(epsagon.WrapLambdaHandler(config, myHandler)) 50 | } 51 | -------------------------------------------------------------------------------- /example/sqs_trigger/makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | deploy: build 4 | sls deploy 5 | 6 | build: triggered hello 7 | 8 | triggered: triggered/main.go 9 | go build -o triggered/main triggered/main.go 10 | 11 | hello: hello/main.go 12 | go build -o hello/main hello/main.go 13 | 14 | .PHONY: all build triggered hello 15 | -------------------------------------------------------------------------------- /example/sqs_trigger/serverless.yml: -------------------------------------------------------------------------------- 1 | service: example-go-app-sqs 2 | 3 | custom: 4 | QueueName: GoExampleTestQueue 5 | 6 | provider: 7 | name: aws 8 | runtime: go1.x 9 | region: eu-west-1 10 | environment: 11 | EPSAGON_TOKEN: ${env:EPSAGON_TOKEN} 12 | EPSAGON_COLLECTOR_URL: ${env:EPSAGON_COLLECTOR_URL} 13 | 14 | queueArn: 15 | Fn::Join: 16 | - ":" 17 | - - "arn:aws:sqs" 18 | - ${self:provider.region} 19 | - Ref: AWS::AccountId 20 | - ${self:custom.QueueName} 21 | 22 | iamRoleStatements: 23 | - Effect: "Allow" 24 | Action: 25 | - "sqs:ListQueues" 26 | Resource: 27 | Fn::Join: 28 | - ":" 29 | - - "arn:aws:sqs" 30 | - ${self:provider.region} 31 | - Ref: AWS::AccountId 32 | - "*" 33 | - Effect: "Allow" 34 | Action: 35 | - "sqs:*" 36 | Resource: ${self:provider.queueArn} 37 | 38 | functions: 39 | hello: 40 | handler: hello/main 41 | events: 42 | - http: 43 | path: hello 44 | method: post 45 | environment: 46 | SQS_NAME: ${self:custom.QueueName} 47 | triggered: 48 | handler: triggered/main 49 | events: 50 | - sqs: 51 | arn: 52 | Fn::GetAtt: 53 | - ExampleTestQueue 54 | - Arn 55 | batchSize: 1 56 | 57 | resources: 58 | Resources: 59 | ExampleTestQueue: 60 | Type: AWS::SQS::Queue 61 | Properties: 62 | QueueName: ${self:custom.QueueName} 63 | -------------------------------------------------------------------------------- /example/sqs_trigger/triggered/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/aws/aws-lambda-go/events" 5 | "github.com/aws/aws-lambda-go/lambda" 6 | "github.com/epsagon/epsagon-go/epsagon" 7 | "log" 8 | ) 9 | 10 | func mySQSHandler(sqsEvents events.SQSEvent) error { 11 | log.Println("In mySQSHandler, received body: ", sqsEvents) 12 | return nil 13 | } 14 | 15 | func main() { 16 | log.Println("enter main") 17 | config := epsagon.NewTracerConfig("sqs-test-go", "") 18 | lambda.Start(epsagon.WrapLambdaHandler(config, mySQSHandler)) 19 | } 20 | -------------------------------------------------------------------------------- /example/sts_example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/aws/aws-lambda-go/events" 7 | "github.com/aws/aws-lambda-go/lambda" 8 | "github.com/aws/aws-sdk-go-v2/aws/external" 9 | "github.com/aws/aws-sdk-go-v2/service/sts" 10 | "github.com/epsagon/epsagon-go/epsagon" 11 | "log" 12 | ) 13 | 14 | func ddbHandler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 15 | cfg, err := external.LoadDefaultAWSConfig() 16 | if err != nil { 17 | panic("Failed to load default aws config") 18 | } 19 | cfg.Region = "eu-west-1" 20 | svc := epsagon.WrapAwsV2Service(sts.New(cfg)).(*sts.Client) 21 | req := svc.GetCallerIdentityRequest(&sts.GetCallerIdentityInput{}) 22 | 23 | resp, err := req.Send(context.Background()) 24 | if err != nil { 25 | return events.APIGatewayProxyResponse{ 26 | Body: fmt.Sprintf("GetCallerIdentityRequest Failed: %s\n%s", 27 | resp.String(), err.Error()), 28 | StatusCode: 500, 29 | }, nil 30 | } 31 | 32 | log.Println("Successfully got caller identity request") 33 | return events.APIGatewayProxyResponse{Body: "", StatusCode: 200}, nil 34 | } 35 | 36 | func main() { 37 | log.Println("enter main") 38 | config := epsagon.NewTracerConfig("ddb-test-go-v2", "") 39 | config.Debug = true 40 | lambda.Start(epsagon.WrapLambdaHandler(config, ddbHandler)) 41 | log.Println("exit main") 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/epsagon/epsagon-go 2 | 3 | go 1.11 4 | 5 | require ( 6 | github.com/alicebob/miniredis/v2 v2.15.1 7 | github.com/aws/aws-lambda-go v1.17.0 8 | github.com/aws/aws-sdk-go v1.36.30 9 | github.com/aws/aws-sdk-go-v2 v0.23.0 10 | github.com/benweissmann/memongo v0.1.1 11 | github.com/gin-gonic/gin v1.7.0 12 | github.com/go-redis/redis/v8 v8.11.0 13 | github.com/gofiber/fiber/v2 v2.11.0 14 | github.com/gogo/protobuf v1.3.1 15 | github.com/golang/protobuf v1.4.3 16 | github.com/google/uuid v1.1.1 17 | github.com/json-iterator/go v1.1.10 // indirect 18 | github.com/kr/text v0.2.0 // indirect 19 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 20 | github.com/modern-go/reflect2 v1.0.1 // indirect 21 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 22 | github.com/onsi/ginkgo v1.16.1 23 | github.com/onsi/gomega v1.11.0 24 | github.com/stretchr/testify v1.7.0 // indirect 25 | github.com/ugorji/go v1.1.13 // indirect 26 | github.com/valyala/fasthttp v1.26.0 27 | go.mongodb.org/mongo-driver v1.5.2 28 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 29 | google.golang.org/protobuf v1.25.0 // indirect 30 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 31 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 32 | 33 | ) 34 | -------------------------------------------------------------------------------- /internal/fake_collector.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/epsagon/epsagon-go/protocol" 6 | "net" 7 | ) 8 | 9 | // FakeCollector implements a fake trace collector that will 10 | // listen on an endpoint untill a trace is received and then will 11 | // return that parsed trace 12 | type FakeCollector struct { 13 | Endpoint string 14 | } 15 | 16 | // Listen on the endpoint for one trace and push it to outChannel 17 | func (fc *FakeCollector) Listen(outChannel chan *protocol.Trace) { 18 | ln, err := net.Listen("tcp", fc.Endpoint) 19 | if err != nil { 20 | outChannel <- nil 21 | return 22 | } 23 | defer ln.Close() 24 | conn, err := ln.Accept() 25 | if err != nil { 26 | outChannel <- nil 27 | return 28 | } 29 | defer conn.Close() 30 | var buf = make([]byte, 0) 31 | _, err = conn.Read(buf) 32 | if err != nil { 33 | outChannel <- nil 34 | return 35 | } 36 | var receivedTrace protocol.Trace 37 | err = json.Unmarshal(buf, &receivedTrace) 38 | if err != nil { 39 | outChannel <- nil 40 | return 41 | } 42 | outChannel <- &receivedTrace 43 | } 44 | -------------------------------------------------------------------------------- /protocol/error_code.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: error_code.proto 3 | 4 | package protocol 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 22 | 23 | type ErrorCode int32 24 | 25 | const ( 26 | ErrorCode_OK ErrorCode = 0 27 | ErrorCode_ERROR ErrorCode = 1 28 | ErrorCode_EXCEPTION ErrorCode = 2 29 | ) 30 | 31 | var ErrorCode_name = map[int32]string{ 32 | 0: "OK", 33 | 1: "ERROR", 34 | 2: "EXCEPTION", 35 | } 36 | 37 | var ErrorCode_value = map[string]int32{ 38 | "OK": 0, 39 | "ERROR": 1, 40 | "EXCEPTION": 2, 41 | } 42 | 43 | func (x ErrorCode) String() string { 44 | return proto.EnumName(ErrorCode_name, int32(x)) 45 | } 46 | 47 | func (ErrorCode) EnumDescriptor() ([]byte, []int) { 48 | return fileDescriptor_c5513ac0a8e17e40, []int{0} 49 | } 50 | 51 | func init() { 52 | proto.RegisterEnum("protocol.ErrorCode", ErrorCode_name, ErrorCode_value) 53 | } 54 | 55 | func init() { proto.RegisterFile("error_code.proto", fileDescriptor_c5513ac0a8e17e40) } 56 | 57 | var fileDescriptor_c5513ac0a8e17e40 = []byte{ 58 | // 99 bytes of a gzipped FileDescriptorProto 59 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x48, 0x2d, 0x2a, 0xca, 60 | 0x2f, 0x8a, 0x4f, 0xce, 0x4f, 0x49, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x00, 0x53, 61 | 0xc9, 0xf9, 0x39, 0x5a, 0xba, 0x5c, 0x9c, 0xae, 0x20, 0x59, 0xe7, 0xfc, 0x94, 0x54, 0x21, 0x36, 62 | 0x2e, 0x26, 0x7f, 0x6f, 0x01, 0x06, 0x21, 0x4e, 0x2e, 0x56, 0xd7, 0xa0, 0x20, 0xff, 0x20, 0x01, 63 | 0x46, 0x21, 0x5e, 0x2e, 0x4e, 0xd7, 0x08, 0x67, 0xd7, 0x80, 0x10, 0x4f, 0x7f, 0x3f, 0x01, 0xa6, 64 | 0x24, 0x36, 0xb0, 0x46, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xec, 0xf2, 0x6c, 0x0e, 0x53, 65 | 0x00, 0x00, 0x00, 66 | } 67 | -------------------------------------------------------------------------------- /protocol/event.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: event.proto 3 | 4 | package protocol 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 22 | 23 | type Resource struct { 24 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 25 | Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` 26 | Operation string `protobuf:"bytes,3,opt,name=operation,proto3" json:"operation,omitempty"` 27 | Metadata map[string]string `protobuf:"bytes,4,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` 28 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 29 | XXX_unrecognized []byte `json:"-"` 30 | XXX_sizecache int32 `json:"-"` 31 | } 32 | 33 | func (m *Resource) Reset() { *m = Resource{} } 34 | func (m *Resource) String() string { return proto.CompactTextString(m) } 35 | func (*Resource) ProtoMessage() {} 36 | func (*Resource) Descriptor() ([]byte, []int) { 37 | return fileDescriptor_2d17a9d3f0ddf27e, []int{0} 38 | } 39 | 40 | func (m *Resource) XXX_Unmarshal(b []byte) error { 41 | return xxx_messageInfo_Resource.Unmarshal(m, b) 42 | } 43 | func (m *Resource) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 44 | return xxx_messageInfo_Resource.Marshal(b, m, deterministic) 45 | } 46 | func (m *Resource) XXX_Merge(src proto.Message) { 47 | xxx_messageInfo_Resource.Merge(m, src) 48 | } 49 | func (m *Resource) XXX_Size() int { 50 | return xxx_messageInfo_Resource.Size(m) 51 | } 52 | func (m *Resource) XXX_DiscardUnknown() { 53 | xxx_messageInfo_Resource.DiscardUnknown(m) 54 | } 55 | 56 | var xxx_messageInfo_Resource proto.InternalMessageInfo 57 | 58 | func (m *Resource) GetName() string { 59 | if m != nil { 60 | return m.Name 61 | } 62 | return "" 63 | } 64 | 65 | func (m *Resource) GetType() string { 66 | if m != nil { 67 | return m.Type 68 | } 69 | return "" 70 | } 71 | 72 | func (m *Resource) GetOperation() string { 73 | if m != nil { 74 | return m.Operation 75 | } 76 | return "" 77 | } 78 | 79 | func (m *Resource) GetMetadata() map[string]string { 80 | if m != nil { 81 | return m.Metadata 82 | } 83 | return nil 84 | } 85 | 86 | type Event struct { 87 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 88 | StartTime float64 `protobuf:"fixed64,2,opt,name=start_time,json=startTime,proto3" json:"start_time,omitempty"` 89 | Resource *Resource `protobuf:"bytes,3,opt,name=resource,proto3" json:"resource,omitempty"` 90 | Origin string `protobuf:"bytes,4,opt,name=origin,proto3" json:"origin,omitempty"` 91 | Duration float64 `protobuf:"fixed64,5,opt,name=duration,proto3" json:"duration,omitempty"` 92 | ErrorCode ErrorCode `protobuf:"varint,6,opt,name=error_code,json=errorCode,proto3,enum=protocol.ErrorCode" json:"error_code,omitempty"` 93 | Exception *Exception `protobuf:"bytes,7,opt,name=exception,proto3" json:"exception,omitempty"` 94 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 95 | XXX_unrecognized []byte `json:"-"` 96 | XXX_sizecache int32 `json:"-"` 97 | } 98 | 99 | func (m *Event) Reset() { *m = Event{} } 100 | func (m *Event) String() string { return proto.CompactTextString(m) } 101 | func (*Event) ProtoMessage() {} 102 | func (*Event) Descriptor() ([]byte, []int) { 103 | return fileDescriptor_2d17a9d3f0ddf27e, []int{1} 104 | } 105 | 106 | func (m *Event) XXX_Unmarshal(b []byte) error { 107 | return xxx_messageInfo_Event.Unmarshal(m, b) 108 | } 109 | func (m *Event) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 110 | return xxx_messageInfo_Event.Marshal(b, m, deterministic) 111 | } 112 | func (m *Event) XXX_Merge(src proto.Message) { 113 | xxx_messageInfo_Event.Merge(m, src) 114 | } 115 | func (m *Event) XXX_Size() int { 116 | return xxx_messageInfo_Event.Size(m) 117 | } 118 | func (m *Event) XXX_DiscardUnknown() { 119 | xxx_messageInfo_Event.DiscardUnknown(m) 120 | } 121 | 122 | var xxx_messageInfo_Event proto.InternalMessageInfo 123 | 124 | func (m *Event) GetId() string { 125 | if m != nil { 126 | return m.Id 127 | } 128 | return "" 129 | } 130 | 131 | func (m *Event) GetStartTime() float64 { 132 | if m != nil { 133 | return m.StartTime 134 | } 135 | return 0 136 | } 137 | 138 | func (m *Event) GetResource() *Resource { 139 | if m != nil { 140 | return m.Resource 141 | } 142 | return nil 143 | } 144 | 145 | func (m *Event) GetOrigin() string { 146 | if m != nil { 147 | return m.Origin 148 | } 149 | return "" 150 | } 151 | 152 | func (m *Event) GetDuration() float64 { 153 | if m != nil { 154 | return m.Duration 155 | } 156 | return 0 157 | } 158 | 159 | func (m *Event) GetErrorCode() ErrorCode { 160 | if m != nil { 161 | return m.ErrorCode 162 | } 163 | return ErrorCode_OK 164 | } 165 | 166 | func (m *Event) GetException() *Exception { 167 | if m != nil { 168 | return m.Exception 169 | } 170 | return nil 171 | } 172 | 173 | func init() { 174 | proto.RegisterType((*Resource)(nil), "protocol.Resource") 175 | proto.RegisterMapType((map[string]string)(nil), "protocol.Resource.MetadataEntry") 176 | proto.RegisterType((*Event)(nil), "protocol.Event") 177 | } 178 | 179 | func init() { proto.RegisterFile("event.proto", fileDescriptor_2d17a9d3f0ddf27e) } 180 | 181 | var fileDescriptor_2d17a9d3f0ddf27e = []byte{ 182 | // 317 bytes of a gzipped FileDescriptorProto 183 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x51, 0x4f, 0x4b, 0xfc, 0x30, 184 | 0x14, 0xa4, 0xdd, 0x3f, 0xbf, 0xf6, 0x2d, 0xbf, 0x75, 0x79, 0x8a, 0x84, 0xa2, 0x50, 0xf6, 0xb4, 185 | 0xa7, 0x82, 0xf5, 0x22, 0xea, 0x4d, 0xf6, 0xe8, 0x25, 0x78, 0x5f, 0x62, 0xfb, 0x90, 0xe0, 0xb6, 186 | 0x29, 0xd9, 0xec, 0x62, 0x8f, 0x7e, 0x3e, 0xbf, 0x94, 0x24, 0x4d, 0x5b, 0xc5, 0x53, 0xe7, 0xcd, 187 | 0xbc, 0xe9, 0x4c, 0x12, 0x58, 0xd0, 0x89, 0x6a, 0x93, 0x35, 0x5a, 0x19, 0x85, 0x91, 0xfb, 0x14, 188 | 0x6a, 0x9f, 0xac, 0x48, 0x6b, 0xa5, 0x77, 0x85, 0x2a, 0xa9, 0xd3, 0x92, 0x33, 0xfa, 0x28, 0xa8, 189 | 0x31, 0x52, 0xd5, 0x1d, 0xb1, 0xfe, 0x0a, 0x20, 0xe2, 0x74, 0x50, 0x47, 0x5d, 0x10, 0x22, 0x4c, 190 | 0x6b, 0x51, 0x11, 0x0b, 0xd2, 0x60, 0x13, 0x73, 0x87, 0x2d, 0x67, 0xda, 0x86, 0x58, 0xd8, 0x71, 191 | 0x16, 0xe3, 0x15, 0xc4, 0xaa, 0x21, 0x2d, 0xec, 0x7f, 0xd8, 0xc4, 0x09, 0x23, 0x81, 0x8f, 0x10, 192 | 0x55, 0x64, 0x44, 0x29, 0x8c, 0x60, 0xd3, 0x74, 0xb2, 0x59, 0xe4, 0x69, 0xd6, 0x57, 0xca, 0xfa, 193 | 0xac, 0xec, 0xd9, 0xaf, 0x6c, 0x6b, 0xa3, 0x5b, 0x3e, 0x38, 0x92, 0x07, 0xf8, 0xff, 0x4b, 0xc2, 194 | 0x15, 0x4c, 0xde, 0xa9, 0xf5, 0x9d, 0x2c, 0xc4, 0x0b, 0x98, 0x9d, 0xc4, 0xfe, 0xd8, 0x77, 0xea, 195 | 0x86, 0xfb, 0xf0, 0x2e, 0x58, 0x7f, 0x86, 0x30, 0xdb, 0xda, 0xab, 0xc0, 0x25, 0x84, 0xb2, 0xf4, 196 | 0xa6, 0x50, 0x96, 0x78, 0x0d, 0x70, 0x30, 0x42, 0x9b, 0x9d, 0x91, 0x55, 0x67, 0x0c, 0x78, 0xec, 197 | 0x98, 0x17, 0x59, 0x11, 0x66, 0x10, 0x69, 0xdf, 0xcc, 0x1d, 0x68, 0x91, 0xe3, 0xdf, 0xce, 0x7c, 198 | 0xd8, 0xc1, 0x4b, 0x98, 0x2b, 0x2d, 0xdf, 0x64, 0xcd, 0xa6, 0x2e, 0xc2, 0x4f, 0x98, 0x40, 0x54, 199 | 0x1e, 0xfd, 0xc5, 0xcc, 0x5c, 0xc8, 0x30, 0x63, 0x0e, 0x30, 0xbe, 0x07, 0x9b, 0xa7, 0xc1, 0x66, 200 | 0x99, 0x9f, 0x8f, 0x29, 0x5b, 0xab, 0x3d, 0xa9, 0x92, 0x78, 0x4c, 0x3d, 0xc4, 0x1b, 0x88, 0x87, 201 | 0x17, 0x63, 0xff, 0x5c, 0xb1, 0x9f, 0x96, 0x5e, 0xe2, 0xe3, 0xd6, 0xeb, 0xdc, 0xc9, 0xb7, 0xdf, 202 | 0x01, 0x00, 0x00, 0xff, 0xff, 0xad, 0xfc, 0xcc, 0x8a, 0x14, 0x02, 0x00, 0x00, 203 | } 204 | -------------------------------------------------------------------------------- /protocol/exception.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: exception.proto 3 | 4 | package protocol 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 22 | 23 | type Exception struct { 24 | Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` 25 | Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` 26 | Traceback string `protobuf:"bytes,3,opt,name=traceback,proto3" json:"traceback,omitempty"` 27 | Time float64 `protobuf:"fixed64,4,opt,name=time,proto3" json:"time,omitempty"` 28 | AdditionalData map[string]string `protobuf:"bytes,5,rep,name=additional_data,json=additionalData,proto3" json:"additional_data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` 29 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 30 | XXX_unrecognized []byte `json:"-"` 31 | XXX_sizecache int32 `json:"-"` 32 | } 33 | 34 | func (m *Exception) Reset() { *m = Exception{} } 35 | func (m *Exception) String() string { return proto.CompactTextString(m) } 36 | func (*Exception) ProtoMessage() {} 37 | func (*Exception) Descriptor() ([]byte, []int) { 38 | return fileDescriptor_fb6dc2e8aaa76c47, []int{0} 39 | } 40 | 41 | func (m *Exception) XXX_Unmarshal(b []byte) error { 42 | return xxx_messageInfo_Exception.Unmarshal(m, b) 43 | } 44 | func (m *Exception) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 45 | return xxx_messageInfo_Exception.Marshal(b, m, deterministic) 46 | } 47 | func (m *Exception) XXX_Merge(src proto.Message) { 48 | xxx_messageInfo_Exception.Merge(m, src) 49 | } 50 | func (m *Exception) XXX_Size() int { 51 | return xxx_messageInfo_Exception.Size(m) 52 | } 53 | func (m *Exception) XXX_DiscardUnknown() { 54 | xxx_messageInfo_Exception.DiscardUnknown(m) 55 | } 56 | 57 | var xxx_messageInfo_Exception proto.InternalMessageInfo 58 | 59 | func (m *Exception) GetType() string { 60 | if m != nil { 61 | return m.Type 62 | } 63 | return "" 64 | } 65 | 66 | func (m *Exception) GetMessage() string { 67 | if m != nil { 68 | return m.Message 69 | } 70 | return "" 71 | } 72 | 73 | func (m *Exception) GetTraceback() string { 74 | if m != nil { 75 | return m.Traceback 76 | } 77 | return "" 78 | } 79 | 80 | func (m *Exception) GetTime() float64 { 81 | if m != nil { 82 | return m.Time 83 | } 84 | return 0 85 | } 86 | 87 | func (m *Exception) GetAdditionalData() map[string]string { 88 | if m != nil { 89 | return m.AdditionalData 90 | } 91 | return nil 92 | } 93 | 94 | func init() { 95 | proto.RegisterType((*Exception)(nil), "protocol.Exception") 96 | proto.RegisterMapType((map[string]string)(nil), "protocol.Exception.AdditionalDataEntry") 97 | } 98 | 99 | func init() { proto.RegisterFile("exception.proto", fileDescriptor_fb6dc2e8aaa76c47) } 100 | 101 | var fileDescriptor_fb6dc2e8aaa76c47 = []byte{ 102 | // 210 bytes of a gzipped FileDescriptorProto 103 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4f, 0xad, 0x48, 0x4e, 104 | 0x2d, 0x28, 0xc9, 0xcc, 0xcf, 0xd3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x00, 0x53, 0xc9, 105 | 0xf9, 0x39, 0x4a, 0x0d, 0x4c, 0x5c, 0x9c, 0xae, 0x30, 0x59, 0x21, 0x21, 0x2e, 0x96, 0x92, 0xca, 106 | 0x82, 0x54, 0x09, 0x46, 0x05, 0x46, 0x0d, 0xce, 0x20, 0x30, 0x5b, 0x48, 0x82, 0x8b, 0x3d, 0x37, 107 | 0xb5, 0xb8, 0x38, 0x31, 0x3d, 0x55, 0x82, 0x09, 0x2c, 0x0c, 0xe3, 0x0a, 0xc9, 0x70, 0x71, 0x96, 108 | 0x14, 0x25, 0x26, 0xa7, 0x26, 0x25, 0x26, 0x67, 0x4b, 0x30, 0x83, 0xe5, 0x10, 0x02, 0x60, 0xb3, 109 | 0x32, 0x73, 0x53, 0x25, 0x58, 0x14, 0x18, 0x35, 0x18, 0x83, 0xc0, 0x6c, 0xa1, 0x00, 0x2e, 0xfe, 110 | 0xc4, 0x94, 0x94, 0x4c, 0x90, 0x5d, 0x89, 0x39, 0xf1, 0x29, 0x89, 0x25, 0x89, 0x12, 0xac, 0x0a, 111 | 0xcc, 0x1a, 0xdc, 0x46, 0xea, 0x7a, 0x30, 0x17, 0xe9, 0xc1, 0x5d, 0xa3, 0xe7, 0x08, 0x57, 0xea, 112 | 0x92, 0x58, 0x92, 0xe8, 0x9a, 0x57, 0x52, 0x54, 0x19, 0xc4, 0x97, 0x88, 0x22, 0x28, 0xe5, 0xc8, 113 | 0x25, 0x8c, 0x45, 0x99, 0x90, 0x00, 0x17, 0x73, 0x76, 0x6a, 0x25, 0xd4, 0x1f, 0x20, 0xa6, 0x90, 114 | 0x08, 0x17, 0x6b, 0x59, 0x62, 0x4e, 0x29, 0xcc, 0x13, 0x10, 0x8e, 0x15, 0x93, 0x05, 0x63, 0x12, 115 | 0x1b, 0xd8, 0x6a, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xbb, 0x2e, 0x17, 0x93, 0x26, 0x01, 116 | 0x00, 0x00, 117 | } 118 | -------------------------------------------------------------------------------- /protocol/timestamp.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: timestamp.proto 3 | 4 | package protocol 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 22 | 23 | type Timestamp struct { 24 | Seconds int64 `protobuf:"varint,1,opt,name=seconds,proto3" json:"seconds,omitempty"` 25 | Nanos int32 `protobuf:"varint,2,opt,name=nanos,proto3" json:"nanos,omitempty"` 26 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 27 | XXX_unrecognized []byte `json:"-"` 28 | XXX_sizecache int32 `json:"-"` 29 | } 30 | 31 | func (m *Timestamp) Reset() { *m = Timestamp{} } 32 | func (m *Timestamp) String() string { return proto.CompactTextString(m) } 33 | func (*Timestamp) ProtoMessage() {} 34 | func (*Timestamp) Descriptor() ([]byte, []int) { 35 | return fileDescriptor_edac929d8ae1e24f, []int{0} 36 | } 37 | 38 | func (m *Timestamp) XXX_Unmarshal(b []byte) error { 39 | return xxx_messageInfo_Timestamp.Unmarshal(m, b) 40 | } 41 | func (m *Timestamp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 42 | return xxx_messageInfo_Timestamp.Marshal(b, m, deterministic) 43 | } 44 | func (m *Timestamp) XXX_Merge(src proto.Message) { 45 | xxx_messageInfo_Timestamp.Merge(m, src) 46 | } 47 | func (m *Timestamp) XXX_Size() int { 48 | return xxx_messageInfo_Timestamp.Size(m) 49 | } 50 | func (m *Timestamp) XXX_DiscardUnknown() { 51 | xxx_messageInfo_Timestamp.DiscardUnknown(m) 52 | } 53 | 54 | var xxx_messageInfo_Timestamp proto.InternalMessageInfo 55 | 56 | func (m *Timestamp) GetSeconds() int64 { 57 | if m != nil { 58 | return m.Seconds 59 | } 60 | return 0 61 | } 62 | 63 | func (m *Timestamp) GetNanos() int32 { 64 | if m != nil { 65 | return m.Nanos 66 | } 67 | return 0 68 | } 69 | 70 | func init() { 71 | proto.RegisterType((*Timestamp)(nil), "protocol.Timestamp") 72 | } 73 | 74 | func init() { proto.RegisterFile("timestamp.proto", fileDescriptor_edac929d8ae1e24f) } 75 | 76 | var fileDescriptor_edac929d8ae1e24f = []byte{ 77 | // 97 bytes of a gzipped FileDescriptorProto 78 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2f, 0xc9, 0xcc, 0x4d, 79 | 0x2d, 0x2e, 0x49, 0xcc, 0x2d, 0xd0, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x00, 0x53, 0xc9, 80 | 0xf9, 0x39, 0x4a, 0xd6, 0x5c, 0x9c, 0x21, 0x30, 0x49, 0x21, 0x09, 0x2e, 0xf6, 0xe2, 0xd4, 0xe4, 81 | 0xfc, 0xbc, 0x94, 0x62, 0x09, 0x46, 0x05, 0x46, 0x0d, 0xe6, 0x20, 0x18, 0x57, 0x48, 0x84, 0x8b, 82 | 0x35, 0x2f, 0x31, 0x2f, 0xbf, 0x58, 0x82, 0x49, 0x81, 0x51, 0x83, 0x35, 0x08, 0xc2, 0x49, 0x62, 83 | 0x03, 0x1b, 0x63, 0x0c, 0x08, 0x00, 0x00, 0xff, 0xff, 0x41, 0x20, 0x05, 0x1a, 0x60, 0x00, 0x00, 84 | 0x00, 85 | } 86 | -------------------------------------------------------------------------------- /protocol/trace.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: trace.proto 3 | 4 | package protocol 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 22 | 23 | type Trace struct { 24 | AppName string `protobuf:"bytes,1,opt,name=app_name,json=appName,proto3" json:"app_name,omitempty"` 25 | Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` 26 | Events []*Event `protobuf:"bytes,3,rep,name=events,proto3" json:"events,omitempty"` 27 | Exceptions []*Exception `protobuf:"bytes,4,rep,name=exceptions,proto3" json:"exceptions,omitempty"` 28 | Version string `protobuf:"bytes,5,opt,name=version,proto3" json:"version,omitempty"` 29 | Platform string `protobuf:"bytes,6,opt,name=platform,proto3" json:"platform,omitempty"` 30 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 31 | XXX_unrecognized []byte `json:"-"` 32 | XXX_sizecache int32 `json:"-"` 33 | } 34 | 35 | func (m *Trace) Reset() { *m = Trace{} } 36 | func (m *Trace) String() string { return proto.CompactTextString(m) } 37 | func (*Trace) ProtoMessage() {} 38 | func (*Trace) Descriptor() ([]byte, []int) { 39 | return fileDescriptor_0571941a1d628a80, []int{0} 40 | } 41 | 42 | func (m *Trace) XXX_Unmarshal(b []byte) error { 43 | return xxx_messageInfo_Trace.Unmarshal(m, b) 44 | } 45 | func (m *Trace) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 46 | return xxx_messageInfo_Trace.Marshal(b, m, deterministic) 47 | } 48 | func (m *Trace) XXX_Merge(src proto.Message) { 49 | xxx_messageInfo_Trace.Merge(m, src) 50 | } 51 | func (m *Trace) XXX_Size() int { 52 | return xxx_messageInfo_Trace.Size(m) 53 | } 54 | func (m *Trace) XXX_DiscardUnknown() { 55 | xxx_messageInfo_Trace.DiscardUnknown(m) 56 | } 57 | 58 | var xxx_messageInfo_Trace proto.InternalMessageInfo 59 | 60 | func (m *Trace) GetAppName() string { 61 | if m != nil { 62 | return m.AppName 63 | } 64 | return "" 65 | } 66 | 67 | func (m *Trace) GetToken() string { 68 | if m != nil { 69 | return m.Token 70 | } 71 | return "" 72 | } 73 | 74 | func (m *Trace) GetEvents() []*Event { 75 | if m != nil { 76 | return m.Events 77 | } 78 | return nil 79 | } 80 | 81 | func (m *Trace) GetExceptions() []*Exception { 82 | if m != nil { 83 | return m.Exceptions 84 | } 85 | return nil 86 | } 87 | 88 | func (m *Trace) GetVersion() string { 89 | if m != nil { 90 | return m.Version 91 | } 92 | return "" 93 | } 94 | 95 | func (m *Trace) GetPlatform() string { 96 | if m != nil { 97 | return m.Platform 98 | } 99 | return "" 100 | } 101 | 102 | func init() { 103 | proto.RegisterType((*Trace)(nil), "protocol.Trace") 104 | } 105 | 106 | func init() { proto.RegisterFile("trace.proto", fileDescriptor_0571941a1d628a80) } 107 | 108 | var fileDescriptor_0571941a1d628a80 = []byte{ 109 | // 196 bytes of a gzipped FileDescriptorProto 110 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8e, 0x3f, 0xcf, 0x82, 0x30, 111 | 0x10, 0xc6, 0xc3, 0xcb, 0xcb, 0x1f, 0x8f, 0x81, 0xa4, 0x3a, 0x54, 0x26, 0xe2, 0x22, 0x13, 0x83, 112 | 0x7c, 0x06, 0x57, 0x07, 0xe2, 0x6e, 0x2a, 0x39, 0x13, 0x22, 0xb4, 0x97, 0xd2, 0x10, 0x3f, 0xa4, 113 | 0x1f, 0xca, 0xd0, 0x02, 0x61, 0x6a, 0x7e, 0xf7, 0x7b, 0x7a, 0xf7, 0x40, 0x62, 0xb4, 0x68, 0xb0, 114 | 0x24, 0xad, 0x8c, 0x62, 0xb1, 0x7d, 0x1a, 0xd5, 0x65, 0x29, 0x7e, 0x1a, 0x24, 0xd3, 0x2a, 0xe9, 115 | 0x54, 0x96, 0xe0, 0x88, 0xd2, 0x38, 0x38, 0x7d, 0x3d, 0x08, 0xee, 0xd3, 0x3f, 0x76, 0x84, 0x58, 116 | 0x10, 0x3d, 0xa4, 0xe8, 0x91, 0x7b, 0xb9, 0x57, 0xec, 0xea, 0x48, 0x10, 0xdd, 0x44, 0x8f, 0xec, 117 | 0x00, 0x81, 0x51, 0x6f, 0x94, 0xfc, 0xcf, 0xce, 0x1d, 0xb0, 0x33, 0x84, 0x76, 0xd3, 0xc0, 0xfd, 118 | 0xdc, 0x2f, 0x92, 0x4b, 0x5a, 0x2e, 0x37, 0xcb, 0xeb, 0x34, 0xaf, 0x67, 0xcd, 0x2a, 0x80, 0xb5, 119 | 0xc3, 0xc0, 0xff, 0x6d, 0x78, 0xbf, 0x09, 0x2f, 0xae, 0xde, 0xc4, 0x18, 0x87, 0x68, 0x44, 0x3d, 120 | 0xb4, 0x4a, 0xf2, 0xc0, 0xb5, 0x99, 0x91, 0x65, 0x10, 0x53, 0x27, 0xcc, 0x4b, 0xe9, 0x9e, 0x87, 121 | 0x56, 0xad, 0xfc, 0x0c, 0xed, 0xd6, 0xea, 0x17, 0x00, 0x00, 0xff, 0xff, 0x98, 0xec, 0x38, 0x25, 122 | 0x0c, 0x01, 0x00, 0x00, 123 | } 124 | -------------------------------------------------------------------------------- /trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-go/d24be95976ce03ae884596af6b793e0027d60c92/trace.png -------------------------------------------------------------------------------- /tracer/common_utils.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // GetTimestamp returns the current time in miliseconds 8 | func GetTimestamp() float64 { 9 | return float64(time.Now().UnixNano()) / float64(time.Millisecond) / float64(time.Nanosecond) / 1000.0 10 | } 11 | -------------------------------------------------------------------------------- /tracer/mask_ignored_keys.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | 7 | "github.com/epsagon/epsagon-go/protocol" 8 | ) 9 | 10 | const maskedValue = "****" 11 | 12 | func arrayToHitMap(arr []string) map[string]bool { 13 | hitMap := make(map[string]bool) 14 | for _, k := range arr { 15 | hitMap[k] = true 16 | } 17 | return hitMap 18 | } 19 | 20 | func maskNestedJSONKeys(decodedJSON interface{}, ignoredKeysMap map[string]bool) (interface{}, bool) { 21 | var changed bool 22 | decodedValue := reflect.ValueOf(decodedJSON) 23 | if decodedValue.Kind() == reflect.Invalid || decodedValue.IsZero() { 24 | return decodedJSON, false 25 | } 26 | switch decodedValue.Kind() { 27 | case reflect.Array, reflect.Slice: 28 | for i := 0; i < decodedValue.Len(); i++ { 29 | nestedValue := decodedValue.Index(i) 30 | newNestedValue, indexChanged := maskNestedJSONKeys(nestedValue.Interface(), ignoredKeysMap) 31 | if indexChanged { 32 | nestedValue.Set(reflect.ValueOf(newNestedValue)) 33 | changed = true 34 | } 35 | } 36 | case reflect.Map: 37 | for _, key := range decodedValue.MapKeys() { 38 | if ignoredKeysMap[key.String()] { 39 | decodedValue.SetMapIndex(key, reflect.ValueOf(maskedValue)) 40 | changed = true 41 | } else { 42 | nestedValue := decodedValue.MapIndex(key) 43 | newNestedValue, valueChanged := maskNestedJSONKeys(nestedValue.Interface(), ignoredKeysMap) 44 | if valueChanged { 45 | decodedValue.SetMapIndex(key, reflect.ValueOf(newNestedValue)) 46 | changed = true 47 | } 48 | } 49 | } 50 | } 51 | return decodedValue.Interface(), changed 52 | } 53 | 54 | // maskIgnoredKeys masks all the keys in the 55 | // event resource metadata that are in ignoredKeys, swapping them with '****'. 56 | // Metadata values that are json decodable will have their nested keys masked as well. 57 | func (tracer *epsagonTracer) maskEventIgnoredKeys(event *protocol.Event, ignoredKeys []string) { 58 | ignoredKeysMap := arrayToHitMap(ignoredKeys) 59 | for key, value := range event.Resource.Metadata { 60 | if ignoredKeysMap[key] { 61 | event.Resource.Metadata[key] = maskedValue 62 | } else { 63 | var decodedJSON interface{} 64 | err := json.Unmarshal([]byte(value), &decodedJSON) 65 | if err == nil { 66 | newValue, changed := maskNestedJSONKeys(decodedJSON, ignoredKeysMap) 67 | if changed { 68 | encodedNewValue, err := json.Marshal(newValue) 69 | if err == nil { 70 | event.Resource.Metadata[key] = string(encodedNewValue) 71 | } else { 72 | exception := createException("internal json encode error", err.Error()) 73 | if tracer.Stopped() { 74 | tracer.exceptions = append(tracer.exceptions, exception) 75 | } else { 76 | tracer.AddException(exception) 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tracer/mask_ignored_keys_test.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/epsagon/epsagon-go/protocol" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("mask_ignored_keys", func() { 12 | Describe("maskEventIgnoredKeys", func() { 13 | config := &Config{ 14 | Disable: true, 15 | } 16 | testTracer := CreateTracer(config).(*epsagonTracer) 17 | Context("sanity", func() { 18 | It("masks sanity test", func() { 19 | event := &protocol.Event{ 20 | Resource: &protocol.Resource{ 21 | Metadata: map[string]string{ 22 | "not-ignored": "hello", 23 | "ignored": "bye", 24 | }, 25 | }, 26 | } 27 | ignoredKeys := []string{"ignored"} 28 | testTracer.Config.IgnoredKeys = ignoredKeys 29 | testTracer.maskEventIgnoredKeys(event, ignoredKeys) 30 | Expect(event.Resource.Metadata["not-ignored"]).To(Equal("hello")) 31 | Expect(event.Resource.Metadata["ignored"]).To(Equal(maskedValue)) 32 | }) 33 | }) 34 | Context("test matrix", func() { 35 | type testCase struct { 36 | metadata map[string]string 37 | ignoredKeys []string 38 | expectedMetadata map[string]string 39 | } 40 | testMatrix := map[string]testCase{ 41 | "handles empty metadata": { 42 | metadata: map[string]string{}, 43 | ignoredKeys: []string{"ignored"}, 44 | expectedMetadata: map[string]string{}, 45 | }, 46 | "handles empty ignoredKeys": { 47 | metadata: map[string]string{"ignored": "bye"}, 48 | ignoredKeys: []string{}, 49 | expectedMetadata: map[string]string{"ignored": "bye"}, 50 | }, 51 | "passes matrix sanity": { 52 | metadata: map[string]string{"to-be-ignored": "bye", "not-ignored": "hello"}, 53 | ignoredKeys: []string{"to-be-ignored"}, 54 | expectedMetadata: map[string]string{"to-be-ignored": maskedValue, "not-ignored": "hello"}, 55 | }, 56 | "handles json map without ignored keys": { 57 | metadata: map[string]string{"not-ignored": "hello", "other-not-ignored": "{\"hello\":\"world\"}"}, 58 | ignoredKeys: []string{"to-be-ignored"}, 59 | expectedMetadata: map[string]string{"not-ignored": "hello", "other-not-ignored": "{\"hello\":\"world\"}"}, 60 | }, 61 | "handles json map *with* ignored keys": { 62 | metadata: map[string]string{"not-ignored": "hello", "other-not-ignored": "{\"to-be-ignored\":\"world\"}"}, 63 | ignoredKeys: []string{"to-be-ignored"}, 64 | expectedMetadata: map[string]string{"not-ignored": "hello", "other-not-ignored": fmt.Sprintf("{\"to-be-ignored\":\"%s\"}", maskedValue)}, 65 | }, 66 | "handles json nested array and map without ignored keys": { 67 | metadata: map[string]string{"not-ignored": "hello", "other-not-ignored": "[{\"hello\":\"world\"},{\"erez\":\"is-cool\"}]"}, 68 | ignoredKeys: []string{"to-be-ignored"}, 69 | expectedMetadata: map[string]string{"not-ignored": "hello", "other-not-ignored": "[{\"hello\":\"world\"},{\"erez\":\"is-cool\"}]"}, 70 | }, 71 | "handles json nested array and map *with* ignored keys": { 72 | metadata: map[string]string{ 73 | "not-ignored": "hello", 74 | "other-not-ignored": "[{\"to-be-ignored\":\"world\"},{\"erez\":\"is-cool\"}]", 75 | }, 76 | ignoredKeys: []string{"to-be-ignored"}, 77 | expectedMetadata: map[string]string{ 78 | "not-ignored": "hello", 79 | "other-not-ignored": fmt.Sprintf( 80 | "[{\"to-be-ignored\":\"%s\"},{\"erez\":\"is-cool\"}]", 81 | maskedValue), 82 | }, 83 | }, 84 | "handles json nested map *with* ignored keys": { 85 | metadata: map[string]string{ 86 | "not-ignored": "hello", 87 | "other-not-ignored": "{\"wait\":{\"for\":{\"it\":{\"to-be-ignored\":\"boom\"},\"not\":[\"I\",\"are\",\"baboon\"]},\"not\":\"it\"}}", 88 | }, 89 | ignoredKeys: []string{"to-be-ignored"}, 90 | expectedMetadata: map[string]string{ 91 | "not-ignored": "hello", 92 | "other-not-ignored": fmt.Sprintf( 93 | "{\"wait\":{\"for\":{\"it\":{\"to-be-ignored\":\"%s\"},\"not\":[\"I\",\"are\",\"baboon\"]},\"not\":\"it\"}}", 94 | maskedValue, 95 | ), 96 | }, 97 | }, 98 | } 99 | for testName, value := range testMatrix { 100 | value := value 101 | It(testName, func() { 102 | testMetadata := make(map[string]string) 103 | for k, v := range value.metadata { 104 | testMetadata[k] = v 105 | } 106 | event := &protocol.Event{ 107 | Resource: &protocol.Resource{ 108 | Metadata: testMetadata, 109 | }, 110 | } 111 | testTracer.maskEventIgnoredKeys(event, value.ignoredKeys) 112 | Expect(event.Resource.Metadata).To(Equal(value.expectedMetadata)) 113 | }) 114 | } 115 | }) 116 | }) 117 | }) 118 | -------------------------------------------------------------------------------- /tracer/mocked_tracer.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/epsagon/epsagon-go/protocol" 7 | ) 8 | 9 | // MockedEpsagonTracer will not send traces if closed 10 | type MockedEpsagonTracer struct { 11 | Exceptions *[]*protocol.Exception 12 | Events *[]*protocol.Event 13 | Labels map[string]interface{} 14 | RunnerException *protocol.Exception 15 | Config *Config 16 | 17 | PanicStart bool 18 | PanicAddEvent bool 19 | PanicAddException bool 20 | PanicStop bool 21 | DelayAddEvent bool 22 | DelayedEventsChan chan bool 23 | stopped bool 24 | } 25 | 26 | // Start implementes mocked Start 27 | func (t *MockedEpsagonTracer) Start() { 28 | if t.PanicStart { 29 | panic("panic in Start()") 30 | } 31 | t.stopped = false 32 | } 33 | 34 | // Running implementes mocked Running 35 | func (t *MockedEpsagonTracer) Running() bool { 36 | return false 37 | } 38 | 39 | // Stop implementes mocked Stop 40 | func (t *MockedEpsagonTracer) SendStopSignal() { 41 | if t.PanicStop { 42 | panic("panic in Stop()") 43 | } 44 | t.stopped = true 45 | } 46 | 47 | // Stop implementes mocked Stop 48 | func (t *MockedEpsagonTracer) Stop() { 49 | if t.PanicStop { 50 | panic("panic in Stop()") 51 | } 52 | t.stopped = true 53 | } 54 | 55 | // Stopped implementes mocked Stopped 56 | func (t *MockedEpsagonTracer) Stopped() bool { 57 | return t.stopped 58 | } 59 | 60 | // AddEvent implementes mocked AddEvent 61 | func (t *MockedEpsagonTracer) AddEvent(e *protocol.Event) { 62 | if t.PanicAddEvent { 63 | panic("panic in AddEvent()") 64 | } 65 | if t.DelayAddEvent { 66 | go func() { 67 | time.Sleep(time.Second) 68 | *t.Events = append(*t.Events, e) 69 | t.DelayedEventsChan <- true 70 | }() 71 | } else { 72 | *t.Events = append(*t.Events, e) 73 | } 74 | } 75 | 76 | // AddException implementes mocked AddEvent 77 | func (t *MockedEpsagonTracer) AddException(e *protocol.Exception) { 78 | if t.PanicAddException { 79 | panic("panic in AddException()") 80 | } 81 | *t.Exceptions = append(*t.Exceptions, e) 82 | } 83 | 84 | // GetConfig implementes mocked AddEvent 85 | func (t *MockedEpsagonTracer) GetConfig() *Config { 86 | return t.Config 87 | } 88 | 89 | // AddExceptionTypeAndMessage implements AddExceptionTypeAndMessage 90 | func (t *MockedEpsagonTracer) AddExceptionTypeAndMessage(exceptionType, msg string) { 91 | t.AddException(&protocol.Exception{ 92 | Type: exceptionType, 93 | Message: msg, 94 | Time: GetTimestamp(), 95 | }) 96 | } 97 | 98 | // AddLabel implements AddLabel 99 | func (t *MockedEpsagonTracer) AddLabel(key string, value interface{}) { 100 | t.Labels[key] = value 101 | } 102 | 103 | // verifyLabel implements verifyLabel 104 | func (t *MockedEpsagonTracer) verifyLabel(label epsagonLabel) bool { 105 | return true 106 | } 107 | 108 | // AddError implements AddError 109 | func (t *MockedEpsagonTracer) AddError(errorType string, value interface{}) { 110 | t.RunnerException = &protocol.Exception{ 111 | Type: errorType, 112 | Message: "test", 113 | } 114 | } 115 | 116 | // GetRunnerEvent implements AddError 117 | func (t *MockedEpsagonTracer) GetRunnerEvent() *protocol.Event { 118 | for _, event := range *t.Events { 119 | if event.Origin == "runner" { 120 | return event 121 | } 122 | } 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /tracer/tracer_test.go: -------------------------------------------------------------------------------- 1 | package tracer_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "net/http/httptest" 12 | "os" 13 | "strings" 14 | 15 | "testing" 16 | "time" 17 | 18 | "github.com/epsagon/epsagon-go/epsagon" 19 | "github.com/epsagon/epsagon-go/protocol" 20 | "github.com/epsagon/epsagon-go/tracer" 21 | . "github.com/onsi/ginkgo" 22 | . "github.com/onsi/gomega" 23 | ) 24 | 25 | func TestEpsagonTracer(t *testing.T) { 26 | RegisterFailHandler(Fail) 27 | RunSpecs(t, "Epsagon Core Suite") 28 | } 29 | 30 | var _ = Describe("epsagonTracer suite", func() { 31 | Describe("Run/Stop", func() { 32 | }) 33 | Describe("AddEvent", func() { 34 | }) 35 | Describe("AddException", func() { 36 | }) 37 | Describe("sendTraces", func() { 38 | }) 39 | }) 40 | 41 | func runWithTracer(endpoint string, operations func()) { 42 | tracer.GlobalTracer = nil 43 | tracer.CreateGlobalTracer(&tracer.Config{ 44 | CollectorURL: endpoint, 45 | Token: "1", 46 | }) 47 | tracer.GlobalTracer.Start() 48 | defer tracer.StopGlobalTracer() 49 | operations() 50 | } 51 | 52 | // testWithTracer runs a test with 53 | func testWithTracer(timeout *time.Duration, operations func()) *protocol.Trace { 54 | traceChannel := make(chan *protocol.Trace) 55 | fakeCollectorServer := httptest.NewServer(http.HandlerFunc( 56 | func(res http.ResponseWriter, req *http.Request) { 57 | buf, err := ioutil.ReadAll(req.Body) 58 | if err != nil { 59 | traceChannel <- nil 60 | return 61 | } 62 | var receivedTrace protocol.Trace 63 | err = json.Unmarshal(buf, &receivedTrace) 64 | if err != nil { 65 | traceChannel <- nil 66 | return 67 | } 68 | traceChannel <- &receivedTrace 69 | res.Write([]byte("")) 70 | }, 71 | )) 72 | go runWithTracer(fakeCollectorServer.URL, operations) 73 | if timeout == nil { 74 | defaultTimeout := time.Second * 10 75 | timeout = &defaultTimeout 76 | } 77 | timer := time.NewTimer(*timeout) 78 | select { 79 | case <-timer.C: 80 | fakeCollectorServer.Close() 81 | return nil 82 | case trace := <-traceChannel: 83 | fakeCollectorServer.Close() 84 | return trace 85 | } 86 | } 87 | 88 | type stubHTTPClient struct { 89 | httpClient *http.Client 90 | PostError error 91 | } 92 | 93 | func (s stubHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { 94 | if s.PostError != nil { 95 | return nil, s.PostError 96 | } 97 | return s.httpClient.Post(url, contentType, body) 98 | } 99 | 100 | func Test_handleSendTracesResponse(t *testing.T) { 101 | tests := []struct { 102 | name string 103 | apiResponse string 104 | apiStatusCode int 105 | httpClient stubHTTPClient 106 | expectedLog string 107 | }{ 108 | { 109 | name: "No Log", 110 | apiResponse: `{"test":"valid"}`, 111 | apiStatusCode: http.StatusOK, 112 | httpClient: stubHTTPClient{ 113 | httpClient: &http.Client{Timeout: time.Duration(time.Second)}, 114 | }, 115 | expectedLog: "", 116 | }, 117 | { 118 | name: "Error With No Response", 119 | httpClient: stubHTTPClient{ 120 | httpClient: &http.Client{Timeout: time.Duration(time.Second)}, 121 | PostError: fmt.Errorf("Post http://not-valid-blackole.local.test: dial tcp: lookup not-valid-blackole.local.test: no such host"), 122 | }, 123 | expectedLog: fmt.Sprintf("Error while sending traces \nPost http://not-valid-blackole.local.test: dial tcp: lookup not-valid-blackole.local.test: no such host"), 124 | }, 125 | { 126 | name: "Error With 5XX Response", 127 | apiResponse: `{"error":"failed to send traces"}`, 128 | httpClient: stubHTTPClient{ 129 | httpClient: &http.Client{Timeout: time.Duration(time.Second)}, 130 | }, 131 | apiStatusCode: http.StatusInternalServerError, 132 | expectedLog: fmt.Sprintf("Error while sending traces \n{\"error\":\"failed to send traces\"}"), 133 | }, 134 | } 135 | for _, test := range tests { 136 | t.Run(test.name, func(t *testing.T) { 137 | //Read the logs to a buffer 138 | buf := bytes.Buffer{} 139 | log.SetOutput(&buf) 140 | defer func() { 141 | log.SetOutput(os.Stderr) 142 | }() 143 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 144 | w.WriteHeader(test.apiStatusCode) 145 | w.Write([]byte(test.apiResponse)) 146 | })) 147 | defer server.Close() 148 | resp, err := test.httpClient.Post(server.URL, "application/json", nil) 149 | tracer.HandleSendTracesResponse(resp, err) 150 | 151 | if !strings.Contains(buf.String(), test.expectedLog) { 152 | t.Errorf("Unexpected log: expected %s got %s", test.expectedLog, buf.String()) 153 | } 154 | 155 | }) 156 | } 157 | } 158 | 159 | func Test_AddLabel_sanity(t *testing.T) { 160 | defaultTimeout := time.Second * 5 161 | timeout := &defaultTimeout 162 | trace := testWithTracer(timeout, func() { epsagon.Label("test_key", "test_value") }) 163 | Expect(trace).ToNot(BeNil()) 164 | } 165 | 166 | func Test_AddError_sanity(t *testing.T) { 167 | defaultTimeout := time.Second * 5 168 | timeout := &defaultTimeout 169 | trace := testWithTracer(timeout, func() { epsagon.Error("some error") }) 170 | Expect(trace).ToNot(BeNil()) 171 | } 172 | 173 | func Test_AddTypeError(t *testing.T) { 174 | defaultTimeout := time.Second * 5 175 | timeout := &defaultTimeout 176 | trace := testWithTracer(timeout, func() { epsagon.TypeError("some error", "test error type") }) 177 | Expect(trace).ToNot(BeNil()) 178 | } 179 | 180 | func Test_MaxTraceSize_sanity(t *testing.T) { 181 | defer os.Unsetenv(tracer.MaxTraceSizeEnvVar) 182 | os.Setenv(tracer.MaxTraceSizeEnvVar, "2048") 183 | defaultTimeout := time.Second * 5 184 | timeout := &defaultTimeout 185 | trace := testWithTracer(timeout, func() { epsagon.Label("1", "2") }) 186 | Expect(trace).ToNot(BeNil()) 187 | os.Setenv(tracer.MaxTraceSizeEnvVar, "64") 188 | trace = testWithTracer(timeout, func() { epsagon.Label("1", "2") }) 189 | Expect(trace).To(BeNil()) 190 | } 191 | -------------------------------------------------------------------------------- /tracer/version.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | // VERSION is Epsagon tracer version 4 | const VERSION = "1.39.0" 5 | -------------------------------------------------------------------------------- /wrappers/aws/aws-sdk-go/aws/aws.go: -------------------------------------------------------------------------------- 1 | package epsagonawswrapper 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/aws/aws-sdk-go/aws/request" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/epsagon/epsagon-go/epsagon" 10 | "github.com/epsagon/epsagon-go/protocol" 11 | "github.com/epsagon/epsagon-go/tracer" 12 | "log" 13 | "time" 14 | ) 15 | 16 | // WrapSession wraps an aws session.Session with epsgaon traces 17 | func WrapSession(s *session.Session, args ...context.Context) *session.Session { 18 | if s == nil { 19 | return s 20 | } 21 | s.Handlers.Complete.PushFrontNamed( 22 | request.NamedHandler{ 23 | Name: "github.com/epsagon/epsagon-go/wrappers/aws/aws-sdk-go/aws/aws.go", 24 | Fn: func(r *request.Request) { 25 | currentTracer := epsagon.ExtractTracer(args) 26 | completeEventData(r, currentTracer) 27 | }, 28 | }) 29 | return s 30 | } 31 | 32 | func getTimeStampFromRequest(r *request.Request) float64 { 33 | return float64(r.Time.UTC().UnixNano()) / float64(time.Millisecond) / float64(time.Nanosecond) / 1000.0 34 | } 35 | 36 | func completeEventData(r *request.Request, currentTracer tracer.Tracer) { 37 | defer epsagon.GeneralEpsagonRecover("aws-sdk-go wrapper", "", currentTracer) 38 | if currentTracer.GetConfig().Debug { 39 | log.Printf("EPSAGON DEBUG OnComplete current tracer: %+v\n", currentTracer) 40 | log.Printf("EPSAGON DEBUG OnComplete request response: %+v\n", r.HTTPResponse) 41 | log.Printf("EPSAGON DEBUG OnComplete request Operation: %+v\n", r.Operation) 42 | log.Printf("EPSAGON DEBUG OnComplete request ClientInfo: %+v\n", r.ClientInfo) 43 | log.Printf("EPSAGON DEBUG OnComplete request Params: %+v\n", r.Params) 44 | log.Printf("EPSAGON DEBUG OnComplete request Data: %+v\n", r.Data) 45 | } 46 | 47 | endTime := tracer.GetTimestamp() 48 | event := protocol.Event{ 49 | Id: r.RequestID, 50 | StartTime: getTimeStampFromRequest(r), 51 | Origin: "aws-sdk", 52 | Resource: extractResourceInformation(r, currentTracer), 53 | } 54 | event.Duration = endTime - event.StartTime 55 | currentTracer.AddEvent(&event) 56 | } 57 | 58 | type factory func(*request.Request, *protocol.Resource, bool, tracer.Tracer) 59 | 60 | var awsResourceEventFactories = map[string]factory{ 61 | "sqs": sqsEventDataFactory, 62 | "s3": s3EventDataFactory, 63 | "dynamodb": dynamodbEventDataFactory, 64 | "kinesis": kinesisEventDataFactory, 65 | "ses": sesEventDataFactory, 66 | "sns": snsEventDataFactory, 67 | "lambda": lambdaEventDataFactory, 68 | "sfn": sfnEventDataFactory, 69 | } 70 | 71 | func extractResourceInformation( 72 | r *request.Request, currentTracer tracer.Tracer) *protocol.Resource { 73 | res := protocol.Resource{ 74 | Type: r.ClientInfo.ServiceName, 75 | Operation: r.Operation.Name, 76 | Metadata: make(map[string]string), 77 | } 78 | factory := awsResourceEventFactories[res.Type] 79 | if factory != nil { 80 | factory(r, &res, currentTracer.GetConfig().MetadataOnly, currentTracer) 81 | } else { 82 | defaultFactory(r, &res, currentTracer.GetConfig().MetadataOnly, currentTracer) 83 | } 84 | return &res 85 | } 86 | 87 | func defaultFactory(r *request.Request, res *protocol.Resource, metadataOnly bool, currentTracer tracer.Tracer) { 88 | if currentTracer.GetConfig().Debug { 89 | log.Println("EPSAGON DEBUG:: entering defaultFactory") 90 | } 91 | if !metadataOnly { 92 | extractInterfaceToMetadata(r.Data, res) 93 | extractInterfaceToMetadata(r.Params, res) 94 | } 95 | } 96 | 97 | func extractInterfaceToMetadata(input interface{}, res *protocol.Resource) { 98 | var data map[string]interface{} 99 | rawJSON, err := json.Marshal(input) 100 | if err != nil { 101 | log.Printf("EPSAGON DEBUG: Failed to marshal input: %+v\n", input) 102 | return 103 | } 104 | err = json.Unmarshal(rawJSON, &data) 105 | if err != nil { 106 | log.Printf("EPSAGON DEBUG: Failed to unmarshal input: %+v\n", rawJSON) 107 | return 108 | } 109 | for key, value := range data { 110 | res.Metadata[key] = fmt.Sprintf("%v", value) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /wrappers/aws/aws-sdk-go/aws/common_utils.go: -------------------------------------------------------------------------------- 1 | package epsagonawswrapper 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | 9 | "github.com/aws/aws-sdk-go/aws/request" 10 | "github.com/epsagon/epsagon-go/protocol" 11 | "github.com/epsagon/epsagon-go/tracer" 12 | ) 13 | 14 | type specificOperationHandler func(r *request.Request, res *protocol.Resource, metadataOnly bool, currenttracer tracer.Tracer) 15 | 16 | func handleSpecificOperation( 17 | r *request.Request, 18 | res *protocol.Resource, 19 | metadataOnly bool, 20 | handlers map[string]specificOperationHandler, 21 | defaultHandler specificOperationHandler, 22 | currentTracer tracer.Tracer, 23 | ) { 24 | handler := handlers[res.Operation] 25 | if handler == nil { 26 | handler = defaultHandler 27 | } 28 | if handler != nil { 29 | handler(r, res, metadataOnly, currentTracer) 30 | } 31 | } 32 | 33 | func isValueZero(value reflect.Value) bool { 34 | return !value.IsValid() || value.IsZero() 35 | } 36 | 37 | func getFieldStringPtr(value reflect.Value, fieldName string) (string, bool) { 38 | field := value.FieldByName(fieldName) 39 | if isValueZero(field) { 40 | return "", false 41 | } 42 | return field.Elem().String(), true 43 | } 44 | 45 | func updateMetadataFromBytes( 46 | value reflect.Value, fieldName string, targetKey string, metadata map[string]string) { 47 | field := value.FieldByName(fieldName) 48 | if isValueZero(field) { 49 | return 50 | } 51 | metadata[targetKey] = string(field.Bytes()) 52 | } 53 | 54 | func updateMetadataFromValue( 55 | value reflect.Value, fieldName string, targetKey string, metadata map[string]string) { 56 | fieldValue, ok := getFieldStringPtr(value, fieldName) 57 | if ok { 58 | metadata[targetKey] = fieldValue 59 | } 60 | } 61 | 62 | func updateMetadataFromInt64( 63 | value reflect.Value, fieldName string, targetKey string, metadata map[string]string) { 64 | field := value.FieldByName(fieldName) 65 | if isValueZero(field) { 66 | return 67 | } 68 | metadata[targetKey] = strconv.FormatInt(field.Elem().Int(), 10) 69 | } 70 | 71 | func updateMetadataWithFieldToJSON( 72 | value reflect.Value, 73 | fieldName string, 74 | targetKey string, 75 | metadata map[string]string, 76 | currentTracer tracer.Tracer, 77 | ) { 78 | field := value.FieldByName(fieldName) 79 | if isValueZero(field) { 80 | return 81 | } 82 | stream, err := json.Marshal(field.Interface()) 83 | if err != nil { 84 | currentTracer.AddExceptionTypeAndMessage("aws-sdk-go", fmt.Sprintf("%v", err)) 85 | return 86 | } 87 | metadata[targetKey] = string(stream) 88 | } 89 | -------------------------------------------------------------------------------- /wrappers/aws/aws-sdk-go/aws/kinesis_factory.go: -------------------------------------------------------------------------------- 1 | package epsagonawswrapper 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/request" 5 | "github.com/epsagon/epsagon-go/protocol" 6 | "github.com/epsagon/epsagon-go/tracer" 7 | "reflect" 8 | ) 9 | 10 | func kinesisEventDataFactory( 11 | r *request.Request, 12 | res *protocol.Resource, 13 | metadataOnly bool, 14 | currentTracer tracer.Tracer, 15 | ) { 16 | inputValue := reflect.ValueOf(r.Params).Elem() 17 | streamName, ok := getFieldStringPtr(inputValue, "StreamName") 18 | if !ok { 19 | currentTracer.AddExceptionTypeAndMessage("aws-sdk-go", 20 | "kinesisEventDataFactory: couldn't find StreamName") 21 | } 22 | res.Name = streamName 23 | updateMetadataFromValue(inputValue, "PartitionKey", "partition_key", res.Metadata) 24 | if !metadataOnly { 25 | dataField := inputValue.FieldByName("Data") 26 | if dataField != (reflect.Value{}) { 27 | res.Metadata["data"] = string(dataField.Bytes()) 28 | } 29 | } 30 | handleSpecificOperation(r, res, metadataOnly, 31 | map[string]specificOperationHandler{ 32 | "PutRecord": handleKinesisPutRecord, 33 | }, nil, currentTracer, 34 | ) 35 | } 36 | 37 | func handleKinesisPutRecord( 38 | r *request.Request, 39 | res *protocol.Resource, 40 | metadataOnly bool, 41 | currentTracer tracer.Tracer, 42 | ) { 43 | outputValue := reflect.ValueOf(r.Data).Elem() 44 | updateMetadataFromValue(outputValue, "ShardId", "shared_id", res.Metadata) 45 | updateMetadataFromValue(outputValue, "SequenceNumber", "sequence_number", res.Metadata) 46 | } 47 | -------------------------------------------------------------------------------- /wrappers/aws/aws-sdk-go/aws/lambda_factory.go: -------------------------------------------------------------------------------- 1 | package epsagonawswrapper 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aws/aws-sdk-go/aws/request" 6 | "github.com/epsagon/epsagon-go/protocol" 7 | "github.com/epsagon/epsagon-go/tracer" 8 | "io" 9 | "reflect" 10 | ) 11 | 12 | func lambdaEventDataFactory( 13 | r *request.Request, 14 | res *protocol.Resource, 15 | metadataOnly bool, 16 | currentTracer tracer.Tracer, 17 | ) { 18 | inputValue := reflect.ValueOf(r.Params).Elem() 19 | functionName, ok := getFieldStringPtr(inputValue, "FunctionName") 20 | if ok { 21 | res.Name = functionName 22 | } 23 | if metadataOnly { 24 | return 25 | } 26 | updateMetadataFromBytes(inputValue, "Payload", "payload", res.Metadata) 27 | invokeArgsField := inputValue.FieldByName("InvokeArgs") 28 | if invokeArgsField == (reflect.Value{}) { 29 | return 30 | } 31 | invokeArgsReader := invokeArgsField.Interface().(io.ReadSeeker) 32 | invokeArgsBytes := make([]byte, 100) 33 | 34 | initialOffset, err := invokeArgsReader.Seek(int64(0), io.SeekStart) 35 | if err != nil { 36 | currentTracer.AddExceptionTypeAndMessage("aws-sdk-go", 37 | fmt.Sprintf("lambdaEventDataFactory: %v", err)) 38 | return 39 | } 40 | 41 | _, err = invokeArgsReader.Read(invokeArgsBytes) 42 | if err != nil { 43 | currentTracer.AddExceptionTypeAndMessage("aws-sdk-go", 44 | fmt.Sprintf("lambdaEventDataFactory: %v", err)) 45 | return 46 | } 47 | res.Metadata["invoke_args"] = string(invokeArgsBytes) 48 | _, err = invokeArgsReader.Seek(initialOffset, io.SeekStart) 49 | if err != nil { 50 | currentTracer.AddExceptionTypeAndMessage("aws-sdk-go", 51 | fmt.Sprintf("lambdaEventDataFactory: %v", err)) 52 | return 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /wrappers/aws/aws-sdk-go/aws/s3_factory.go: -------------------------------------------------------------------------------- 1 | package epsagonawswrapper 2 | 3 | import ( 4 | "fmt" 5 | "github.com/aws/aws-sdk-go/aws/request" 6 | "github.com/epsagon/epsagon-go/protocol" 7 | "github.com/epsagon/epsagon-go/tracer" 8 | "reflect" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func s3EventDataFactory( 14 | r *request.Request, 15 | res *protocol.Resource, 16 | metadataOnly bool, 17 | currentTracer tracer.Tracer, 18 | ) { 19 | inputValue := reflect.ValueOf(r.Params).Elem() 20 | bucketName, ok := getFieldStringPtr(inputValue, "Bucket") 21 | if ok { 22 | res.Name = bucketName 23 | } 24 | handleSpecificOperations := map[string]specificOperationHandler{ 25 | "HeadObject": handleS3GetOrHeadObject, 26 | "GetObject": handleS3GetOrHeadObject, 27 | "PutObject": handleS3PutObject, 28 | "ListObjects": handleS3ListObject, 29 | } 30 | handler := handleSpecificOperations[res.Operation] 31 | if handler != nil { 32 | handler(r, res, metadataOnly, currentTracer) 33 | } 34 | } 35 | 36 | func commonS3OpertionHandler(r *request.Request, res *protocol.Resource, metadataOnly bool) { 37 | inputValue := reflect.ValueOf(r.Params).Elem() 38 | updateMetadataFromValue(inputValue, "Key", "key", res.Metadata) 39 | outputValue := reflect.ValueOf(r.Data).Elem() 40 | etag, ok := getFieldStringPtr(outputValue, "ETag") 41 | if ok { 42 | etag = strings.Trim(etag, "\"") 43 | res.Metadata["etag"] = etag 44 | } 45 | } 46 | 47 | func handleS3GetOrHeadObject(r *request.Request, res *protocol.Resource, metadataOnly bool, _ tracer.Tracer) { 48 | commonS3OpertionHandler(r, res, metadataOnly) 49 | outputValue := reflect.ValueOf(r.Data).Elem() 50 | updateMetadataFromValue(outputValue, "ContentLength", "file_size", res.Metadata) 51 | 52 | lastModifiedField := outputValue.FieldByName("LastModified") 53 | if lastModifiedField == (reflect.Value{}) { 54 | return 55 | } 56 | lastModified := lastModifiedField.Elem().Interface().(time.Time) 57 | res.Metadata["last_modified"] = lastModified.String() 58 | } 59 | 60 | func handleS3PutObject(r *request.Request, res *protocol.Resource, metadataOnly bool, _ tracer.Tracer) { 61 | commonS3OpertionHandler(r, res, metadataOnly) 62 | } 63 | 64 | type s3File struct { 65 | key string 66 | size int64 67 | etag string 68 | } 69 | 70 | func handleS3ListObject(r *request.Request, res *protocol.Resource, metadataOnly bool, _ tracer.Tracer) { 71 | if metadataOnly { 72 | return 73 | } 74 | 75 | outputValue := reflect.ValueOf(r.Data).Elem() 76 | contentsField := outputValue.FieldByName("Contents") 77 | if contentsField == (reflect.Value{}) { 78 | return 79 | } 80 | length := contentsField.Len() 81 | files := make([]s3File, length) 82 | for i := 0; i < length; i++ { 83 | var key, etag string 84 | var size int64 85 | fileObject := contentsField.Index(i).Elem() 86 | etag = fileObject.FieldByName("ETag").Elem().String() 87 | key = fileObject.FieldByName("Key").Elem().String() 88 | size = fileObject.FieldByName("Size").Elem().Int() 89 | 90 | files = append(files, s3File{key, size, etag}) 91 | } 92 | res.Metadata["files"] = fmt.Sprintf("%+v", files) 93 | } 94 | -------------------------------------------------------------------------------- /wrappers/aws/aws-sdk-go/aws/ses_factory.go: -------------------------------------------------------------------------------- 1 | package epsagonawswrapper 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/request" 5 | "github.com/epsagon/epsagon-go/protocol" 6 | "github.com/epsagon/epsagon-go/tracer" 7 | "reflect" 8 | ) 9 | 10 | func sesEventDataFactory( 11 | r *request.Request, 12 | res *protocol.Resource, 13 | metadataOnly bool, 14 | currentTracer tracer.Tracer, 15 | ) { 16 | handleSpecificOperation(r, res, metadataOnly, 17 | map[string]specificOperationHandler{ 18 | "SendEmail": handleSESSendEmail, 19 | }, 20 | nil, currentTracer, 21 | ) 22 | } 23 | 24 | func handleSESSendEmail( 25 | r *request.Request, 26 | res *protocol.Resource, 27 | metadataOnly bool, 28 | currentTracer tracer.Tracer, 29 | ) { 30 | inputValue := reflect.ValueOf(r.Params).Elem() 31 | updateMetadataFromValue(inputValue, "Source", "source", res.Metadata) 32 | updateMetadataWithFieldToJSON(inputValue, "Destination", "destination", res.Metadata, currentTracer) 33 | messageField := inputValue.FieldByName("Message") 34 | if messageField != (reflect.Value{}) { 35 | updateMetadataWithFieldToJSON(messageField, "Subject", "subject", res.Metadata, currentTracer) 36 | if !metadataOnly { 37 | updateMetadataWithFieldToJSON(messageField, "Body", "body", res.Metadata, currentTracer) 38 | } 39 | } 40 | outputValue := reflect.ValueOf(r.Data).Elem() 41 | updateMetadataFromValue(outputValue, "MessageId", "message_id", res.Metadata) 42 | } 43 | -------------------------------------------------------------------------------- /wrappers/aws/aws-sdk-go/aws/sfn_factory.go: -------------------------------------------------------------------------------- 1 | package epsagonawswrapper 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/request" 5 | "github.com/epsagon/epsagon-go/protocol" 6 | "github.com/epsagon/epsagon-go/tracer" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | func sfnEventDataFactory( 12 | r *request.Request, 13 | res *protocol.Resource, 14 | metadataOnly bool, 15 | currentTracer tracer.Tracer, 16 | ) { 17 | res.Type = "stepfunctions" 18 | handleSpecificOperation(r, res, metadataOnly, 19 | map[string]specificOperationHandler{ 20 | "PutRecord": handleSFNStartExecution, 21 | }, nil, currentTracer, 22 | ) 23 | } 24 | 25 | func handleSFNStartExecution(r *request.Request, res *protocol.Resource, metadataOnly bool, _ tracer.Tracer) { 26 | inputValue := reflect.ValueOf(r.Params).Elem() 27 | arn, ok := getFieldStringPtr(inputValue, "StateMachineArn") 28 | if ok { 29 | arnParts := strings.Split(arn, ":") 30 | res.Name = arnParts[len(arnParts)-1] 31 | res.Metadata["State Machine ARN"] = arn 32 | } 33 | updateMetadataFromValue(inputValue, "Name", "Execution Name", res.Metadata) 34 | if !metadataOnly { 35 | updateMetadataFromValue(inputValue, "Input", "input", res.Metadata) 36 | } 37 | outputValue := reflect.ValueOf(r.Data).Elem() 38 | updateMetadataFromValue(outputValue, "ExecutionArn", "Execution ARN", res.Metadata) 39 | } 40 | -------------------------------------------------------------------------------- /wrappers/aws/aws-sdk-go/aws/sns_factory.go: -------------------------------------------------------------------------------- 1 | package epsagonawswrapper 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/request" 5 | "github.com/epsagon/epsagon-go/protocol" 6 | "github.com/epsagon/epsagon-go/tracer" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | const InvalidFieldValue = "" 12 | 13 | func snsEventDataFactory( 14 | r *request.Request, 15 | res *protocol.Resource, 16 | metadataOnly bool, 17 | currentTracer tracer.Tracer, 18 | ) { 19 | if !metadataOnly { 20 | inputValue := reflect.ValueOf(r.Params).Elem() 21 | updateMetadataFromValue(inputValue, "Message", "Notification Message", res.Metadata) 22 | } 23 | handleSpecificOperation(r, res, metadataOnly, 24 | map[string]specificOperationHandler{ 25 | "CreateTopic": handleSNSCreateTopic, 26 | "Publish": handlerSNSPublish, 27 | }, 28 | handleSNSdefault, currentTracer, 29 | ) 30 | } 31 | 32 | // gets the target name 33 | func getSNStargetName(inputValue reflect.Value, targetKey string) (string, bool) { 34 | arnString, ok := getFieldStringPtr(inputValue, targetKey) 35 | if !ok { 36 | return "", false 37 | } 38 | arnSplit := strings.Split(arnString, ":") 39 | targetName := arnSplit[len(arnSplit)-1] 40 | return targetName, targetName != InvalidFieldValue 41 | } 42 | 43 | func handleSNSdefault(r *request.Request, res *protocol.Resource, metadataOnly bool, _ tracer.Tracer) { 44 | inputValue := reflect.ValueOf(r.Params).Elem() 45 | targetName, ok := getSNStargetName(inputValue, "TopicArn") 46 | if ok { 47 | res.Name = targetName 48 | return 49 | } 50 | targetName, ok = getSNStargetName(inputValue, "TargetArn") 51 | if ok { 52 | res.Name = targetName 53 | } 54 | } 55 | 56 | func handleSNSCreateTopic(r *request.Request, res *protocol.Resource, metadataOnly bool, _ tracer.Tracer) { 57 | inputValue := reflect.ValueOf(r.Params).Elem() 58 | name, ok := getFieldStringPtr(inputValue, "Name") 59 | if ok { 60 | res.Name = name 61 | } 62 | } 63 | 64 | func handlerSNSPublish(r *request.Request, res *protocol.Resource, metadataOnly bool, currentTracer tracer.Tracer) { 65 | handleSNSdefault(r, res, metadataOnly, currentTracer) 66 | outputValue := reflect.ValueOf(r.Data).Elem() 67 | updateMetadataFromValue(outputValue, "MessageId", "Message ID", res.Metadata) 68 | } 69 | -------------------------------------------------------------------------------- /wrappers/aws/aws-sdk-go/aws/sqs_factories.go: -------------------------------------------------------------------------------- 1 | package epsagonawswrapper 2 | 3 | import ( 4 | "github.com/aws/aws-sdk-go/aws/request" 5 | "github.com/epsagon/epsagon-go/protocol" 6 | "github.com/epsagon/epsagon-go/tracer" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | func sqsEventDataFactory( 13 | r *request.Request, 14 | res *protocol.Resource, 15 | metadataOnly bool, 16 | currentTracer tracer.Tracer, 17 | ) { 18 | inputValue := reflect.ValueOf(r.Params).Elem() 19 | identifyQueueName(inputValue, res) 20 | 21 | if !metadataOnly { 22 | updateMessageBody(inputValue, res) 23 | } 24 | 25 | handleSpecificOperations := map[string]specificOperationHandler{ 26 | "SendMessage": handleSQSSendMessage, 27 | "ReceiveMessage": handleSQSReceiveMessage, 28 | } 29 | handler := handleSpecificOperations[res.Operation] 30 | if handler != nil { 31 | handler(r, res, metadataOnly, currentTracer) 32 | } 33 | } 34 | 35 | func updateMessageBody(inputValue reflect.Value, res *protocol.Resource) { 36 | entry := inputValue.FieldByName("Entries") 37 | if entry == (reflect.Value{}) || entry.Len() == 0 { 38 | entry = inputValue 39 | } else { 40 | // TODO currently only records the first message 41 | entry = entry.Index(0).Elem() 42 | } 43 | updateMetadataFromValue(entry, "MessageBody", "MessageBody", res.Metadata) 44 | } 45 | 46 | func identifyQueueName(inputValue reflect.Value, res *protocol.Resource) { 47 | queueURLField := inputValue.FieldByName("QueueUrl") 48 | if queueURLField != (reflect.Value{}) { 49 | queueURL, ok := queueURLField.Elem().Interface().(string) 50 | if ok { 51 | urlParts := strings.Split(queueURL, "/") 52 | res.Name = urlParts[len(urlParts)-1] 53 | } // TODO else send exception? 54 | } else { 55 | queueNameField := inputValue.FieldByName("QueueName") 56 | if queueNameField != (reflect.Value{}) { 57 | res.Name = queueNameField.Elem().String() 58 | } 59 | } 60 | } 61 | 62 | func handleSQSSendMessage(r *request.Request, res *protocol.Resource, metadataOnly bool, _ tracer.Tracer) { 63 | outputValue := reflect.ValueOf(r.Data).Elem() 64 | updateMetadataFromValue(outputValue, "MessageId", "Message ID", res.Metadata) 65 | updateMetadataFromValue(outputValue, "MD5OfMessageBodyMessageId", 66 | "MD5 Of Message Body", res.Metadata) 67 | } 68 | 69 | func handleSQSReceiveMessage(r *request.Request, res *protocol.Resource, metadataOnly bool, _ tracer.Tracer) { 70 | var numberOfMessages int 71 | outputValue := reflect.ValueOf(r.Data).Elem() 72 | messages := outputValue.FieldByName("Messages") 73 | if messages == (reflect.Value{}) { 74 | numberOfMessages = 0 75 | } else { 76 | numberOfMessages = messages.Len() 77 | if numberOfMessages > 0 { 78 | updateMetadataFromValue(messages.Index(0).Elem(), "MessageId", "Message ID", res.Metadata) 79 | updateMetadataFromValue(messages.Index(0).Elem(), "MD5OfMessageBodyMessageId", 80 | "MD5 Of Message Body", res.Metadata) 81 | } 82 | } 83 | res.Metadata["Number Of Messages"] = strconv.Itoa(numberOfMessages) 84 | } 85 | -------------------------------------------------------------------------------- /wrappers/fiber/fiber.go: -------------------------------------------------------------------------------- 1 | package epsagonfiber 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "runtime/debug" 7 | "strconv" 8 | 9 | "github.com/epsagon/epsagon-go/epsagon" 10 | "github.com/epsagon/epsagon-go/protocol" 11 | "github.com/epsagon/epsagon-go/tracer" 12 | "github.com/gofiber/fiber/v2" 13 | "github.com/valyala/fasthttp" 14 | ) 15 | 16 | // FiberEpsagonMiddleware is an epsagon instumentation middleware for fiber apps 17 | type FiberEpsagonMiddleware struct { 18 | Config *epsagon.Config 19 | } 20 | 21 | func parseQueryArgs(args *fasthttp.Args) map[string]string { 22 | result := make(map[string]string) 23 | args.VisitAll(func(key, val []byte) { 24 | result[string(key)] = string(val) 25 | }) 26 | return result 27 | } 28 | 29 | // convert map values to string. On error, add exception to tracer with the 30 | // given error message 31 | func convertMapValuesToString( 32 | values map[string]string, 33 | wrapperTracer tracer.Tracer, 34 | errorMessage string) string { 35 | processed, err := json.Marshal(values) 36 | if err != nil { 37 | wrapperTracer.AddException(&protocol.Exception{ 38 | Type: "trigger-creation", 39 | Message: errorMessage, 40 | Traceback: string(debug.Stack()), 41 | Time: tracer.GetTimestamp(), 42 | }) 43 | return "" 44 | } 45 | return string(processed) 46 | } 47 | 48 | func processQueryFromURI(uriObj *fasthttp.URI, wrapperTracer tracer.Tracer) string { 49 | if uriObj == nil { 50 | return "" 51 | } 52 | args := parseQueryArgs(uriObj.QueryArgs()) 53 | return convertMapValuesToString( 54 | args, 55 | wrapperTracer, 56 | fmt.Sprintf("Failed to serialize query params %s", uriObj.QueryString())) 57 | 58 | } 59 | 60 | func processRequestHeaders(requestHeaders *fasthttp.RequestHeader, wrapperTracer tracer.Tracer) string { 61 | if requestHeaders == nil { 62 | return "" 63 | } 64 | headers := make(map[string]string) 65 | requestHeaders.VisitAll(func(key, val []byte) { 66 | headers[string(key)] = string(val) 67 | }) 68 | return convertMapValuesToString( 69 | headers, 70 | wrapperTracer, 71 | fmt.Sprintf("Failed to serialize request headers")) 72 | } 73 | 74 | func processResponseHeaders(responseHeaders *fasthttp.ResponseHeader, wrapperTracer tracer.Tracer) string { 75 | if responseHeaders == nil { 76 | return "" 77 | } 78 | headers := make(map[string]string) 79 | responseHeaders.VisitAll(func(key, val []byte) { 80 | headers[string(key)] = string(val) 81 | }) 82 | return convertMapValuesToString( 83 | headers, 84 | wrapperTracer, 85 | fmt.Sprintf("Failed to serialize response headers")) 86 | } 87 | 88 | // Gets the request content type header, empty string if not found 89 | func getRequestContentType(fiberCtx *fiber.Ctx) string { 90 | return string(fiberCtx.Request().Header.ContentType()) 91 | } 92 | 93 | // CreateHTTPTriggerEvent creates an HTTP trigger event 94 | func CreateHTTPTriggerEvent(wrapperTracer tracer.Tracer, fiberCtx *fiber.Ctx, resourceName string) *protocol.Event { 95 | request := fiberCtx.Request() 96 | 97 | name := resourceName 98 | if len(name) == 0 { 99 | name = fiberCtx.Hostname() 100 | } 101 | 102 | event := &protocol.Event{ 103 | Id: "", 104 | Origin: "trigger", 105 | StartTime: tracer.GetTimestamp(), 106 | Resource: &protocol.Resource{ 107 | Name: name, 108 | Type: "http", 109 | Operation: fiberCtx.Method(), 110 | Metadata: map[string]string{ 111 | "path": fiberCtx.Path(), 112 | }, 113 | }, 114 | } 115 | if !wrapperTracer.GetConfig().MetadataOnly { 116 | event.Resource.Metadata["query_string_params"] = processQueryFromURI(request.URI(), wrapperTracer) 117 | event.Resource.Metadata["request_headers"] = processRequestHeaders(&request.Header, wrapperTracer) 118 | event.Resource.Metadata["request_body"] = string(fiberCtx.Body()) 119 | } 120 | return event 121 | } 122 | 123 | func fiberHandler(c *fiber.Ctx) (err error) { 124 | err = c.Next() 125 | return err 126 | } 127 | 128 | func (middleware *FiberEpsagonMiddleware) HandlerFunc() fiber.Handler { 129 | config := middleware.Config 130 | if config == nil { 131 | config = &epsagon.Config{} 132 | } 133 | return func(c *fiber.Ctx) (err error) { 134 | if epsagon.ShouldIgnoreRequest(getRequestContentType(c), c.Path()) { 135 | return c.Next() 136 | } 137 | 138 | callingOriginalHandler := false 139 | called := false 140 | var triggerEvent *protocol.Event = nil 141 | defer func() { 142 | userError := recover() 143 | if userError == nil { 144 | return 145 | } 146 | if !callingOriginalHandler { 147 | err = c.Next() 148 | return 149 | } 150 | if !called { // panic only if error happened in original handler 151 | triggerEvent.Resource.Metadata["status_code"] = "500" 152 | panic(userError) 153 | } 154 | }() 155 | 156 | wrapperTracer := tracer.CreateTracer(&config.Config) 157 | wrapperTracer.Start() 158 | defer wrapperTracer.SendStopSignal() 159 | userContext := c.UserContext() 160 | c.SetUserContext(epsagon.ContextWithTracer(wrapperTracer, userContext)) 161 | triggerEvent = CreateHTTPTriggerEvent(wrapperTracer, c, c.Hostname()) 162 | wrapperTracer.AddEvent(triggerEvent) 163 | wrapper := epsagon.WrapGenericFunction( 164 | func(c *fiber.Ctx) error { 165 | err = c.Next() 166 | return err 167 | }, config, wrapperTracer, false, c.Path()) 168 | defer postExecutionUpdates(wrapperTracer, triggerEvent, c, wrapper) 169 | callingOriginalHandler = true 170 | wrapper.Call(c) 171 | called = true 172 | return err 173 | } 174 | } 175 | 176 | func postExecutionUpdates( 177 | wrapperTracer tracer.Tracer, triggerEvent *protocol.Event, 178 | c *fiber.Ctx, handlerWrapper *epsagon.GenericWrapper) { 179 | runner := handlerWrapper.GetRunnerEvent() 180 | if runner != nil { 181 | runner.Resource.Type = "fiber" 182 | } 183 | response := c.Response() 184 | triggerEvent.Resource.Metadata["status_code"] = strconv.Itoa(response.StatusCode()) 185 | if !wrapperTracer.GetConfig().MetadataOnly { 186 | triggerEvent.Resource.Metadata["response_headers"] = processResponseHeaders(&response.Header, wrapperTracer) 187 | triggerEvent.Resource.Metadata["response_body"] = string(response.Body()) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /wrappers/gin/gin.go: -------------------------------------------------------------------------------- 1 | package epsagongin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/epsagon/epsagon-go/epsagon" 9 | "github.com/epsagon/epsagon-go/protocol" 10 | "github.com/epsagon/epsagon-go/tracer" 11 | epsagonhttp "github.com/epsagon/epsagon-go/wrappers/net/http" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | // TracerKey is the key of the epsagon tracer in the gin.Context Keys map passed to the handlers 16 | const TracerKey = "EpsagonTracer" 17 | 18 | // EpsagonContext creates a context.Background() with epsagon's associated tracer for nexted instrumentations 19 | func EpsagonContext(c *gin.Context) context.Context { 20 | return epsagon.ContextWithTracer(c.Keys[TracerKey].(tracer.Tracer)) 21 | } 22 | 23 | // GinRouterWrapper is an epsagon instumentation wrapper for gin.RouterGroup 24 | type GinRouterWrapper struct { 25 | gin.IRouter 26 | Hostname string 27 | Config *epsagon.Config 28 | } 29 | 30 | type wrappedGinWriter struct { 31 | gin.ResponseWriter 32 | htrw http.ResponseWriter 33 | } 34 | 35 | func wrapGinWriter(c *gin.Context, triggerEvent *protocol.Event) { 36 | wrappedResponseWriter := &wrappedGinWriter{ 37 | ResponseWriter: c.Writer, 38 | htrw: epsagonhttp.CreateWrappedResponseWriter(c.Writer, triggerEvent.Resource), 39 | } 40 | c.Writer = wrappedResponseWriter 41 | } 42 | 43 | func postExecutionUpdates( 44 | wrapperTracer tracer.Tracer, triggerEvent *protocol.Event, 45 | c *gin.Context, handlerWrapper *epsagon.GenericWrapper) { 46 | runner := handlerWrapper.GetRunnerEvent() 47 | if runner != nil { 48 | runner.Resource.Type = "gin" 49 | } 50 | wrappedResponseWriter, ok := c.Writer.(*wrappedGinWriter) 51 | if ok { 52 | wrappedResponseWriter.htrw.(*epsagonhttp.WrappedResponseWriter).UpdateResource() 53 | } 54 | userError := recover() 55 | if userError != nil { 56 | triggerEvent.Resource.Metadata["status_code"] = "500" 57 | panic(userError) 58 | } 59 | } 60 | 61 | func wrapGinHandler(handler gin.HandlerFunc, hostname string, relativePath string, config *epsagon.Config) gin.HandlerFunc { 62 | if config == nil { 63 | config = &epsagon.Config{} 64 | } 65 | return func(c *gin.Context) { 66 | wrapperTracer := tracer.CreateTracer(&config.Config) 67 | wrapperTracer.Start() 68 | defer wrapperTracer.SendStopSignal() 69 | 70 | c.Set(TracerKey, wrapperTracer) 71 | wrapper := epsagon.WrapGenericFunction( 72 | handler, config, wrapperTracer, false, relativePath, 73 | ) 74 | triggerEvent := epsagonhttp.CreateHTTPTriggerEvent( 75 | wrapperTracer, c.Request, hostname) 76 | wrapperTracer.AddEvent(triggerEvent) 77 | if !config.MetadataOnly { 78 | wrapGinWriter(c, triggerEvent) 79 | } 80 | defer postExecutionUpdates(wrapperTracer, triggerEvent, c, wrapper) 81 | wrapper.Call(c) 82 | triggerEvent.Resource.Metadata["status_code"] = fmt.Sprint(c.Writer.Status()) 83 | } 84 | } 85 | 86 | // Handle is a wrapper for gin.RouterGroup.Handle that adds epsagon instrumentaiton and event triggers 87 | // to all invocations of that handler 88 | func (router *GinRouterWrapper) Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { 89 | if len(handlers) >= 1 { 90 | handlers[0] = wrapGinHandler(handlers[0], router.Hostname, relativePath, router.Config) 91 | } 92 | return router.IRouter.Handle(httpMethod, relativePath, handlers...) 93 | } 94 | 95 | // POST is a shortcut for router.Handle("POST", path, handle). 96 | func (router *GinRouterWrapper) POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { 97 | return router.Handle(http.MethodPost, relativePath, handlers...) 98 | } 99 | 100 | // GET is a shortcut for router.Handle("GET", path, handle). 101 | func (router *GinRouterWrapper) GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { 102 | return router.Handle(http.MethodGet, relativePath, handlers...) 103 | } 104 | 105 | // DELETE is a shortcut for router.Handle("DELETE", path, handle). 106 | func (router *GinRouterWrapper) DELETE(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { 107 | return router.Handle(http.MethodDelete, relativePath, handlers...) 108 | } 109 | 110 | // PATCH is a shortcut for router.Handle("PATCH", path, handle). 111 | func (router *GinRouterWrapper) PATCH(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { 112 | return router.Handle(http.MethodPatch, relativePath, handlers...) 113 | } 114 | 115 | // PUT is a shortcut for router.Handle("PUT", path, handle). 116 | func (router *GinRouterWrapper) PUT(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { 117 | return router.Handle(http.MethodPut, relativePath, handlers...) 118 | } 119 | 120 | // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle). 121 | func (router *GinRouterWrapper) OPTIONS(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { 122 | return router.Handle(http.MethodOptions, relativePath, handlers...) 123 | } 124 | 125 | // HEAD is a shortcut for router.Handle("HEAD", path, handle). 126 | func (router *GinRouterWrapper) HEAD(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes { 127 | return router.Handle(http.MethodHead, relativePath, handlers...) 128 | } 129 | 130 | // Run is a shortcut for router.IRouter.(*gin.Engine).Run() 131 | func (router *GinRouterWrapper) Run(addr ...string) error { 132 | engine, ok := router.IRouter.(*gin.Engine) 133 | 134 | if !ok { 135 | return fmt.Errorf("Could not start Gin engine") 136 | } 137 | 138 | return engine.Run(addr...) 139 | } 140 | 141 | func (grw *wrappedGinWriter) Header() http.Header { 142 | return grw.htrw.Header() 143 | } 144 | 145 | func (grw *wrappedGinWriter) Write(data []byte) (int, error) { 146 | return grw.htrw.Write(data) 147 | } 148 | 149 | func (grw *wrappedGinWriter) WriteHeader(statusCode int) { 150 | grw.htrw.WriteHeader(statusCode) 151 | } 152 | -------------------------------------------------------------------------------- /wrappers/mongo/common_utils.go: -------------------------------------------------------------------------------- 1 | package epsagonmongo 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/epsagon/epsagon-go/epsagon" 8 | "github.com/epsagon/epsagon-go/protocol" 9 | "github.com/epsagon/epsagon-go/tracer" 10 | "github.com/google/uuid" 11 | "go.mongodb.org/mongo-driver/mongo" 12 | "strconv" 13 | 14 | "reflect" 15 | "runtime" 16 | "strings" 17 | ) 18 | 19 | // currentFuncName returns the name of the caller function as a string 20 | // for func Foo() { x := currentFuncName() }, x == "Foo" 21 | func currentFuncName() string { 22 | current := make([]uintptr, 1) 23 | if level := runtime.Callers(2, current); level == 0 { 24 | return "" 25 | } 26 | caller := runtime.FuncForPC(current[0] - 1) 27 | if caller == nil { 28 | return "" 29 | } 30 | sysFuncName := caller.Name() 31 | return partitionByDelimiterAtIndex(sysFuncName, ".", -1) 32 | 33 | } 34 | 35 | func startMongoEvent(opName string, coll *MongoCollectionWrapper) *protocol.Event { 36 | defer epsagon.GeneralEpsagonRecover("mongo-driver", currentFuncName(), coll.tracer) 37 | return &protocol.Event{ 38 | Id: "mongodb-" + uuid.New().String(), 39 | Origin: "mongodb", 40 | ErrorCode: protocol.ErrorCode_OK, 41 | StartTime: tracer.GetTimestamp(), 42 | Resource: createMongoResource(opName, coll), 43 | } 44 | } 45 | 46 | func completeMongoEvent(currentTracer tracer.Tracer, event *protocol.Event) { 47 | defer epsagon.GeneralEpsagonRecover("mongo-driver", currentFuncName(), currentTracer) 48 | event.Duration = tracer.GetTimestamp() - event.StartTime 49 | currentTracer.AddEvent(event) 50 | 51 | } 52 | 53 | func createMongoResource(opName string, coll *MongoCollectionWrapper) *protocol.Resource { 54 | defer epsagon.GeneralEpsagonRecover("mongo-driver", currentFuncName(), coll.tracer) 55 | return &protocol.Resource{ 56 | Name: coll.Database().Name() + "." + coll.collection.Name(), 57 | Type: "mongodb", 58 | Operation: opName, 59 | Metadata: make(map[string]string), 60 | } 61 | } 62 | 63 | // extractStructFields parses the fields from a struct and adds it to metadata under field name metaField 64 | // attempts to convert to int if possible, else keeps as a string 65 | func extractStructFields( 66 | metadata map[string]string, 67 | metaField string, 68 | s interface{}, 69 | ) { 70 | val := reflect.ValueOf(s) 71 | if val.Kind() == reflect.Ptr { 72 | val = val.Elem() 73 | } 74 | valuesMap := make(map[string]string, val.NumField()) 75 | 76 | for i := 0; i < val.NumField(); i++ { 77 | if val.Field(i).CanInterface() { 78 | fieldVal := val.Field(i) 79 | v := fmt.Sprintf("%q", fieldVal) 80 | if _, err := strconv.Atoi(v); err == nil { 81 | v = strconv.FormatInt(fieldVal.Int(), 10) 82 | } 83 | valuesMap[val.Type().Field(i).Name] = v 84 | } 85 | } 86 | doc, _ := json.Marshal(valuesMap) 87 | metadata[metaField] = string(doc) 88 | } 89 | 90 | // marshalToMetadata marshals any object to JSON and adds it as a string to metadata under metaFielf 91 | func marshalToMetadata( 92 | metadata map[string]string, 93 | metaField string, 94 | s interface{}, 95 | config *tracer.Config, 96 | ) { 97 | docBytes, err := json.Marshal(s) 98 | if err != nil { 99 | epsagon.DebugLog(config.Debug, "Could not Marshal JSON", err) 100 | } 101 | docString := string(docBytes) 102 | if docString == "" { 103 | return 104 | } 105 | metadata[metaField] = docString 106 | } 107 | 108 | // readCursor accepts a cursor and returns a slice of maps 109 | // each map represents a mongo document 110 | func readCursor(cursor *mongo.Cursor) ([]map[string]string, error) { 111 | var documents []map[string]string 112 | err := cursor.All(context.Background(), &documents) 113 | return documents, err 114 | } 115 | 116 | func logOperationFailure(messages ...string) { 117 | for _, m := range messages { 118 | epsagon.DebugLog(true, "[MONGO]", m) 119 | } 120 | } 121 | 122 | // partition a string by delimiter and return the partitioned at the index 123 | func partitionByDelimiterAtIndex(original, delimiter string, index int) string { 124 | s := strings.Split(original, delimiter) 125 | i := moduloFloor(len(s), index) 126 | return s[i] 127 | 128 | } 129 | 130 | // flooring an index by size 131 | // useful for wrapping around negative indices to positive 132 | func moduloFloor(size, index int) int { 133 | return (index + size) % size 134 | } 135 | -------------------------------------------------------------------------------- /wrappers/mongo/mongo_test.go: -------------------------------------------------------------------------------- 1 | package epsagonmongo 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/benweissmann/memongo" 9 | "github.com/epsagon/epsagon-go/epsagon" 10 | "github.com/epsagon/epsagon-go/protocol" 11 | "github.com/epsagon/epsagon-go/tracer" 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | "go.mongodb.org/mongo-driver/bson" 15 | "go.mongodb.org/mongo-driver/mongo" 16 | "go.mongodb.org/mongo-driver/mongo/options" 17 | ) 18 | 19 | func TestMongoWrapper(t *testing.T) { 20 | RegisterFailHandler(Fail) 21 | RunSpecs(t, "Mongo Driver Test Suite") 22 | } 23 | 24 | var _ = Describe("mongo_wrapper", func() { 25 | Describe("CollectionWrapper", func() { 26 | var ( 27 | mongoServer *memongo.Server 28 | mongoOptions *memongo.Options 29 | started chan bool 30 | testConf *epsagon.Config 31 | events []*protocol.Event 32 | exceptions []*protocol.Exception 33 | wrapper *MongoCollectionWrapper 34 | testContext context.Context 35 | testDatabaseName string 36 | testCollectionName string 37 | cancel func() 38 | ) 39 | BeforeEach(func() { 40 | started = make(chan bool) 41 | // start server goroutine, runs in background until block 42 | go func() { 43 | mongoOptions = &memongo.Options{ 44 | MongoVersion: "4.2.0", 45 | StartupTimeout: 5 * time.Second, 46 | } 47 | mongoServer, _ = memongo.StartWithOptions(mongoOptions) 48 | started <- true 49 | }() 50 | 51 | testConf = &epsagon.Config{Config: tracer.Config{ 52 | Disable: true, 53 | TestMode: true, 54 | }} 55 | events = make([]*protocol.Event, 0) 56 | exceptions = make([]*protocol.Exception, 0) 57 | tracer.GlobalTracer = &tracer.MockedEpsagonTracer{ 58 | Events: &events, 59 | Exceptions: &exceptions, 60 | Labels: make(map[string]interface{}), 61 | Config: &testConf.Config, 62 | } 63 | 64 | testCollectionName = "collectionName" 65 | testDatabaseName = "databaseName" 66 | testContext, cancel = context.WithTimeout(context.Background(), 2*time.Second) 67 | 68 | // blocking await until server is started 69 | select { 70 | case <-started: 71 | break 72 | } 73 | client, _ := mongo.Connect(testContext, options.Client().ApplyURI(mongoServer.URI())) 74 | wrapper = &MongoCollectionWrapper{ 75 | collection: client.Database(testDatabaseName).Collection(testCollectionName), 76 | tracer: tracer.GlobalTracer, 77 | } 78 | }) 79 | AfterEach(func() { 80 | mongoServer.Stop() 81 | cancel() 82 | }) 83 | Context("Writing DB", func() { 84 | It("calls InsertOne", func() { 85 | _, err := wrapper.InsertOne(context.Background(), struct { 86 | Name string 87 | }{"TestName"}) 88 | Expect(err).To(BeNil()) 89 | }) 90 | It("calls InsertMany", func() { 91 | _, err := wrapper.InsertMany(context.Background(), []interface{}{ 92 | bson.D{ 93 | {Key: "name", Value: "hello"}, 94 | {Key: "age", Value: "33"}, 95 | }, 96 | bson.D{ 97 | {Key: "name", Value: "world"}, 98 | {Key: "age", Value: "44"}, 99 | }, 100 | }) 101 | Expect(err).To(BeNil()) 102 | }) 103 | }) 104 | Context("Reading DB", func() { 105 | It("calls InsertOne and FindOne", func() { 106 | type doc struct { 107 | Name string 108 | } 109 | reqDoc := doc{Name: "TestName"} 110 | resDoc := doc{} 111 | 112 | _, err := wrapper.InsertOne(context.Background(), reqDoc) 113 | Expect(err).To(BeNil()) 114 | 115 | response := wrapper.FindOne( 116 | context.Background(), 117 | bson.D{{Key: "name", Value: "TestName"}}, 118 | ) 119 | 120 | response.Decode(&resDoc) 121 | Expect(reqDoc).To(Equal(resDoc)) 122 | Expect(response.Err()).To(BeNil()) 123 | 124 | }) 125 | It("calls InsertMany and Find", func() { 126 | type doc struct { 127 | Name string 128 | } 129 | docs := []interface{}{ 130 | bson.D{ 131 | {Key: "name", Value: "hello"}, 132 | {Key: "age", Value: "33"}, 133 | }, 134 | bson.D{ 135 | {Key: "name", Value: "world"}, 136 | {Key: "age", Value: "44"}, 137 | }, 138 | } 139 | 140 | _, err := wrapper.InsertMany(context.Background(), docs) 141 | Expect(err).To(BeNil()) 142 | 143 | cur, err := wrapper.Find( 144 | context.Background(), 145 | bson.M{}, 146 | ) 147 | Expect(err).To(BeNil()) 148 | 149 | 150 | readCursor(cur) 151 | }) 152 | It("calls CountDocuments", func() { 153 | docs := []interface{}{ 154 | bson.D{ 155 | {Key: "name", Value: "hello"}, 156 | {Key: "age", Value: "33"}, 157 | }, 158 | bson.D{ 159 | {Key: "name", Value: "world"}, 160 | {Key: "age", Value: "44"}, 161 | }, 162 | } 163 | 164 | wrapper.InsertMany(context.Background(), docs) 165 | res, err := wrapper.CountDocuments( 166 | context.Background(), 167 | bson.D{{}}, 168 | ) 169 | Expect(err).To(BeNil()) 170 | Expect(res).To(Equal(int64(2))) 171 | }) 172 | }) 173 | }) 174 | }) 175 | -------------------------------------------------------------------------------- /wrappers/net/http/handler_wrapper.go: -------------------------------------------------------------------------------- 1 | package epsagonhttp 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "runtime/debug" 9 | 10 | "github.com/epsagon/epsagon-go/epsagon" 11 | "github.com/epsagon/epsagon-go/protocol" 12 | "github.com/epsagon/epsagon-go/tracer" 13 | ) 14 | 15 | // HandlerFunction is a generic http handler function 16 | type HandlerFunction func(http.ResponseWriter, *http.Request) 17 | 18 | func processRawQuery(urlObj *url.URL, wrapperTracer tracer.Tracer) string { 19 | if urlObj == nil { 20 | return "" 21 | } 22 | processed, err := json.Marshal(urlObj.Query()) 23 | if err != nil { 24 | wrapperTracer.AddException(&protocol.Exception{ 25 | Type: "trigger-creation", 26 | Message: fmt.Sprintf("Failed to serialize query params %s", urlObj.RawQuery), 27 | Traceback: string(debug.Stack()), 28 | Time: tracer.GetTimestamp(), 29 | }) 30 | return "" 31 | } 32 | return string(processed) 33 | } 34 | 35 | // CreateHTTPTriggerEvent creates an HTTP trigger event 36 | func CreateHTTPTriggerEvent(wrapperTracer tracer.Tracer, request *http.Request, resourceName string) *protocol.Event { 37 | name := resourceName 38 | if len(name) == 0 { 39 | name = request.Host 40 | } 41 | event := &protocol.Event{ 42 | Id: "", 43 | Origin: "trigger", 44 | StartTime: tracer.GetTimestamp(), 45 | Resource: &protocol.Resource{ 46 | Name: name, 47 | Type: "http", 48 | Operation: request.Method, 49 | Metadata: map[string]string{ 50 | "path": request.URL.Path, 51 | }, 52 | }, 53 | } 54 | if !wrapperTracer.GetConfig().MetadataOnly { 55 | event.Resource.Metadata["query_string_parameters"] = processRawQuery( 56 | request.URL, wrapperTracer) 57 | headers, body := epsagon.ExtractRequestData(request) 58 | event.Resource.Metadata["request_headers"] = headers 59 | event.Resource.Metadata["request_body"] = body 60 | } 61 | return event 62 | } 63 | 64 | // WrapHandleFunc wraps a generic http.HandleFunc handler function with Epsagon 65 | // Last two optional paramerts are the name of the handler (will be the resource name of the events) and an optional hardcoded hostname 66 | func WrapHandleFunc( 67 | config *epsagon.Config, handler HandlerFunction, names ...string) HandlerFunction { 68 | var hostName, handlerName string 69 | if config == nil { 70 | config = &epsagon.Config{} 71 | } 72 | if len(names) >= 1 { 73 | handlerName = names[0] 74 | } 75 | if len(names) >= 2 { 76 | hostName = names[1] 77 | } 78 | return func(rw http.ResponseWriter, request *http.Request) { 79 | wrapperTracer := tracer.CreateTracer(&config.Config) 80 | wrapperTracer.Start() 81 | defer wrapperTracer.Stop() 82 | 83 | triggerEvent := CreateHTTPTriggerEvent( 84 | wrapperTracer, request, hostName) 85 | wrapperTracer.AddEvent(triggerEvent) 86 | triggerEvent.Resource.Metadata["status_code"] = "200" 87 | defer func() { 88 | if userError := recover(); userError != nil { 89 | triggerEvent.Resource.Metadata["status_code"] = "500" 90 | panic(userError) 91 | } 92 | }() 93 | 94 | newRequest := request.WithContext( 95 | epsagon.ContextWithTracer(wrapperTracer, request.Context())) 96 | 97 | if !config.MetadataOnly { 98 | rw = &WrappedResponseWriter{ 99 | ResponseWriter: rw, 100 | resource: triggerEvent.Resource, 101 | } 102 | } 103 | defer func() { 104 | if wrappedResponseWriter, ok := rw.(*WrappedResponseWriter); ok { 105 | wrappedResponseWriter.UpdateResource() 106 | } 107 | }() 108 | 109 | wrapper := epsagon.WrapGenericFunction( 110 | handler, config, wrapperTracer, false, handlerName, 111 | ) 112 | wrapper.Call(rw, newRequest) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /wrappers/net/http/response_writer.go: -------------------------------------------------------------------------------- 1 | package epsagonhttp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/epsagon/epsagon-go/epsagon" 9 | "github.com/epsagon/epsagon-go/protocol" 10 | ) 11 | 12 | // WrappedResponseWriter is wrapping Resposne writer with Epsagon 13 | // to enrich the trace with data from the response 14 | type WrappedResponseWriter struct { 15 | http.ResponseWriter 16 | resource *protocol.Resource 17 | buf bytes.Buffer 18 | } 19 | 20 | // CreateWrappedResponseWriter creates a newWrappedResponseWriter 21 | func CreateWrappedResponseWriter(rw http.ResponseWriter, resource *protocol.Resource) *WrappedResponseWriter { 22 | return &WrappedResponseWriter{ 23 | ResponseWriter: rw, 24 | resource: resource, 25 | buf: bytes.Buffer{}, 26 | } 27 | } 28 | 29 | // Header wrapper 30 | func (w *WrappedResponseWriter) Header() http.Header { 31 | return w.ResponseWriter.Header() 32 | } 33 | 34 | // WriteHeader wrapper, will set status_code immediately 35 | func (w *WrappedResponseWriter) WriteHeader(statusCode int) { 36 | w.resource.Metadata["status_code"] = fmt.Sprint(statusCode) 37 | w.ResponseWriter.WriteHeader(statusCode) 38 | } 39 | 40 | // Write wrapper 41 | func (w *WrappedResponseWriter) Write(data []byte) (int, error) { 42 | w.buf.Write(data) 43 | return w.ResponseWriter.Write(data) 44 | } 45 | 46 | // UpdateResource updates the connected resource with the response headers and body 47 | func (w *WrappedResponseWriter) UpdateResource() { 48 | w.resource.Metadata["response_headers"], _ = epsagon.FormatHeaders(w.ResponseWriter.Header()) 49 | w.resource.Metadata["response_body"] = w.buf.String() 50 | } 51 | -------------------------------------------------------------------------------- /wrappers/redis/redis.go: -------------------------------------------------------------------------------- 1 | package epsagonredis 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "net" 8 | "runtime/debug" 9 | 10 | "github.com/epsagon/epsagon-go/epsagon" 11 | "github.com/epsagon/epsagon-go/protocol" 12 | "github.com/epsagon/epsagon-go/tracer" 13 | "github.com/go-redis/redis/v8" 14 | "github.com/google/uuid" 15 | ) 16 | 17 | type epsagonHook struct { 18 | host string 19 | port string 20 | dbIndex string 21 | event *protocol.Event 22 | tracer tracer.Tracer 23 | } 24 | 25 | func NewClient(opt *redis.Options, epsagonCtx context.Context) *redis.Client { 26 | client := redis.NewClient(opt) 27 | return wrapClient(client, opt, epsagonCtx) 28 | } 29 | 30 | func wrapClient(client *redis.Client, opt *redis.Options, epsagonCtx context.Context) (wrappedClient *redis.Client) { 31 | wrappedClient = client 32 | defer func() { recover() }() 33 | 34 | currentTracer := epsagon.ExtractTracer([]context.Context{epsagonCtx}) 35 | if currentTracer != nil { 36 | host, port := getClientHostPort(opt) 37 | client.AddHook(&epsagonHook{ 38 | host: host, 39 | port: port, 40 | dbIndex: fmt.Sprint(opt.DB), 41 | tracer: currentTracer, 42 | }) 43 | } 44 | return wrappedClient 45 | } 46 | 47 | func getClientHostPort(opt *redis.Options) (host, port string) { 48 | if opt.Network == "unix" { 49 | return "localhost", opt.Addr 50 | } 51 | host, port, err := net.SplitHostPort(opt.Addr) 52 | if err == nil { 53 | return host, port 54 | } 55 | return opt.Addr, "" 56 | } 57 | 58 | func (epsHook *epsagonHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (processCtx context.Context, err error) { 59 | processCtx, err = ctx, nil 60 | defer func() { recover() }() 61 | 62 | cmdArgs := safeJsonify(cmd.Args()) 63 | epsHook.before(cmd.Name(), cmdArgs) 64 | return 65 | } 66 | 67 | func (epsHook *epsagonHook) AfterProcess(ctx context.Context, cmd redis.Cmder) (err error) { 68 | defer func() { recover() }() 69 | 70 | var errMsg string 71 | if err := cmd.Err(); err != nil { 72 | errMsg = err.Error() 73 | } 74 | epsHook.after(cmd.String(), errMsg) 75 | return 76 | } 77 | 78 | func (epsHook *epsagonHook) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (processCtx context.Context, err error) { 79 | processCtx, err = ctx, nil 80 | defer func() { recover() }() 81 | 82 | cmdArgs := safeJsonify(getPiplineCmdArgs(cmds)) 83 | epsHook.before("Pipeline", cmdArgs) 84 | return 85 | } 86 | 87 | func (epsHook *epsagonHook) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) (err error) { 88 | defer func() { recover() }() 89 | 90 | var errMsg string 91 | if errors := getPipelineErrors(cmds); len(errors) > 0 { 92 | errMsg = safeJsonify(errors) 93 | 94 | } 95 | response := safeJsonify(getPipelineResponse(cmds)) 96 | epsHook.after(response, errMsg) 97 | return 98 | } 99 | 100 | func (epsHook *epsagonHook) before(operation, cmdArgs string) { 101 | metadata := getResourceMetadata(epsHook, cmdArgs) 102 | epsHook.event = createEvent(epsHook, operation, metadata) 103 | } 104 | 105 | func (epsHook *epsagonHook) after(response, errMsg string) { 106 | event := epsHook.event 107 | if event == nil { 108 | return 109 | } 110 | if !epsHook.tracer.GetConfig().MetadataOnly { 111 | event.Resource.Metadata["redis.response"] = response 112 | } 113 | 114 | eventEndTime := tracer.GetTimestamp() 115 | event.Duration = eventEndTime - event.StartTime 116 | 117 | if errMsg != "" { 118 | event.ErrorCode = protocol.ErrorCode_EXCEPTION 119 | event.Exception = &protocol.Exception{ 120 | Message: errMsg, 121 | Traceback: string(debug.Stack()), 122 | Time: eventEndTime, 123 | } 124 | } 125 | 126 | epsHook.tracer.AddEvent(event) 127 | } 128 | 129 | func createEvent(epsHook *epsagonHook, operation string, metadata map[string]string) *protocol.Event { 130 | return &protocol.Event{ 131 | Id: "redis-" + uuid.New().String(), 132 | StartTime: tracer.GetTimestamp(), 133 | Resource: &protocol.Resource{ 134 | Name: epsHook.host, 135 | Type: "redis", 136 | Operation: operation, 137 | Metadata: metadata, 138 | }, 139 | } 140 | } 141 | 142 | func getResourceMetadata(epsHook *epsagonHook, cmdArgs string) map[string]string { 143 | metadata := getConnectionMetadata(epsHook) 144 | if !epsHook.tracer.GetConfig().MetadataOnly { 145 | metadata["Command Arguments"] = cmdArgs 146 | } 147 | return metadata 148 | } 149 | 150 | func getConnectionMetadata(epsHook *epsagonHook) map[string]string { 151 | return map[string]string{ 152 | "Redis Host": epsHook.host, 153 | "Redis Port": epsHook.port, 154 | "Redis DB Index": epsHook.dbIndex, 155 | } 156 | } 157 | 158 | func getPiplineCmdArgs(cmds []redis.Cmder) []interface{} { 159 | var cmdArgs []interface{} 160 | for _, cmd := range cmds { 161 | cmdArgs = append(cmdArgs, cmd.Args()) 162 | } 163 | return cmdArgs 164 | } 165 | 166 | func getPipelineResponse(cmds []redis.Cmder) []string { 167 | var responses []string 168 | for _, cmd := range cmds { 169 | responses = append(responses, cmd.String()) 170 | } 171 | return responses 172 | } 173 | 174 | func getPipelineErrors(cmds []redis.Cmder) []string { 175 | var errors []string 176 | for _, cmd := range cmds { 177 | if err := cmd.Err(); err != nil { 178 | errors = append(errors, err.Error()) 179 | } 180 | } 181 | return errors 182 | } 183 | 184 | func safeJsonify(v interface{}) string { 185 | encodedValue, err := json.Marshal(v) 186 | if err == nil { 187 | return string(encodedValue) 188 | } 189 | return "" 190 | } 191 | --------------------------------------------------------------------------------