github.com/newrelic/go-agent@v3.26.0+incompatible/_integrations/nrmicro/nrmicro.go (about)

     1  // Copyright 2020 New Relic Corporation. All rights reserved.
     2  // SPDX-License-Identifier: Apache-2.0
     3  
     4  package nrmicro
     5  
     6  import (
     7  	"context"
     8  	"net/http"
     9  	"net/url"
    10  	"strings"
    11  
    12  	"github.com/micro/go-micro/client"
    13  	"github.com/micro/go-micro/errors"
    14  	"github.com/micro/go-micro/metadata"
    15  	"github.com/micro/go-micro/registry"
    16  	"github.com/micro/go-micro/server"
    17  	newrelic "github.com/newrelic/go-agent"
    18  	"github.com/newrelic/go-agent/internal"
    19  	"github.com/newrelic/go-agent/internal/integrationsupport"
    20  )
    21  
    22  type nrWrapper struct {
    23  	client.Client
    24  }
    25  
    26  var addrMap = make(map[string]string)
    27  
    28  func startExternal(ctx context.Context, procedure, host string) (context.Context, newrelic.ExternalSegment) {
    29  	var seg newrelic.ExternalSegment
    30  	if txn := newrelic.FromContext(ctx); nil != txn {
    31  		seg = newrelic.ExternalSegment{
    32  			StartTime: newrelic.StartSegmentNow(txn),
    33  			Procedure: procedure,
    34  			Library:   "Micro",
    35  			Host:      host,
    36  		}
    37  		ctx = addDTPayloadToContext(ctx, txn)
    38  	}
    39  	return ctx, seg
    40  }
    41  
    42  func startMessage(ctx context.Context, topic string) (context.Context, *newrelic.MessageProducerSegment) {
    43  	var seg *newrelic.MessageProducerSegment
    44  	if txn := newrelic.FromContext(ctx); nil != txn {
    45  		seg = &newrelic.MessageProducerSegment{
    46  			StartTime:       newrelic.StartSegmentNow(txn),
    47  			Library:         "Micro",
    48  			DestinationType: newrelic.MessageTopic,
    49  			DestinationName: topic,
    50  		}
    51  		ctx = addDTPayloadToContext(ctx, txn)
    52  	}
    53  	return ctx, seg
    54  }
    55  
    56  func addDTPayloadToContext(ctx context.Context, txn newrelic.Transaction) context.Context {
    57  	payload := txn.CreateDistributedTracePayload()
    58  	if txt := payload.Text(); "" != txt {
    59  		md, _ := metadata.FromContext(ctx)
    60  		md = metadata.Copy(md)
    61  		md[newrelic.DistributedTracePayloadHeader] = txt
    62  		ctx = metadata.NewContext(ctx, md)
    63  	}
    64  	return ctx
    65  }
    66  
    67  func extractHost(addr string) string {
    68  	if host, ok := addrMap[addr]; ok {
    69  		return host
    70  	}
    71  
    72  	host := addr
    73  	if strings.HasPrefix(host, "unix://") {
    74  		host = "localhost"
    75  	} else if u, err := url.Parse(host); nil == err {
    76  		if "" != u.Host {
    77  			host = u.Host
    78  		} else {
    79  			host = u.Path
    80  		}
    81  	}
    82  
    83  	addrMap[addr] = host
    84  	return host
    85  }
    86  
    87  func (n *nrWrapper) Publish(ctx context.Context, msg client.Message, opts ...client.PublishOption) error {
    88  	ctx, seg := startMessage(ctx, msg.Topic())
    89  	defer seg.End()
    90  	return n.Client.Publish(ctx, msg, opts...)
    91  }
    92  
    93  func (n *nrWrapper) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
    94  	ctx, seg := startExternal(ctx, req.Endpoint(), req.Service())
    95  	defer seg.End()
    96  	return n.Client.Stream(ctx, req, opts...)
    97  }
    98  
    99  func (n *nrWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
   100  	ctx, seg := startExternal(ctx, req.Endpoint(), req.Service())
   101  	defer seg.End()
   102  	return n.Client.Call(ctx, req, rsp, opts...)
   103  }
   104  
   105  // ClientWrapper wraps a Micro `client.Client`
   106  // (https://godoc.org/github.com/micro/go-micro/client#Client) instance.  External
   107  // segments will be created for each call to the client's `Call`, `Publish`, or
   108  // `Stream` methods.  The `newrelic.Transaction` must be put into the context
   109  // using `newrelic.NewContext`
   110  // (https://godoc.org/github.com/newrelic/go-agent#NewContext) when calling one
   111  // of those methods.
   112  func ClientWrapper() client.Wrapper {
   113  	return func(c client.Client) client.Client {
   114  		return &nrWrapper{c}
   115  	}
   116  }
   117  
   118  // CallWrapper wraps the `Call` method of a Micro `client.Client`
   119  // (https://godoc.org/github.com/micro/go-micro/client#Client) instance.
   120  // External segments will be created for each call to the client's `Call`
   121  // method.  The `newrelic.Transaction` must be put into the context using
   122  // `newrelic.NewContext`
   123  // (https://godoc.org/github.com/newrelic/go-agent#NewContext) when calling
   124  // `Call`.
   125  func CallWrapper() client.CallWrapper {
   126  	return func(cf client.CallFunc) client.CallFunc {
   127  		return func(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
   128  			ctx, seg := startExternal(ctx, req.Endpoint(), req.Service())
   129  			defer seg.End()
   130  			return cf(ctx, node, req, rsp, opts)
   131  		}
   132  	}
   133  }
   134  
   135  // HandlerWrapper wraps a Micro `server.Server`
   136  // (https://godoc.org/github.com/micro/go-micro/server#Server) handler.
   137  //
   138  // This wrapper creates transactions for inbound calls.  The transaction is
   139  // added to the call context and can be accessed in your method handlers using
   140  // `newrelic.FromContext`
   141  // (https://godoc.org/github.com/newrelic/go-agent#FromContext).
   142  //
   143  // When an error is returned and it is of type Micro `errors.Error`
   144  // (https://godoc.org/github.com/micro/go-micro/errors#Error), the error that
   145  // is recorded is based on the HTTP response code (found in the Code field).
   146  // Values above 400 or below 100 that are not in the IgnoreStatusCodes
   147  // (https://godoc.org/github.com/newrelic/go-agent#Config) configuration list
   148  // are recorded as errors. A 500 response code and corresponding error is
   149  // recorded when the error is of any other type. A 200 response code is
   150  // recorded if no error is returned.
   151  func HandlerWrapper(app newrelic.Application) server.HandlerWrapper {
   152  	return func(fn server.HandlerFunc) server.HandlerFunc {
   153  		if app == nil {
   154  			return fn
   155  		}
   156  		return func(ctx context.Context, req server.Request, rsp interface{}) error {
   157  			txn := startWebTransaction(ctx, app, req)
   158  			defer txn.End()
   159  			err := fn(newrelic.NewContext(ctx, txn), req, rsp)
   160  			var code int
   161  			if err != nil {
   162  				if t, ok := err.(*errors.Error); ok {
   163  					code = int(t.Code)
   164  				} else {
   165  					code = 500
   166  				}
   167  			} else {
   168  				code = 200
   169  			}
   170  			txn.WriteHeader(code)
   171  			return err
   172  		}
   173  	}
   174  }
   175  
   176  // SubscriberWrapper wraps a Micro `server.Subscriber`
   177  // (https://godoc.org/github.com/micro/go-micro/server#Subscriber) instance.
   178  //
   179  // This wrapper creates background transactions for inbound calls.  The
   180  // transaction is added to the subscriber context and can be accessed in your
   181  // subscriber handlers using `newrelic.FromContext`
   182  // (https://godoc.org/github.com/newrelic/go-agent#FromContext).
   183  //
   184  // The attribute `"message.routingKey"` is added to the transaction and will
   185  // appear on transaction events, transaction traces, error events, and error
   186  // traces. It corresponds to the `server.Message`'s Topic
   187  // (https://godoc.org/github.com/micro/go-micro/server#Message).
   188  //
   189  // If a Subscriber returns an error, it will be recorded and reported.
   190  func SubscriberWrapper(app newrelic.Application) server.SubscriberWrapper {
   191  	return func(fn server.SubscriberFunc) server.SubscriberFunc {
   192  		if app == nil {
   193  			return fn
   194  		}
   195  		return func(ctx context.Context, m server.Message) (err error) {
   196  			namer := internal.MessageMetricKey{
   197  				Library:         "Micro",
   198  				DestinationType: string(newrelic.MessageTopic),
   199  				DestinationName: m.Topic(),
   200  				Consumer:        true,
   201  			}
   202  			txn := app.StartTransaction(namer.Name(), nil, nil)
   203  			defer txn.End()
   204  			integrationsupport.AddAgentAttribute(txn, internal.AttributeMessageRoutingKey, m.Topic(), nil)
   205  			md, ok := metadata.FromContext(ctx)
   206  			if ok {
   207  				txn.AcceptDistributedTracePayload(newrelic.TransportHTTP, md[newrelic.DistributedTracePayloadHeader])
   208  			}
   209  			ctx = newrelic.NewContext(ctx, txn)
   210  			err = fn(ctx, m)
   211  			if err != nil {
   212  				txn.NoticeError(err)
   213  			}
   214  			return err
   215  		}
   216  	}
   217  }
   218  
   219  func startWebTransaction(ctx context.Context, app newrelic.Application, req server.Request) newrelic.Transaction {
   220  	var hdrs http.Header
   221  	if md, ok := metadata.FromContext(ctx); ok {
   222  		hdrs = make(http.Header, len(md))
   223  		for k, v := range md {
   224  			hdrs.Add(k, v)
   225  		}
   226  	}
   227  	txn := app.StartTransaction(req.Endpoint(), nil, nil)
   228  	u := &url.URL{
   229  		Scheme: "micro",
   230  		Host:   req.Service(),
   231  		Path:   req.Endpoint(),
   232  	}
   233  
   234  	webReq := newrelic.NewStaticWebRequest(hdrs, u, req.Method(), newrelic.TransportHTTP)
   235  	txn.SetWebRequest(webReq)
   236  
   237  	return txn
   238  }