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  }