github.com/klaytn/klaytn@v1.12.1/networks/rpc/http_newrelic.go (about)

     1  package rpc
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"io"
     7  	"net/http"
     8  	"os"
     9  
    10  	"github.com/newrelic/go-agent/v3/newrelic"
    11  )
    12  
    13  // dupWriter writes data to the buffer as well as http response
    14  type dupWriter struct {
    15  	http.ResponseWriter
    16  	body *bytes.Buffer
    17  }
    18  
    19  func (w dupWriter) Write(b []byte) (int, error) {
    20  	w.body.Write(b)
    21  	return w.ResponseWriter.Write(b)
    22  }
    23  
    24  // KASAttrs contains identifications for a KAS request
    25  type KASAttrs struct {
    26  	ChainID      string `json:"x-chain-id"`
    27  	AccountID    string `json:"x-account-id"`
    28  	RequestID    string `json:"x-request-id"`
    29  	ParentSpanID string `json:"x-b3-parentspanid,omitempty"`
    30  	SpanID       string `json:"x-b3-spanid,omitempty"`
    31  	TraceID      string `json:"x-b3-traceid,omitempty"`
    32  }
    33  
    34  func parseKASHeader(r *http.Request) KASAttrs {
    35  	return KASAttrs{
    36  		ChainID:      r.Header.Get("x-chain-id"),
    37  		AccountID:    r.Header.Get("x-account-id"),
    38  		RequestID:    r.Header.Get("x-request-id"),
    39  		ParentSpanID: r.Header.Get("x-b3-parentspanid"),
    40  		SpanID:       r.Header.Get("x-b3-spanid"),
    41  		TraceID:      r.Header.Get("x-b3-traceid"),
    42  	}
    43  }
    44  
    45  func newNewRelicApp() *newrelic.Application {
    46  	appName := os.Getenv("NEWRELIC_APP_NAME")
    47  	license := os.Getenv("NEWRELIC_LICENSE")
    48  	if appName == "" && license == "" {
    49  		return nil
    50  	}
    51  
    52  	nrApp, err := newrelic.NewApplication(
    53  		newrelic.ConfigAppName(appName),
    54  		newrelic.ConfigLicense(license),
    55  		newrelic.ConfigDistributedTracerEnabled(true),
    56  	)
    57  	if err != nil {
    58  		logger.Crit("failed to create NewRelic application. If you want to register a NewRelic HTTP handler," +
    59  			" specify NEWRELIC_APP_NAME and NEWRELIC_LICENSE os environment variables with valid values. " +
    60  			"If you don't want to register the handler, specify them with an empty string.")
    61  	}
    62  
    63  	logger.Info("NewRelic APM is enabled", "appName", appName)
    64  	return nrApp
    65  }
    66  
    67  // newNewRelicHTTPHandler enables NewRelic web transaction monitor.
    68  // It also prints error logs when RPC returns contains error messages.
    69  func newNewRelicHTTPHandler(nrApp *newrelic.Application, handler http.Handler) http.Handler {
    70  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    71  		defer func() {
    72  			if err := recover(); err != nil {
    73  				logger.ErrorWithStack("NewRelic http handler panic", "err", err)
    74  			}
    75  		}()
    76  
    77  		reqMethod := ""
    78  
    79  		// parse RPC requests
    80  		reqs, isBatch, err := getRPCRequests(r)
    81  		if err != nil || len(reqs) < 1 {
    82  			// The error will be handled in `handler.ServeHTTP()` and printed with `printRPCErrorLog()`
    83  			logger.Debug("failed to parse RPC request", "err", err, "len(reqs)", len(reqs))
    84  		} else {
    85  			reqMethod = reqs[0].Method
    86  			if isBatch {
    87  				reqMethod += "_batch"
    88  			}
    89  		}
    90  
    91  		// new relic transaction name contains the first API method of the request
    92  		txn := nrApp.StartTransaction(r.Method + " " + r.URL.String() + " " + reqMethod)
    93  		defer txn.End()
    94  
    95  		w = txn.SetWebResponse(w)
    96  		txn.SetWebRequestHTTP(r)
    97  		r = newrelic.RequestWithTransactionContext(r, txn)
    98  
    99  		// duplicate writer
   100  		dupW := &dupWriter{
   101  			ResponseWriter: w,
   102  			body:           bytes.NewBufferString(""),
   103  		}
   104  
   105  		// serve HTTP
   106  		handler.ServeHTTP(dupW, r)
   107  
   108  		// print RPC error logs if errors exist
   109  		if isBatch {
   110  			var rpcReturns []interface{}
   111  			if err := json.Unmarshal(dupW.body.Bytes(), &rpcReturns); err == nil {
   112  				for i, rpcReturn := range rpcReturns {
   113  					if data, err := json.Marshal(rpcReturn); err == nil {
   114  						// TODO-Klaytn: make the log level configurable or separate module name of the logger
   115  						printRPCErrorLog(data, reqs[i].Method, r)
   116  					}
   117  				}
   118  			}
   119  		} else {
   120  			// TODO-Klaytn: make the log level configurable or separate module name of the logger
   121  			printRPCErrorLog(dupW.body.Bytes(), reqMethod, r)
   122  		}
   123  	})
   124  }
   125  
   126  // getRPCRequests copies a http request body data and parses RPC requests from the data.
   127  // It returns a slice of RPC request, an indication if these requests are in batch, and an error.
   128  // Ethereum returns []*jsonrpcMessage, which replaces []rpcRequest
   129  func getRPCRequests(r *http.Request) ([]*jsonrpcMessage, bool, error) {
   130  	reqBody, err := io.ReadAll(r.Body)
   131  	if err != nil {
   132  		logger.Error("cannot read a request body", "err", err)
   133  		return nil, false, err
   134  	}
   135  
   136  	r.Body = io.NopCloser(bytes.NewReader(reqBody))
   137  	conn := &httpServerConn{Reader: io.NopCloser(bytes.NewReader(reqBody)), Writer: bytes.NewBufferString(""), r: r}
   138  
   139  	codec := NewCodec(conn)
   140  
   141  	defer codec.close()
   142  
   143  	return codec.readBatch()
   144  }
   145  
   146  // printRPCErrorLog prints an error log if responseBody contains RPC error message.
   147  // It does nothing if responseBody doesn't contain RPC error message.
   148  func printRPCErrorLog(responseBody []byte, method string, r *http.Request) {
   149  	// check whether the responseBody contains json error
   150  	var rpcError jsonErrResponse
   151  	if err := json.Unmarshal(responseBody, &rpcError); err != nil || rpcError.Error.Code == 0 {
   152  		// do nothing if the responseBody didn't contain json error data
   153  		return
   154  	}
   155  
   156  	// parse KAS HTTP header
   157  	kasHeader := parseKASHeader(r)
   158  	kasHeaderJson, err := json.Marshal(kasHeader)
   159  	if err != nil {
   160  		logger.Error("failed to marshal a KAS HTTP header", "err", err, "kasHeader", kasHeader)
   161  	}
   162  
   163  	// print RPC error log
   164  	logger.Error("RPC error response", "rpcErr", rpcError.Error.Message, "kasHeader", string(kasHeaderJson),
   165  		"method", method)
   166  }