github.com/newrelic/go-agent@v3.26.0+incompatible/internal/attributes.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  	"fmt"
     9  	"net/http"
    10  	"net/url"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  )
    15  
    16  // AgentAttributeID uniquely identifies each agent attribute.
    17  type AgentAttributeID int
    18  
    19  // New agent attributes must be added in the following places:
    20  // * Constants here.
    21  // * Top level attributes.go file.
    22  // * agentAttributeInfo
    23  const (
    24  	AttributeHostDisplayName AgentAttributeID = iota
    25  	attributeRequestMethod
    26  	attributeRequestAcceptHeader
    27  	attributeRequestContentType
    28  	attributeRequestContentLength
    29  	attributeRequestHeadersHost
    30  	attributeRequestHeadersUserAgent
    31  	attributeRequestHeadersReferer
    32  	attributeRequestURI
    33  	attributeResponseHeadersContentType
    34  	attributeResponseHeadersContentLength
    35  	attributeResponseCode
    36  	AttributeAWSRequestID
    37  	AttributeAWSLambdaARN
    38  	AttributeAWSLambdaColdStart
    39  	AttributeAWSLambdaEventSourceARN
    40  	AttributeMessageRoutingKey
    41  	AttributeMessageQueueName
    42  	AttributeMessageExchangeType
    43  	AttributeMessageReplyTo
    44  	AttributeMessageCorrelationID
    45  )
    46  
    47  // SpanAttribute is an attribute put in span events.
    48  type SpanAttribute string
    49  
    50  // AddAgentSpanAttributer should be implemented by the Transaction.
    51  type AddAgentSpanAttributer interface {
    52  	AddAgentSpanAttribute(key SpanAttribute, val string)
    53  }
    54  
    55  // AddAgentSpanAttribute allows instrumentation packages to add span attributes.
    56  func AddAgentSpanAttribute(txn interface{}, key SpanAttribute, val string) {
    57  	if aa, ok := txn.(AddAgentSpanAttributer); ok {
    58  		aa.AddAgentSpanAttribute(key, val)
    59  	}
    60  }
    61  
    62  // These span event string constants must match the contents of the top level
    63  // attributes.go file.
    64  const (
    65  	spanAttributeDBStatement  SpanAttribute = "db.statement"
    66  	spanAttributeDBInstance   SpanAttribute = "db.instance"
    67  	spanAttributeDBCollection SpanAttribute = "db.collection"
    68  	spanAttributePeerAddress  SpanAttribute = "peer.address"
    69  	spanAttributePeerHostname SpanAttribute = "peer.hostname"
    70  	spanAttributeHTTPURL      SpanAttribute = "http.url"
    71  	spanAttributeHTTPMethod   SpanAttribute = "http.method"
    72  	// query parameters only appear in segments, not span events, but is
    73  	// listed as span attributes to simplify code.
    74  	spanAttributeQueryParameters SpanAttribute = "query_parameters"
    75  	// These span attributes are added by aws sdk instrumentation.
    76  	// https://source.datanerd.us/agents/agent-specs/blob/master/implementation_guides/aws-sdk.md#span-and-segment-attributes
    77  	SpanAttributeAWSOperation SpanAttribute = "aws.operation"
    78  	SpanAttributeAWSRequestID SpanAttribute = "aws.requestId"
    79  	SpanAttributeAWSRegion    SpanAttribute = "aws.region"
    80  )
    81  
    82  func (sa SpanAttribute) String() string { return string(sa) }
    83  
    84  var (
    85  	usualDests         = DestAll &^ destBrowser
    86  	tracesDests        = destTxnTrace | destError
    87  	agentAttributeInfo = map[AgentAttributeID]struct {
    88  		name         string
    89  		defaultDests destinationSet
    90  	}{
    91  		AttributeHostDisplayName:              {name: "host.displayName", defaultDests: usualDests},
    92  		attributeRequestMethod:                {name: "request.method", defaultDests: usualDests},
    93  		attributeRequestAcceptHeader:          {name: "request.headers.accept", defaultDests: usualDests},
    94  		attributeRequestContentType:           {name: "request.headers.contentType", defaultDests: usualDests},
    95  		attributeRequestContentLength:         {name: "request.headers.contentLength", defaultDests: usualDests},
    96  		attributeRequestHeadersHost:           {name: "request.headers.host", defaultDests: usualDests},
    97  		attributeRequestHeadersUserAgent:      {name: "request.headers.User-Agent", defaultDests: tracesDests},
    98  		attributeRequestHeadersReferer:        {name: "request.headers.referer", defaultDests: tracesDests},
    99  		attributeRequestURI:                   {name: "request.uri", defaultDests: usualDests},
   100  		attributeResponseHeadersContentType:   {name: "response.headers.contentType", defaultDests: usualDests},
   101  		attributeResponseHeadersContentLength: {name: "response.headers.contentLength", defaultDests: usualDests},
   102  		attributeResponseCode:                 {name: "httpResponseCode", defaultDests: usualDests},
   103  		AttributeAWSRequestID:                 {name: "aws.requestId", defaultDests: usualDests},
   104  		AttributeAWSLambdaARN:                 {name: "aws.lambda.arn", defaultDests: usualDests},
   105  		AttributeAWSLambdaColdStart:           {name: "aws.lambda.coldStart", defaultDests: usualDests},
   106  		AttributeAWSLambdaEventSourceARN:      {name: "aws.lambda.eventSource.arn", defaultDests: usualDests},
   107  		AttributeMessageRoutingKey:            {name: "message.routingKey", defaultDests: usualDests},
   108  		AttributeMessageQueueName:             {name: "message.queueName", defaultDests: usualDests},
   109  		AttributeMessageExchangeType:          {name: "message.exchangeType", defaultDests: destNone},
   110  		AttributeMessageReplyTo:               {name: "message.replyTo", defaultDests: destNone},
   111  		AttributeMessageCorrelationID:         {name: "message.correlationId", defaultDests: destNone},
   112  	}
   113  	spanAttributes = []SpanAttribute{
   114  		spanAttributeDBStatement,
   115  		spanAttributeDBInstance,
   116  		spanAttributeDBCollection,
   117  		spanAttributePeerAddress,
   118  		spanAttributePeerHostname,
   119  		spanAttributeHTTPURL,
   120  		spanAttributeHTTPMethod,
   121  		spanAttributeQueryParameters,
   122  		SpanAttributeAWSOperation,
   123  		SpanAttributeAWSRequestID,
   124  		SpanAttributeAWSRegion,
   125  	}
   126  )
   127  
   128  func (id AgentAttributeID) name() string { return agentAttributeInfo[id].name }
   129  
   130  // https://source.datanerd.us/agents/agent-specs/blob/master/Agent-Attributes-PORTED.md
   131  
   132  // AttributeDestinationConfig matches newrelic.AttributeDestinationConfig to
   133  // avoid circular dependency issues.
   134  type AttributeDestinationConfig struct {
   135  	Enabled bool
   136  	Include []string
   137  	Exclude []string
   138  }
   139  
   140  type destinationSet int
   141  
   142  const (
   143  	destTxnEvent destinationSet = 1 << iota
   144  	destError
   145  	destTxnTrace
   146  	destBrowser
   147  	destSpan
   148  	destSegment
   149  )
   150  
   151  const (
   152  	destNone destinationSet = 0
   153  	// DestAll contains all destinations.
   154  	DestAll destinationSet = destTxnEvent | destTxnTrace | destError | destBrowser | destSpan | destSegment
   155  )
   156  
   157  const (
   158  	attributeWildcardSuffix = '*'
   159  )
   160  
   161  type attributeModifier struct {
   162  	match string // This will not contain a trailing '*'.
   163  	includeExclude
   164  }
   165  
   166  type byMatch []*attributeModifier
   167  
   168  func (m byMatch) Len() int           { return len(m) }
   169  func (m byMatch) Swap(i, j int)      { m[i], m[j] = m[j], m[i] }
   170  func (m byMatch) Less(i, j int) bool { return m[i].match < m[j].match }
   171  
   172  // AttributeConfig is created at connect and shared between all transactions.
   173  type AttributeConfig struct {
   174  	disabledDestinations destinationSet
   175  	exactMatchModifiers  map[string]*attributeModifier
   176  	// Once attributeConfig is constructed, wildcardModifiers is sorted in
   177  	// lexicographical order.  Modifiers appearing later have precedence
   178  	// over modifiers appearing earlier.
   179  	wildcardModifiers []*attributeModifier
   180  	agentDests        map[AgentAttributeID]destinationSet
   181  	spanDests         map[SpanAttribute]destinationSet
   182  }
   183  
   184  type includeExclude struct {
   185  	include destinationSet
   186  	exclude destinationSet
   187  }
   188  
   189  func modifierApply(m *attributeModifier, d destinationSet) destinationSet {
   190  	// Include before exclude, since exclude has priority.
   191  	d |= m.include
   192  	d &^= m.exclude
   193  	return d
   194  }
   195  
   196  func applyAttributeConfig(c *AttributeConfig, key string, d destinationSet) destinationSet {
   197  	// Important: The wildcard modifiers must be applied before the exact
   198  	// match modifiers, and the slice must be iterated in a forward
   199  	// direction.
   200  	for _, m := range c.wildcardModifiers {
   201  		if strings.HasPrefix(key, m.match) {
   202  			d = modifierApply(m, d)
   203  		}
   204  	}
   205  
   206  	if m, ok := c.exactMatchModifiers[key]; ok {
   207  		d = modifierApply(m, d)
   208  	}
   209  
   210  	d &^= c.disabledDestinations
   211  
   212  	return d
   213  }
   214  
   215  func addModifier(c *AttributeConfig, match string, d includeExclude) {
   216  	if "" == match {
   217  		return
   218  	}
   219  	exactMatch := true
   220  	if attributeWildcardSuffix == match[len(match)-1] {
   221  		exactMatch = false
   222  		match = match[0 : len(match)-1]
   223  	}
   224  	mod := &attributeModifier{
   225  		match:          match,
   226  		includeExclude: d,
   227  	}
   228  
   229  	if exactMatch {
   230  		if m, ok := c.exactMatchModifiers[mod.match]; ok {
   231  			m.include |= mod.include
   232  			m.exclude |= mod.exclude
   233  		} else {
   234  			c.exactMatchModifiers[mod.match] = mod
   235  		}
   236  	} else {
   237  		for _, m := range c.wildcardModifiers {
   238  			// Important: Duplicate entries for the same match
   239  			// string would not work because exclude needs
   240  			// precedence over include.
   241  			if m.match == mod.match {
   242  				m.include |= mod.include
   243  				m.exclude |= mod.exclude
   244  				return
   245  			}
   246  		}
   247  		c.wildcardModifiers = append(c.wildcardModifiers, mod)
   248  	}
   249  }
   250  
   251  func processDest(c *AttributeConfig, includeEnabled bool, dc *AttributeDestinationConfig, d destinationSet) {
   252  	if !dc.Enabled {
   253  		c.disabledDestinations |= d
   254  	}
   255  	if includeEnabled {
   256  		for _, match := range dc.Include {
   257  			addModifier(c, match, includeExclude{include: d})
   258  		}
   259  	}
   260  	for _, match := range dc.Exclude {
   261  		addModifier(c, match, includeExclude{exclude: d})
   262  	}
   263  }
   264  
   265  // AttributeConfigInput is used as the input to CreateAttributeConfig:  it
   266  // transforms newrelic.Config settings into an AttributeConfig.
   267  type AttributeConfigInput struct {
   268  	Attributes        AttributeDestinationConfig
   269  	ErrorCollector    AttributeDestinationConfig
   270  	TransactionEvents AttributeDestinationConfig
   271  	BrowserMonitoring AttributeDestinationConfig
   272  	TransactionTracer AttributeDestinationConfig
   273  	SpanEvents        AttributeDestinationConfig
   274  	TraceSegments     AttributeDestinationConfig
   275  }
   276  
   277  var (
   278  	sampleAttributeConfigInput = AttributeConfigInput{
   279  		Attributes:        AttributeDestinationConfig{Enabled: true},
   280  		ErrorCollector:    AttributeDestinationConfig{Enabled: true},
   281  		TransactionEvents: AttributeDestinationConfig{Enabled: true},
   282  		TransactionTracer: AttributeDestinationConfig{Enabled: true},
   283  		BrowserMonitoring: AttributeDestinationConfig{Enabled: true},
   284  		SpanEvents:        AttributeDestinationConfig{Enabled: true},
   285  		TraceSegments:     AttributeDestinationConfig{Enabled: true},
   286  	}
   287  )
   288  
   289  // CreateAttributeConfig creates a new AttributeConfig.
   290  func CreateAttributeConfig(input AttributeConfigInput, includeEnabled bool) *AttributeConfig {
   291  	c := &AttributeConfig{
   292  		exactMatchModifiers: make(map[string]*attributeModifier),
   293  		wildcardModifiers:   make([]*attributeModifier, 0, 64),
   294  	}
   295  
   296  	processDest(c, includeEnabled, &input.Attributes, DestAll)
   297  	processDest(c, includeEnabled, &input.ErrorCollector, destError)
   298  	processDest(c, includeEnabled, &input.TransactionEvents, destTxnEvent)
   299  	processDest(c, includeEnabled, &input.TransactionTracer, destTxnTrace)
   300  	processDest(c, includeEnabled, &input.BrowserMonitoring, destBrowser)
   301  	processDest(c, includeEnabled, &input.SpanEvents, destSpan)
   302  	processDest(c, includeEnabled, &input.TraceSegments, destSegment)
   303  
   304  	sort.Sort(byMatch(c.wildcardModifiers))
   305  
   306  	c.agentDests = make(map[AgentAttributeID]destinationSet)
   307  	for id, info := range agentAttributeInfo {
   308  		c.agentDests[id] = applyAttributeConfig(c, info.name, info.defaultDests)
   309  	}
   310  	c.spanDests = make(map[SpanAttribute]destinationSet, len(spanAttributes))
   311  	for _, id := range spanAttributes {
   312  		c.spanDests[id] = applyAttributeConfig(c, id.String(), destSpan|destSegment)
   313  	}
   314  
   315  	return c
   316  }
   317  
   318  type userAttribute struct {
   319  	value interface{}
   320  	dests destinationSet
   321  }
   322  
   323  type agentAttributeValue struct {
   324  	stringVal string
   325  	otherVal  interface{}
   326  }
   327  
   328  type agentAttributes map[AgentAttributeID]agentAttributeValue
   329  
   330  func (a *Attributes) filterSpanAttributes(s map[SpanAttribute]jsonWriter, d destinationSet) map[SpanAttribute]jsonWriter {
   331  	if nil != a {
   332  		for key := range s {
   333  			if a.config.spanDests[key]&d == 0 {
   334  				delete(s, key)
   335  			}
   336  		}
   337  	}
   338  	return s
   339  }
   340  
   341  // GetAgentValue is used to access agent attributes.  This function returns ("",
   342  // nil) if the attribute doesn't exist or it doesn't match the destinations
   343  // provided.
   344  func (a *Attributes) GetAgentValue(id AgentAttributeID, d destinationSet) (string, interface{}) {
   345  	if nil == a || 0 == a.config.agentDests[id]&d {
   346  		return "", nil
   347  	}
   348  	v, _ := a.Agent[id]
   349  	return v.stringVal, v.otherVal
   350  }
   351  
   352  // AddAgentAttributer allows instrumentation to add agent attributes without
   353  // exposing a Transaction method.
   354  type AddAgentAttributer interface {
   355  	AddAgentAttribute(id AgentAttributeID, stringVal string, otherVal interface{})
   356  }
   357  
   358  // Add is used to add agent attributes.  Only one of stringVal and
   359  // otherVal should be populated.  Since most agent attribute values are strings,
   360  // stringVal exists to avoid allocations.
   361  func (attr agentAttributes) Add(id AgentAttributeID, stringVal string, otherVal interface{}) {
   362  	if "" != stringVal || otherVal != nil {
   363  		attr[id] = agentAttributeValue{
   364  			stringVal: truncateStringValueIfLong(stringVal),
   365  			otherVal:  otherVal,
   366  		}
   367  	}
   368  }
   369  
   370  // Attributes are key value pairs attached to the various collected data types.
   371  type Attributes struct {
   372  	config *AttributeConfig
   373  	user   map[string]userAttribute
   374  	Agent  agentAttributes
   375  }
   376  
   377  // NewAttributes creates a new Attributes.
   378  func NewAttributes(config *AttributeConfig) *Attributes {
   379  	return &Attributes{
   380  		config: config,
   381  		Agent:  make(agentAttributes),
   382  	}
   383  }
   384  
   385  // ErrInvalidAttributeType is returned when the value is not valid.
   386  type ErrInvalidAttributeType struct {
   387  	key string
   388  	val interface{}
   389  }
   390  
   391  func (e ErrInvalidAttributeType) Error() string {
   392  	return fmt.Sprintf("attribute '%s' value of type %T is invalid", e.key, e.val)
   393  }
   394  
   395  type invalidAttributeKeyErr struct{ key string }
   396  
   397  func (e invalidAttributeKeyErr) Error() string {
   398  	return fmt.Sprintf("attribute key '%.32s...' exceeds length limit %d",
   399  		e.key, attributeKeyLengthLimit)
   400  }
   401  
   402  type userAttributeLimitErr struct{ key string }
   403  
   404  func (e userAttributeLimitErr) Error() string {
   405  	return fmt.Sprintf("attribute '%s' discarded: limit of %d reached", e.key,
   406  		attributeUserLimit)
   407  }
   408  
   409  func truncateStringValueIfLong(val string) string {
   410  	if len(val) > attributeValueLengthLimit {
   411  		return StringLengthByteLimit(val, attributeValueLengthLimit)
   412  	}
   413  	return val
   414  }
   415  
   416  // ValidateUserAttribute validates a user attribute.
   417  func ValidateUserAttribute(key string, val interface{}) (interface{}, error) {
   418  	if str, ok := val.(string); ok {
   419  		val = interface{}(truncateStringValueIfLong(str))
   420  	}
   421  
   422  	switch val.(type) {
   423  	case string, bool,
   424  		uint8, uint16, uint32, uint64, int8, int16, int32, int64,
   425  		float32, float64, uint, int, uintptr:
   426  	default:
   427  		return nil, ErrInvalidAttributeType{
   428  			key: key,
   429  			val: val,
   430  		}
   431  	}
   432  
   433  	// Attributes whose keys are excessively long are dropped rather than
   434  	// truncated to avoid worrying about the application of configuration to
   435  	// truncated values or performing the truncation after configuration.
   436  	if len(key) > attributeKeyLengthLimit {
   437  		return nil, invalidAttributeKeyErr{key: key}
   438  	}
   439  	return val, nil
   440  }
   441  
   442  // AddUserAttribute adds a user attribute.
   443  func AddUserAttribute(a *Attributes, key string, val interface{}, d destinationSet) error {
   444  	val, err := ValidateUserAttribute(key, val)
   445  	if nil != err {
   446  		return err
   447  	}
   448  	dests := applyAttributeConfig(a.config, key, d)
   449  	if destNone == dests {
   450  		return nil
   451  	}
   452  	if nil == a.user {
   453  		a.user = make(map[string]userAttribute)
   454  	}
   455  
   456  	if _, exists := a.user[key]; !exists && len(a.user) >= attributeUserLimit {
   457  		return userAttributeLimitErr{key}
   458  	}
   459  
   460  	// Note: Duplicates are overridden: last attribute in wins.
   461  	a.user[key] = userAttribute{
   462  		value: val,
   463  		dests: dests,
   464  	}
   465  	return nil
   466  }
   467  
   468  func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) {
   469  	switch v := val.(type) {
   470  	case string:
   471  		w.stringField(key, v)
   472  	case bool:
   473  		if v {
   474  			w.rawField(key, `true`)
   475  		} else {
   476  			w.rawField(key, `false`)
   477  		}
   478  	case uint8:
   479  		w.intField(key, int64(v))
   480  	case uint16:
   481  		w.intField(key, int64(v))
   482  	case uint32:
   483  		w.intField(key, int64(v))
   484  	case uint64:
   485  		w.intField(key, int64(v))
   486  	case uint:
   487  		w.intField(key, int64(v))
   488  	case uintptr:
   489  		w.intField(key, int64(v))
   490  	case int8:
   491  		w.intField(key, int64(v))
   492  	case int16:
   493  		w.intField(key, int64(v))
   494  	case int32:
   495  		w.intField(key, int64(v))
   496  	case int64:
   497  		w.intField(key, v)
   498  	case int:
   499  		w.intField(key, int64(v))
   500  	case float32:
   501  		w.floatField(key, float64(v))
   502  	case float64:
   503  		w.floatField(key, v)
   504  	default:
   505  		w.stringField(key, fmt.Sprintf("%T", v))
   506  	}
   507  }
   508  
   509  func agentAttributesJSON(a *Attributes, buf *bytes.Buffer, d destinationSet) {
   510  	if nil == a {
   511  		buf.WriteString("{}")
   512  		return
   513  	}
   514  	w := jsonFieldsWriter{buf: buf}
   515  	buf.WriteByte('{')
   516  	for id, val := range a.Agent {
   517  		if 0 != a.config.agentDests[id]&d {
   518  			if val.stringVal != "" {
   519  				w.stringField(id.name(), val.stringVal)
   520  			} else {
   521  				writeAttributeValueJSON(&w, id.name(), val.otherVal)
   522  			}
   523  		}
   524  	}
   525  	buf.WriteByte('}')
   526  
   527  }
   528  
   529  func userAttributesJSON(a *Attributes, buf *bytes.Buffer, d destinationSet, extraAttributes map[string]interface{}) {
   530  	buf.WriteByte('{')
   531  	if nil != a {
   532  		w := jsonFieldsWriter{buf: buf}
   533  		for key, val := range extraAttributes {
   534  			outputDest := applyAttributeConfig(a.config, key, d)
   535  			if 0 != outputDest&d {
   536  				writeAttributeValueJSON(&w, key, val)
   537  			}
   538  		}
   539  		for name, atr := range a.user {
   540  			if 0 != atr.dests&d {
   541  				if _, found := extraAttributes[name]; found {
   542  					continue
   543  				}
   544  				writeAttributeValueJSON(&w, name, atr.value)
   545  			}
   546  		}
   547  	}
   548  	buf.WriteByte('}')
   549  }
   550  
   551  // userAttributesStringJSON is only used for testing.
   552  func userAttributesStringJSON(a *Attributes, d destinationSet, extraAttributes map[string]interface{}) string {
   553  	estimate := len(a.user) * 128
   554  	buf := bytes.NewBuffer(make([]byte, 0, estimate))
   555  	userAttributesJSON(a, buf, d, extraAttributes)
   556  	return buf.String()
   557  }
   558  
   559  // RequestAgentAttributes gathers agent attributes out of the request.
   560  func RequestAgentAttributes(a *Attributes, method string, h http.Header, u *url.URL) {
   561  	a.Agent.Add(attributeRequestMethod, method, nil)
   562  
   563  	if nil != u {
   564  		a.Agent.Add(attributeRequestURI, SafeURL(u), nil)
   565  	}
   566  
   567  	if nil == h {
   568  		return
   569  	}
   570  	a.Agent.Add(attributeRequestAcceptHeader, h.Get("Accept"), nil)
   571  	a.Agent.Add(attributeRequestContentType, h.Get("Content-Type"), nil)
   572  	a.Agent.Add(attributeRequestHeadersHost, h.Get("Host"), nil)
   573  	a.Agent.Add(attributeRequestHeadersUserAgent, h.Get("User-Agent"), nil)
   574  	a.Agent.Add(attributeRequestHeadersReferer, SafeURLFromString(h.Get("Referer")), nil)
   575  
   576  	if l := GetContentLengthFromHeader(h); l >= 0 {
   577  		a.Agent.Add(attributeRequestContentLength, "", l)
   578  	}
   579  }
   580  
   581  // ResponseHeaderAttributes gather agent attributes from the response headers.
   582  func ResponseHeaderAttributes(a *Attributes, h http.Header) {
   583  	if nil == h {
   584  		return
   585  	}
   586  	a.Agent.Add(attributeResponseHeadersContentType, h.Get("Content-Type"), nil)
   587  
   588  	if l := GetContentLengthFromHeader(h); l >= 0 {
   589  		a.Agent.Add(attributeResponseHeadersContentLength, "", l)
   590  	}
   591  }
   592  
   593  var (
   594  	// statusCodeLookup avoids a strconv.Itoa call.
   595  	statusCodeLookup = map[int]string{
   596  		100: "100", 101: "101",
   597  		200: "200", 201: "201", 202: "202", 203: "203", 204: "204", 205: "205", 206: "206",
   598  		300: "300", 301: "301", 302: "302", 303: "303", 304: "304", 305: "305", 307: "307",
   599  		400: "400", 401: "401", 402: "402", 403: "403", 404: "404", 405: "405", 406: "406",
   600  		407: "407", 408: "408", 409: "409", 410: "410", 411: "411", 412: "412", 413: "413",
   601  		414: "414", 415: "415", 416: "416", 417: "417", 418: "418", 428: "428", 429: "429",
   602  		431: "431", 451: "451",
   603  		500: "500", 501: "501", 502: "502", 503: "503", 504: "504", 505: "505", 511: "511",
   604  	}
   605  )
   606  
   607  // ResponseCodeAttribute sets the response code agent attribute.
   608  func ResponseCodeAttribute(a *Attributes, code int) {
   609  	rc := statusCodeLookup[code]
   610  	if rc == "" {
   611  		rc = strconv.Itoa(code)
   612  	}
   613  	a.Agent.Add(attributeResponseCode, rc, nil)
   614  }