diff --git a/gateway/api_loader.go b/gateway/api_loader.go index 3a2b9c01984..90f954c9ca2 100644 --- a/gateway/api_loader.go +++ b/gateway/api_loader.go @@ -29,6 +29,7 @@ import ( "github.com/TykTechnologies/tyk/internal/httpctx" "github.com/TykTechnologies/tyk/internal/httputil" "github.com/TykTechnologies/tyk/internal/otel" + "github.com/TykTechnologies/tyk/internal/service/newrelic" ) const ( @@ -769,6 +770,8 @@ func (gw *Gateway) loadHTTPService(spec *APISpec, apisByListen map[string]int, g router := muxer.router(port, spec.Protocol, gwConfig) if router == nil { router = mux.NewRouter() + newrelic.Mount(router, gw.NewRelicApplication) + muxer.setRouter(port, spec.Protocol, router, gwConfig) } diff --git a/gateway/middleware.go b/gateway/middleware.go index bcef9fe6ecf..52c1b9cf840 100644 --- a/gateway/middleware.go +++ b/gateway/middleware.go @@ -15,7 +15,6 @@ import ( "github.com/gocraft/health" "github.com/justinas/alice" - newrelic "github.com/newrelic/go-agent" "github.com/paulbellamy/ratecounter" "github.com/sirupsen/logrus" "golang.org/x/sync/singleflight" @@ -27,6 +26,7 @@ import ( "github.com/TykTechnologies/tyk/internal/middleware" "github.com/TykTechnologies/tyk/internal/otel" "github.com/TykTechnologies/tyk/internal/policy" + "github.com/TykTechnologies/tyk/internal/service/newrelic" "github.com/TykTechnologies/tyk/request" "github.com/TykTechnologies/tyk/rpc" "github.com/TykTechnologies/tyk/storage" @@ -137,10 +137,8 @@ func (gw *Gateway) createMiddleware(actualMW TykMiddleware) func(http.Handler) h return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logger := mw.Base().SetRequestLogger(r) - if gw.GetConfig().NewRelic.AppName != "" { - if txn, ok := w.(newrelic.Transaction); ok { - defer newrelic.StartSegment(txn, mw.Name()).End() - } + if txn := newrelic.FromContext(r.Context()); txn != nil { + defer txn.StartSegment(mw.Name()).End() } job := instrument.NewJob("MiddlewareCall") diff --git a/gateway/newrelic.go b/gateway/newrelic.go deleted file mode 100644 index 513ddd577a3..00000000000 --- a/gateway/newrelic.go +++ /dev/null @@ -1,100 +0,0 @@ -package gateway - -import ( - "fmt" - "strconv" - - "github.com/gocraft/health" - "github.com/gorilla/mux" - newrelic "github.com/newrelic/go-agent" - "github.com/newrelic/go-agent/_integrations/nrgorilla/v1" - "github.com/sirupsen/logrus" -) - -// SetupNewRelic creates new newrelic.Application instance -func (gw *Gateway) SetupNewRelic() (app newrelic.Application) { - var ( - err error - gwConfig = gw.GetConfig() - ) - - logger := log.WithFields(logrus.Fields{"prefix": "newrelic"}) - - logger.Info("Initializing NewRelic...") - - cfg := newrelic.NewConfig(gwConfig.NewRelic.AppName, gwConfig.NewRelic.LicenseKey) - if gwConfig.NewRelic.AppName != "" { - cfg.Enabled = true - } - cfg.DistributedTracer.Enabled = gwConfig.NewRelic.EnableDistributedTracing - - cfg.Logger = &newRelicLogger{logger} - - if app, err = newrelic.NewApplication(cfg); err != nil { - logger.Warn("Error initializing NewRelic, skipping... ", err) - return - } - - instrument.AddSink(&newRelicSink{relic: app}) - logger.Info("NewRelic initialized") - - return -} - -// AddNewRelicInstrumentation adds NewRelic instrumentation to the router -func AddNewRelicInstrumentation(app newrelic.Application, r *mux.Router) { - if app != nil { - nrgorilla.InstrumentRoutes(r, app) - } -} - -type newRelicLogger struct{ *logrus.Entry } - -func (l *newRelicLogger) Error(msg string, c map[string]interface{}) { - l.WithFields(c).Error(msg) -} -func (l *newRelicLogger) Warn(msg string, c map[string]interface{}) { - l.WithFields(c).Warn(msg) -} -func (l *newRelicLogger) Info(msg string, c map[string]interface{}) { - l.WithFields(c).Info(msg) -} -func (l *newRelicLogger) Debug(msg string, c map[string]interface{}) { - l.WithFields(c).Debug(msg) -} -func (l *newRelicLogger) DebugEnabled() bool { - return l.Level >= logrus.DebugLevel -} - -type newRelicSink struct { - relic newrelic.Application - health.Sink -} - -func (s *newRelicSink) EmitEvent(job string, event string, kvs map[string]string) { - s.relic.RecordCustomEvent(job+":"+event, makeParams(kvs)) -} - -func (s *newRelicSink) EmitEventErr(job string, event string, err error, kvs map[string]string) { - s.relic.RecordCustomEvent(job+":"+event+":msg:"+err.Error(), makeParams(kvs)) -} - -func (s *newRelicSink) EmitTiming(job string, event string, nanoseconds int64, kvs map[string]string) { - s.relic.RecordCustomEvent(job+":"+event+":dur(ns):"+strconv.FormatInt(nanoseconds, 10), makeParams(kvs)) -} - -func (s *newRelicSink) EmitComplete(job string, status health.CompletionStatus, nanoseconds int64, kvs map[string]string) { - s.relic.RecordCustomEvent(job+":health:"+status.String()+":dur(ns):"+strconv.FormatInt(nanoseconds, 10), makeParams(kvs)) -} - -func (s *newRelicSink) EmitGauge(job string, event string, value float64, kvs map[string]string) { - s.relic.RecordCustomEvent(job+":"+event+":value:"+fmt.Sprintf("%.2f", value), makeParams(kvs)) -} - -func makeParams(kvs map[string]string) (params map[string]interface{}) { - params = make(map[string]interface{}, len(kvs)) - for k, v := range kvs { - params[k] = v - } - return -} diff --git a/gateway/proxy_muxer.go b/gateway/proxy_muxer.go index 89b23f6d4ad..5a6e27870da 100644 --- a/gateway/proxy_muxer.go +++ b/gateway/proxy_muxer.go @@ -95,12 +95,6 @@ func (h *handleWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if NewRelicApplication != nil { - txn := NewRelicApplication.StartTransaction(r.URL.Path, w, r) - defer txn.End() - h.router.ServeHTTP(txn, r) - return - } h.router.ServeHTTP(w, r) } diff --git a/gateway/server.go b/gateway/server.go index 745b9dabafb..2b206c70214 100644 --- a/gateway/server.go +++ b/gateway/server.go @@ -36,7 +36,6 @@ import ( grayloghook "github.com/gemnasium/logrus-graylog-hook" "github.com/gorilla/mux" "github.com/lonelycode/osin" - newrelic "github.com/newrelic/go-agent" "github.com/sirupsen/logrus" logrussyslog "github.com/sirupsen/logrus/hooks/syslog" @@ -67,6 +66,7 @@ import ( "github.com/TykTechnologies/tyk/internal/cache" "github.com/TykTechnologies/tyk/internal/model" "github.com/TykTechnologies/tyk/internal/netutil" + "github.com/TykTechnologies/tyk/internal/service/newrelic" ) var ( @@ -77,8 +77,7 @@ var ( pubSubLog = log.WithField("prefix", "pub-sub") rawLog = logger.GetRaw() - memProfFile *os.File - NewRelicApplication newrelic.Application + memProfFile *os.File // confPaths is the series of paths to try to use as config files. The // first one to exist will be used. If none exists, a default config @@ -125,6 +124,7 @@ type Gateway struct { HostCheckTicker chan struct{} HostCheckerClient *http.Client TracerProvider otel.TracerProvider + NewRelicApplication *newrelic.Application keyGen DefaultKeyGenerator @@ -256,6 +256,33 @@ func NewGateway(config config.Config, ctx context.Context) *Gateway { return gw } +// SetupNewRelic creates new newrelic.Application instance. +func (gw *Gateway) SetupNewRelic() (app *newrelic.Application) { + var ( + err error + gwConfig = gw.GetConfig() + ) + + log := log.WithFields(logrus.Fields{"prefix": "newrelic"}) + + cfg := []newrelic.ConfigOption{ + newrelic.ConfigAppName(gwConfig.NewRelic.AppName), + newrelic.ConfigLicense(gwConfig.NewRelic.LicenseKey), + newrelic.ConfigEnabled(gwConfig.NewRelic.AppName != ""), + newrelic.ConfigDistributedTracerEnabled(gwConfig.NewRelic.EnableDistributedTracing), + newrelic.ConfigLogger(newrelic.NewLogger(log)), + } + + if app, err = newrelic.NewApplication(cfg...); err != nil { + log.Warn("Error initializing NewRelic, skipping... ", err) + return + } + + instrument.AddSink(newrelic.NewSink(app)) + + return +} + func (gw *Gateway) UnmarshalJSON(data []byte) error { return nil } @@ -440,7 +467,7 @@ func (gw *Gateway) setupGlobals() { } if gw.GetConfig().NewRelic.AppName != "" { - NewRelicApplication = gw.SetupNewRelic() + gw.NewRelicApplication = gw.SetupNewRelic() } gw.readGraphqlPlaygroundTemplate() diff --git a/gateway/testutil.go b/gateway/testutil.go index 267f28d1cd2..106e9fa3266 100644 --- a/gateway/testutil.go +++ b/gateway/testutil.go @@ -1161,6 +1161,10 @@ func (s *Test) newGateway(genConf func(globalConf *config.Config)) *Gateway { gwConfig.BundleBaseURL = testHttpBundles gwConfig.MiddlewarePath = testMiddlewarePath + if err := config.FillEnv(&gwConfig); err != nil { + log.WithError(err).Error("error filling test config from env") + } + // force ipv4 for now, to work around the docker bug affecting // Go 1.8 and earlier gwConfig.ListenAddress = "127.0.0.1" @@ -1309,6 +1313,7 @@ func (s *Test) Close() { s.Gw.Analytics.Stop() s.Gw.ReloadTestCase.StopTicker() s.Gw.GlobalHostChecker.StopPoller() + s.Gw.NewRelicApplication.Shutdown(5 * time.Second) err = s.RemoveApis() if err != nil { diff --git a/go.mod b/go.mod index 3c10e2b4e2b..e20de39d9b2 100644 --- a/go.mod +++ b/go.mod @@ -92,7 +92,8 @@ require ( github.com/goccy/go-json v0.10.4 github.com/google/go-cmp v0.6.0 github.com/nats-io/nats.go v1.38.0 - github.com/newrelic/go-agent v2.13.0+incompatible + github.com/newrelic/go-agent/v3 v3.35.1 + github.com/newrelic/go-agent/v3/integrations/nrgorilla v1.2.2 github.com/testcontainers/testcontainers-go v0.34.0 github.com/testcontainers/testcontainers-go/modules/kafka v0.33.0 github.com/testcontainers/testcontainers-go/modules/nats v0.33.0 diff --git a/go.sum b/go.sum index 26a12714560..374644330f6 100644 --- a/go.sum +++ b/go.sum @@ -604,8 +604,10 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/stan.go v0.10.4 h1:19GS/eD1SeQJaVkeM9EkvEYattnvnWrZ3wkSWSw4uXw= github.com/nats-io/stan.go v0.10.4/go.mod h1:3XJXH8GagrGqajoO/9+HgPyKV5MWsv7S5ccdda+pc6k= -github.com/newrelic/go-agent v2.13.0+incompatible h1:Dl6m75MHAzfB0kicv9GiLxzQatRjTLUAdrnYyoT8s4M= -github.com/newrelic/go-agent v2.13.0+incompatible/go.mod h1:a8Fv1b/fYhFSReoTU6HDkTYIMZeSVNffmoS726Y0LzQ= +github.com/newrelic/go-agent/v3 v3.35.1 h1:N43qBNDILmnwLDCSfnE1yy6adyoVEU95nAOtdUgG4vA= +github.com/newrelic/go-agent/v3 v3.35.1/go.mod h1:GNTda53CohAhkgsc7/gqSsJhDZjj8vaky5u+vKz7wqM= +github.com/newrelic/go-agent/v3/integrations/nrgorilla v1.2.2 h1:YaFf6tmxSKNVgS9ZHx6O8HSpckWiyNSBZQKwaXfG1fQ= +github.com/newrelic/go-agent/v3/integrations/nrgorilla v1.2.2/go.mod h1:NlYWXdP4WVAg8v7ZM0FRWulv0OtssOS3l4R6pYlWGf0= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1 h1:dOYG7LS/WK00RWZc8XGgcUTlTxpp3mKhdR2Q9z9HbXM= github.com/nsf/jsondiff v0.0.0-20230430225905-43f6cf3098c1/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= diff --git a/internal/service/newrelic/logger.go b/internal/service/newrelic/logger.go new file mode 100644 index 00000000000..7fac94c20f1 --- /dev/null +++ b/internal/service/newrelic/logger.go @@ -0,0 +1,33 @@ +package newrelic + +import ( + "github.com/newrelic/go-agent/v3/newrelic" + + "github.com/sirupsen/logrus" +) + +type Logger struct { + *logrus.Entry +} + +var _ newrelic.Logger = &Logger{} + +func NewLogger(e *logrus.Entry) *Logger { + return &Logger{e} +} + +func (l *Logger) Error(msg string, c map[string]interface{}) { + l.WithFields(c).Error(msg) +} +func (l *Logger) Warn(msg string, c map[string]interface{}) { + l.WithFields(c).Warn(msg) +} +func (l *Logger) Info(msg string, c map[string]interface{}) { + l.WithFields(c).Info(msg) +} +func (l *Logger) Debug(msg string, c map[string]interface{}) { + l.WithFields(c).Debug(msg) +} +func (l *Logger) DebugEnabled() bool { + return l.Level >= logrus.DebugLevel +} diff --git a/internal/service/newrelic/newrelic.go b/internal/service/newrelic/newrelic.go new file mode 100644 index 00000000000..775ff650c32 --- /dev/null +++ b/internal/service/newrelic/newrelic.go @@ -0,0 +1,36 @@ +package newrelic + +import ( + "github.com/gorilla/mux" + "github.com/newrelic/go-agent/v3/integrations/nrgorilla" + "github.com/newrelic/go-agent/v3/newrelic" +) + +// Type aliases used from newrelic pkg. +type ( + Application = newrelic.Application + Transaction = newrelic.Transaction + ConfigOption = newrelic.ConfigOption +) + +// Variable aliases used from newrelic pkg. +var ( + NewApplication = newrelic.NewApplication + FromContext = newrelic.FromContext + + ConfigLogger = newrelic.ConfigLogger + ConfigEnabled = newrelic.ConfigEnabled + ConfigAppName = newrelic.ConfigAppName + ConfigLicense = newrelic.ConfigLicense + ConfigDistributedTracerEnabled = newrelic.ConfigDistributedTracerEnabled +) + +// Mount adds the nrgorilla middleware to the router. The application is added to the request context. +// If app is nil, nothing will be done and the function will return. +func Mount(router *mux.Router, app *Application) { + if app == nil { + return + } + + router.Use(nrgorilla.Middleware(app)) +} diff --git a/internal/service/newrelic/sink.go b/internal/service/newrelic/sink.go new file mode 100644 index 00000000000..51c13ca6f9e --- /dev/null +++ b/internal/service/newrelic/sink.go @@ -0,0 +1,49 @@ +package newrelic + +import ( + "fmt" + "strconv" + + "github.com/newrelic/go-agent/v3/newrelic" + + "github.com/gocraft/health" +) + +type Sink struct { + relic *newrelic.Application + health.Sink +} + +func NewSink(relic *newrelic.Application) *Sink { + return &Sink{ + relic: relic, + } +} + +func (s *Sink) EmitEvent(job string, event string, kvs map[string]string) { + s.relic.RecordCustomEvent(job+":"+event, makeParams(kvs)) +} + +func (s *Sink) EmitEventErr(job string, event string, err error, kvs map[string]string) { + s.relic.RecordCustomEvent(job+":"+event+":msg:"+err.Error(), makeParams(kvs)) +} + +func (s *Sink) EmitTiming(job string, event string, nanoseconds int64, kvs map[string]string) { + s.relic.RecordCustomEvent(job+":"+event+":duration_ns:"+strconv.FormatInt(nanoseconds, 10), makeParams(kvs)) +} + +func (s *Sink) EmitComplete(job string, status health.CompletionStatus, nanoseconds int64, kvs map[string]string) { + s.relic.RecordCustomEvent(job+":health:"+status.String()+":duration_ns:"+strconv.FormatInt(nanoseconds, 10), makeParams(kvs)) +} + +func (s *Sink) EmitGauge(job string, event string, value float64, kvs map[string]string) { + s.relic.RecordCustomEvent(job+":"+event+":value:"+fmt.Sprintf("%.2f", value), makeParams(kvs)) +} + +func makeParams(kvs map[string]string) (params map[string]interface{}) { + params = make(map[string]interface{}, len(kvs)) + for k, v := range kvs { + params[k] = v + } + return +}