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 }