github.com/newrelic/go-agent@v3.26.0+incompatible/internal/serverless.go (about) 1 // Copyright 2020 New Relic Corporation. All rights reserved. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package internal 5 6 import ( 7 "bytes" 8 "compress/gzip" 9 "encoding/base64" 10 "encoding/json" 11 "fmt" 12 "io" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/newrelic/go-agent/internal/logger" 18 ) 19 20 const ( 21 lambdaMetadataVersion = 2 22 23 // AgentLanguage is used in the connect JSON and the Lambda JSON. 24 AgentLanguage = "go" 25 ) 26 27 // ServerlessHarvest is used to store and log data when the agent is running in 28 // serverless mode. 29 type ServerlessHarvest struct { 30 logger logger.Logger 31 version string 32 awsExecutionEnv string 33 34 // The Lambda handler could be using multiple goroutines so we use a 35 // mutex to prevent race conditions. 36 sync.Mutex 37 harvest *Harvest 38 } 39 40 // NewServerlessHarvest creates a new ServerlessHarvest. 41 func NewServerlessHarvest(logger logger.Logger, version string, getEnv func(string) string) *ServerlessHarvest { 42 return &ServerlessHarvest{ 43 logger: logger, 44 version: version, 45 awsExecutionEnv: getEnv("AWS_EXECUTION_ENV"), 46 47 // We can use a default HarvestConfigured parameter because 48 // serverless mode doesn't have a connect, and therefore won't 49 // have custom event limits from the server. 50 harvest: NewHarvest(time.Now(), &DfltHarvestCfgr{}), 51 } 52 } 53 54 // Consume adds data to the harvest. 55 func (sh *ServerlessHarvest) Consume(data Harvestable) { 56 if nil == sh { 57 return 58 } 59 sh.Lock() 60 defer sh.Unlock() 61 62 data.MergeIntoHarvest(sh.harvest) 63 } 64 65 func (sh *ServerlessHarvest) swapHarvest() *Harvest { 66 sh.Lock() 67 defer sh.Unlock() 68 69 h := sh.harvest 70 sh.harvest = NewHarvest(time.Now(), &DfltHarvestCfgr{}) 71 return h 72 } 73 74 // Write logs the data in the format described by: 75 // https://source.datanerd.us/agents/agent-specs/blob/master/Lambda.md 76 func (sh *ServerlessHarvest) Write(arn string, writer io.Writer) { 77 if nil == sh { 78 return 79 } 80 harvest := sh.swapHarvest() 81 payloads := harvest.Payloads(false) 82 // Note that *json.RawMessage (instead of json.RawMessage) is used to 83 // support older Go versions: https://go-review.googlesource.com/c/go/+/21811/ 84 harvestPayloads := make(map[string]*json.RawMessage, len(payloads)) 85 for _, p := range payloads { 86 agentRunID := "" 87 cmd := p.EndpointMethod() 88 data, err := p.Data(agentRunID, time.Now()) 89 if err != nil { 90 sh.logger.Error("error creating payload json", map[string]interface{}{ 91 "command": cmd, 92 "error": err.Error(), 93 }) 94 continue 95 } 96 if nil == data { 97 continue 98 } 99 // NOTE! This code relies on the fact that each payload is 100 // using a different endpoint method. Sometimes the transaction 101 // events payload might be split, but since there is only one 102 // transaction event per serverless transaction, that's not an 103 // issue. Likewise, if we ever split normal transaction events 104 // apart from synthetics events, the transaction will either be 105 // normal or synthetic, so that won't be an issue. Log an error 106 // if this happens for future defensiveness. 107 if _, ok := harvestPayloads[cmd]; ok { 108 sh.logger.Error("data with duplicate command name lost", map[string]interface{}{ 109 "command": cmd, 110 }) 111 } 112 d := json.RawMessage(data) 113 harvestPayloads[cmd] = &d 114 } 115 116 if len(harvestPayloads) == 0 { 117 // The harvest may not contain any data if the serverless 118 // transaction was ignored. 119 return 120 } 121 122 data, err := json.Marshal(harvestPayloads) 123 if nil != err { 124 sh.logger.Error("error creating serverless data json", map[string]interface{}{ 125 "error": err.Error(), 126 }) 127 return 128 } 129 130 var dataBuf bytes.Buffer 131 gz := gzip.NewWriter(&dataBuf) 132 gz.Write(data) 133 gz.Flush() 134 gz.Close() 135 136 js, err := json.Marshal([]interface{}{ 137 lambdaMetadataVersion, 138 "NR_LAMBDA_MONITORING", 139 struct { 140 MetadataVersion int `json:"metadata_version"` 141 ARN string `json:"arn,omitempty"` 142 ProtocolVersion int `json:"protocol_version"` 143 ExecutionEnvironment string `json:"execution_environment,omitempty"` 144 AgentVersion string `json:"agent_version"` 145 AgentLanguage string `json:"agent_language"` 146 }{ 147 MetadataVersion: lambdaMetadataVersion, 148 ProtocolVersion: ProcotolVersion, 149 AgentVersion: sh.version, 150 ExecutionEnvironment: sh.awsExecutionEnv, 151 ARN: arn, 152 AgentLanguage: AgentLanguage, 153 }, 154 base64.StdEncoding.EncodeToString(dataBuf.Bytes()), 155 }) 156 157 if err != nil { 158 sh.logger.Error("error creating serverless json", map[string]interface{}{ 159 "error": err.Error(), 160 }) 161 return 162 } 163 164 fmt.Fprintln(writer, string(js)) 165 } 166 167 // ParseServerlessPayload exists for testing. 168 func ParseServerlessPayload(data []byte) (metadata, uncompressedData map[string]json.RawMessage, err error) { 169 var arr [4]json.RawMessage 170 if err = json.Unmarshal(data, &arr); nil != err { 171 err = fmt.Errorf("unable to unmarshal serverless data array: %v", err) 172 return 173 } 174 var dataJSON []byte 175 compressed := strings.Trim(string(arr[3]), `"`) 176 if dataJSON, err = decodeUncompress(compressed); nil != err { 177 err = fmt.Errorf("unable to uncompress serverless data: %v", err) 178 return 179 } 180 if err = json.Unmarshal(dataJSON, &uncompressedData); nil != err { 181 err = fmt.Errorf("unable to unmarshal uncompressed serverless data: %v", err) 182 return 183 } 184 if err = json.Unmarshal(arr[2], &metadata); nil != err { 185 err = fmt.Errorf("unable to unmarshal serverless metadata: %v", err) 186 return 187 } 188 return 189 } 190 191 func decodeUncompress(input string) ([]byte, error) { 192 decoded, err := base64.StdEncoding.DecodeString(input) 193 if nil != err { 194 return nil, err 195 } 196 197 buf := bytes.NewBuffer(decoded) 198 gz, err := gzip.NewReader(buf) 199 if nil != err { 200 return nil, err 201 } 202 var out bytes.Buffer 203 io.Copy(&out, gz) 204 gz.Close() 205 206 return out.Bytes(), nil 207 } 208 209 // ServerlessWriter is implemented by newrelic.Application. 210 type ServerlessWriter interface { 211 ServerlessWrite(arn string, writer io.Writer) 212 } 213 214 // ServerlessWrite exists to avoid type assertion in the nrlambda integration 215 // package. 216 func ServerlessWrite(app interface{}, arn string, writer io.Writer) { 217 if s, ok := app.(ServerlessWriter); ok { 218 s.ServerlessWrite(arn, writer) 219 } 220 }