573 lines
16 KiB
Go
573 lines
16 KiB
Go
package internal
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// New agent attributes must be added in the following places:
|
|
// * Constants here.
|
|
// * Top level attributes.go file.
|
|
// * agentAttributes
|
|
// * agentAttributeDests
|
|
// * calculateAgentAttributeDests
|
|
// * writeAgentAttributes
|
|
const (
|
|
responseCode = "httpResponseCode"
|
|
requestMethod = "request.method"
|
|
requestAccept = "request.headers.accept"
|
|
requestContentType = "request.headers.contentType"
|
|
requestContentLength = "request.headers.contentLength"
|
|
requestHost = "request.headers.host"
|
|
responseContentType = "response.headers.contentType"
|
|
responseContentLength = "response.headers.contentLength"
|
|
hostDisplayName = "host.displayName"
|
|
requestUserAgent = "request.headers.User-Agent"
|
|
requestReferer = "request.headers.referer"
|
|
)
|
|
|
|
// https://source.datanerd.us/agents/agent-specs/blob/master/Agent-Attributes-PORTED.md
|
|
|
|
// AttributeDestinationConfig matches newrelic.AttributeDestinationConfig to
|
|
// avoid circular dependency issues.
|
|
type AttributeDestinationConfig struct {
|
|
Enabled bool
|
|
Include []string
|
|
Exclude []string
|
|
}
|
|
|
|
type destinationSet int
|
|
|
|
const (
|
|
destTxnEvent destinationSet = 1 << iota
|
|
destError
|
|
destTxnTrace
|
|
destBrowser
|
|
)
|
|
|
|
const (
|
|
destNone destinationSet = 0
|
|
// DestAll contains all destinations.
|
|
DestAll destinationSet = destTxnEvent | destTxnTrace | destError | destBrowser
|
|
)
|
|
|
|
const (
|
|
attributeWildcardSuffix = '*'
|
|
)
|
|
|
|
type attributeModifier struct {
|
|
match string // This will not contain a trailing '*'.
|
|
includeExclude
|
|
}
|
|
|
|
type byMatch []*attributeModifier
|
|
|
|
func (m byMatch) Len() int { return len(m) }
|
|
func (m byMatch) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
|
|
func (m byMatch) Less(i, j int) bool { return m[i].match < m[j].match }
|
|
|
|
// AttributeConfig is created at application creation and shared between all
|
|
// transactions.
|
|
type AttributeConfig struct {
|
|
disabledDestinations destinationSet
|
|
exactMatchModifiers map[string]*attributeModifier
|
|
// Once attributeConfig is constructed, wildcardModifiers is sorted in
|
|
// lexicographical order. Modifiers appearing later have precedence
|
|
// over modifiers appearing earlier.
|
|
wildcardModifiers []*attributeModifier
|
|
agentDests agentAttributeDests
|
|
}
|
|
|
|
type includeExclude struct {
|
|
include destinationSet
|
|
exclude destinationSet
|
|
}
|
|
|
|
func modifierApply(m *attributeModifier, d destinationSet) destinationSet {
|
|
// Include before exclude, since exclude has priority.
|
|
d |= m.include
|
|
d &^= m.exclude
|
|
return d
|
|
}
|
|
|
|
func applyAttributeConfig(c *AttributeConfig, key string, d destinationSet) destinationSet {
|
|
// Important: The wildcard modifiers must be applied before the exact
|
|
// match modifiers, and the slice must be iterated in a forward
|
|
// direction.
|
|
for _, m := range c.wildcardModifiers {
|
|
if strings.HasPrefix(key, m.match) {
|
|
d = modifierApply(m, d)
|
|
}
|
|
}
|
|
|
|
if m, ok := c.exactMatchModifiers[key]; ok {
|
|
d = modifierApply(m, d)
|
|
}
|
|
|
|
d &^= c.disabledDestinations
|
|
|
|
return d
|
|
}
|
|
|
|
func addModifier(c *AttributeConfig, match string, d includeExclude) {
|
|
if "" == match {
|
|
return
|
|
}
|
|
exactMatch := true
|
|
if attributeWildcardSuffix == match[len(match)-1] {
|
|
exactMatch = false
|
|
match = match[0 : len(match)-1]
|
|
}
|
|
mod := &attributeModifier{
|
|
match: match,
|
|
includeExclude: d,
|
|
}
|
|
|
|
if exactMatch {
|
|
if m, ok := c.exactMatchModifiers[mod.match]; ok {
|
|
m.include |= mod.include
|
|
m.exclude |= mod.exclude
|
|
} else {
|
|
c.exactMatchModifiers[mod.match] = mod
|
|
}
|
|
} else {
|
|
for _, m := range c.wildcardModifiers {
|
|
// Important: Duplicate entries for the same match
|
|
// string would not work because exclude needs
|
|
// precedence over include.
|
|
if m.match == mod.match {
|
|
m.include |= mod.include
|
|
m.exclude |= mod.exclude
|
|
return
|
|
}
|
|
}
|
|
c.wildcardModifiers = append(c.wildcardModifiers, mod)
|
|
}
|
|
}
|
|
|
|
func processDest(c *AttributeConfig, dc *AttributeDestinationConfig, d destinationSet) {
|
|
if !dc.Enabled {
|
|
c.disabledDestinations |= d
|
|
}
|
|
for _, match := range dc.Include {
|
|
addModifier(c, match, includeExclude{include: d})
|
|
}
|
|
for _, match := range dc.Exclude {
|
|
addModifier(c, match, includeExclude{exclude: d})
|
|
}
|
|
}
|
|
|
|
// AttributeConfigInput is used as the input to CreateAttributeConfig: it
|
|
// transforms newrelic.Config settings into an AttributeConfig.
|
|
type AttributeConfigInput struct {
|
|
Attributes AttributeDestinationConfig
|
|
ErrorCollector AttributeDestinationConfig
|
|
TransactionEvents AttributeDestinationConfig
|
|
browserMonitoring AttributeDestinationConfig
|
|
TransactionTracer AttributeDestinationConfig
|
|
}
|
|
|
|
var (
|
|
sampleAttributeConfigInput = AttributeConfigInput{
|
|
Attributes: AttributeDestinationConfig{Enabled: true},
|
|
ErrorCollector: AttributeDestinationConfig{Enabled: true},
|
|
TransactionEvents: AttributeDestinationConfig{Enabled: true},
|
|
TransactionTracer: AttributeDestinationConfig{Enabled: true},
|
|
}
|
|
)
|
|
|
|
// CreateAttributeConfig creates a new AttributeConfig.
|
|
func CreateAttributeConfig(input AttributeConfigInput) *AttributeConfig {
|
|
c := &AttributeConfig{
|
|
exactMatchModifiers: make(map[string]*attributeModifier),
|
|
wildcardModifiers: make([]*attributeModifier, 0, 64),
|
|
}
|
|
|
|
processDest(c, &input.Attributes, DestAll)
|
|
processDest(c, &input.ErrorCollector, destError)
|
|
processDest(c, &input.TransactionEvents, destTxnEvent)
|
|
processDest(c, &input.TransactionTracer, destTxnTrace)
|
|
processDest(c, &input.browserMonitoring, destBrowser)
|
|
|
|
sort.Sort(byMatch(c.wildcardModifiers))
|
|
|
|
c.agentDests = calculateAgentAttributeDests(c)
|
|
|
|
return c
|
|
}
|
|
|
|
type userAttribute struct {
|
|
value interface{}
|
|
dests destinationSet
|
|
}
|
|
|
|
// Attributes are key value pairs attached to the various collected data types.
|
|
type Attributes struct {
|
|
config *AttributeConfig
|
|
user map[string]userAttribute
|
|
Agent agentAttributes
|
|
}
|
|
|
|
type agentAttributes struct {
|
|
HostDisplayName string
|
|
RequestMethod string
|
|
RequestAcceptHeader string
|
|
RequestContentType string
|
|
RequestContentLength int
|
|
RequestHeadersHost string
|
|
RequestHeadersUserAgent string
|
|
RequestHeadersReferer string
|
|
ResponseHeadersContentType string
|
|
ResponseHeadersContentLength int
|
|
ResponseCode string
|
|
}
|
|
|
|
type agentAttributeDests struct {
|
|
HostDisplayName destinationSet
|
|
RequestMethod destinationSet
|
|
RequestAcceptHeader destinationSet
|
|
RequestContentType destinationSet
|
|
RequestContentLength destinationSet
|
|
RequestHeadersHost destinationSet
|
|
RequestHeadersUserAgent destinationSet
|
|
RequestHeadersReferer destinationSet
|
|
ResponseHeadersContentType destinationSet
|
|
ResponseHeadersContentLength destinationSet
|
|
ResponseCode destinationSet
|
|
}
|
|
|
|
func calculateAgentAttributeDests(c *AttributeConfig) agentAttributeDests {
|
|
usual := DestAll &^ destBrowser
|
|
traces := destTxnTrace | destError
|
|
return agentAttributeDests{
|
|
HostDisplayName: applyAttributeConfig(c, hostDisplayName, usual),
|
|
RequestMethod: applyAttributeConfig(c, requestMethod, usual),
|
|
RequestAcceptHeader: applyAttributeConfig(c, requestAccept, usual),
|
|
RequestContentType: applyAttributeConfig(c, requestContentType, usual),
|
|
RequestContentLength: applyAttributeConfig(c, requestContentLength, usual),
|
|
RequestHeadersHost: applyAttributeConfig(c, requestHost, usual),
|
|
RequestHeadersUserAgent: applyAttributeConfig(c, requestUserAgent, traces),
|
|
RequestHeadersReferer: applyAttributeConfig(c, requestReferer, traces),
|
|
ResponseHeadersContentType: applyAttributeConfig(c, responseContentType, usual),
|
|
ResponseHeadersContentLength: applyAttributeConfig(c, responseContentLength, usual),
|
|
ResponseCode: applyAttributeConfig(c, responseCode, usual),
|
|
}
|
|
}
|
|
|
|
type agentAttributeWriter struct {
|
|
jsonFieldsWriter
|
|
d destinationSet
|
|
}
|
|
|
|
func (w *agentAttributeWriter) writeString(name string, val string, d destinationSet) {
|
|
if "" != val && 0 != w.d&d {
|
|
w.stringField(name, truncateStringValueIfLong(val))
|
|
}
|
|
}
|
|
|
|
func (w *agentAttributeWriter) writeInt(name string, val int, d destinationSet) {
|
|
if val >= 0 && 0 != w.d&d {
|
|
w.intField(name, int64(val))
|
|
}
|
|
}
|
|
|
|
func writeAgentAttributes(buf *bytes.Buffer, d destinationSet, values agentAttributes, dests agentAttributeDests) {
|
|
w := &agentAttributeWriter{
|
|
jsonFieldsWriter: jsonFieldsWriter{buf: buf},
|
|
d: d,
|
|
}
|
|
buf.WriteByte('{')
|
|
w.writeString(hostDisplayName, values.HostDisplayName, dests.HostDisplayName)
|
|
w.writeString(requestMethod, values.RequestMethod, dests.RequestMethod)
|
|
w.writeString(requestAccept, values.RequestAcceptHeader, dests.RequestAcceptHeader)
|
|
w.writeString(requestContentType, values.RequestContentType, dests.RequestContentType)
|
|
w.writeInt(requestContentLength, values.RequestContentLength, dests.RequestContentLength)
|
|
w.writeString(requestHost, values.RequestHeadersHost, dests.RequestHeadersHost)
|
|
w.writeString(requestUserAgent, values.RequestHeadersUserAgent, dests.RequestHeadersUserAgent)
|
|
w.writeString(requestReferer, values.RequestHeadersReferer, dests.RequestHeadersReferer)
|
|
w.writeString(responseContentType, values.ResponseHeadersContentType, dests.ResponseHeadersContentType)
|
|
w.writeInt(responseContentLength, values.ResponseHeadersContentLength, dests.ResponseHeadersContentLength)
|
|
w.writeString(responseCode, values.ResponseCode, dests.ResponseCode)
|
|
buf.WriteByte('}')
|
|
}
|
|
|
|
// NewAttributes creates a new Attributes.
|
|
func NewAttributes(config *AttributeConfig) *Attributes {
|
|
return &Attributes{
|
|
config: config,
|
|
Agent: agentAttributes{
|
|
RequestContentLength: -1,
|
|
ResponseHeadersContentLength: -1,
|
|
},
|
|
}
|
|
}
|
|
|
|
// ErrInvalidAttribute is returned when the value is not valid.
|
|
type ErrInvalidAttribute struct{ typeString string }
|
|
|
|
func (e ErrInvalidAttribute) Error() string {
|
|
return fmt.Sprintf("attribute value type %s is invalid", e.typeString)
|
|
}
|
|
|
|
func valueIsValid(val interface{}) error {
|
|
switch val.(type) {
|
|
case string, bool, nil,
|
|
uint8, uint16, uint32, uint64, int8, int16, int32, int64,
|
|
float32, float64, uint, int, uintptr:
|
|
return nil
|
|
default:
|
|
return ErrInvalidAttribute{
|
|
typeString: fmt.Sprintf("%T", val),
|
|
}
|
|
}
|
|
}
|
|
|
|
type invalidAttributeKeyErr struct{ key string }
|
|
|
|
func (e invalidAttributeKeyErr) Error() string {
|
|
return fmt.Sprintf("attribute key '%.32s...' exceeds length limit %d",
|
|
e.key, attributeKeyLengthLimit)
|
|
}
|
|
|
|
type userAttributeLimitErr struct{ key string }
|
|
|
|
func (e userAttributeLimitErr) Error() string {
|
|
return fmt.Sprintf("attribute '%s' discarded: limit of %d reached", e.key,
|
|
attributeUserLimit)
|
|
}
|
|
|
|
func validAttributeKey(key string) error {
|
|
// Attributes whose keys are excessively long are dropped rather than
|
|
// truncated to avoid worrying about the application of configuration to
|
|
// truncated values or performing the truncation after configuration.
|
|
if len(key) > attributeKeyLengthLimit {
|
|
return invalidAttributeKeyErr{key: key}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func truncateStringValueIfLong(val string) string {
|
|
if len(val) > attributeValueLengthLimit {
|
|
return StringLengthByteLimit(val, attributeValueLengthLimit)
|
|
}
|
|
return val
|
|
}
|
|
|
|
func truncateStringValueIfLongInterface(val interface{}) interface{} {
|
|
if str, ok := val.(string); ok {
|
|
val = interface{}(truncateStringValueIfLong(str))
|
|
}
|
|
return val
|
|
}
|
|
|
|
// AddUserAttribute adds a user attribute.
|
|
func AddUserAttribute(a *Attributes, key string, val interface{}, d destinationSet) error {
|
|
val = truncateStringValueIfLongInterface(val)
|
|
if err := valueIsValid(val); nil != err {
|
|
return err
|
|
}
|
|
if err := validAttributeKey(key); nil != err {
|
|
return err
|
|
}
|
|
dests := applyAttributeConfig(a.config, key, d)
|
|
if destNone == dests {
|
|
return nil
|
|
}
|
|
if nil == a.user {
|
|
a.user = make(map[string]userAttribute)
|
|
}
|
|
|
|
if _, exists := a.user[key]; !exists && len(a.user) >= attributeUserLimit {
|
|
return userAttributeLimitErr{key}
|
|
}
|
|
|
|
// Note: Duplicates are overridden: last attribute in wins.
|
|
a.user[key] = userAttribute{
|
|
value: val,
|
|
dests: dests,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeAttributeValueJSON(w *jsonFieldsWriter, key string, val interface{}) {
|
|
switch v := val.(type) {
|
|
case nil:
|
|
w.rawField(key, `null`)
|
|
case string:
|
|
w.stringField(key, v)
|
|
case bool:
|
|
if v {
|
|
w.rawField(key, `true`)
|
|
} else {
|
|
w.rawField(key, `false`)
|
|
}
|
|
case uint8:
|
|
w.intField(key, int64(v))
|
|
case uint16:
|
|
w.intField(key, int64(v))
|
|
case uint32:
|
|
w.intField(key, int64(v))
|
|
case uint64:
|
|
w.intField(key, int64(v))
|
|
case uint:
|
|
w.intField(key, int64(v))
|
|
case uintptr:
|
|
w.intField(key, int64(v))
|
|
case int8:
|
|
w.intField(key, int64(v))
|
|
case int16:
|
|
w.intField(key, int64(v))
|
|
case int32:
|
|
w.intField(key, int64(v))
|
|
case int64:
|
|
w.intField(key, v)
|
|
case int:
|
|
w.intField(key, int64(v))
|
|
case float32:
|
|
w.floatField(key, float64(v))
|
|
case float64:
|
|
w.floatField(key, v)
|
|
default:
|
|
w.stringField(key, fmt.Sprintf("%T", v))
|
|
}
|
|
}
|
|
|
|
type agentAttributesJSONWriter struct {
|
|
attributes *Attributes
|
|
dest destinationSet
|
|
}
|
|
|
|
func (w agentAttributesJSONWriter) WriteJSON(buf *bytes.Buffer) {
|
|
if nil == w.attributes {
|
|
buf.WriteString("{}")
|
|
return
|
|
}
|
|
writeAgentAttributes(buf, w.dest, w.attributes.Agent, w.attributes.config.agentDests)
|
|
}
|
|
|
|
func agentAttributesJSON(a *Attributes, buf *bytes.Buffer, d destinationSet) {
|
|
agentAttributesJSONWriter{
|
|
attributes: a,
|
|
dest: d,
|
|
}.WriteJSON(buf)
|
|
}
|
|
|
|
type userAttributesJSONWriter struct {
|
|
attributes *Attributes
|
|
dest destinationSet
|
|
}
|
|
|
|
func (u userAttributesJSONWriter) WriteJSON(buf *bytes.Buffer) {
|
|
buf.WriteByte('{')
|
|
if nil != u.attributes {
|
|
w := jsonFieldsWriter{buf: buf}
|
|
for name, atr := range u.attributes.user {
|
|
if 0 != atr.dests&u.dest {
|
|
writeAttributeValueJSON(&w, name, atr.value)
|
|
}
|
|
}
|
|
}
|
|
buf.WriteByte('}')
|
|
}
|
|
|
|
func userAttributesJSON(a *Attributes, buf *bytes.Buffer, d destinationSet) {
|
|
userAttributesJSONWriter{
|
|
attributes: a,
|
|
dest: d,
|
|
}.WriteJSON(buf)
|
|
}
|
|
|
|
func userAttributesStringJSON(a *Attributes, d destinationSet) JSONString {
|
|
if nil == a {
|
|
return JSONString("{}")
|
|
}
|
|
estimate := len(a.user) * 128
|
|
buf := bytes.NewBuffer(make([]byte, 0, estimate))
|
|
userAttributesJSON(a, buf, d)
|
|
bs := buf.Bytes()
|
|
return JSONString(bs)
|
|
}
|
|
|
|
func agentAttributesStringJSON(a *Attributes, d destinationSet) JSONString {
|
|
if nil == a {
|
|
return JSONString("{}")
|
|
}
|
|
estimate := 1024
|
|
buf := bytes.NewBuffer(make([]byte, 0, estimate))
|
|
agentAttributesJSON(a, buf, d)
|
|
return JSONString(buf.Bytes())
|
|
}
|
|
|
|
func getUserAttributes(a *Attributes, d destinationSet) map[string]interface{} {
|
|
v := make(map[string]interface{})
|
|
json.Unmarshal([]byte(userAttributesStringJSON(a, d)), &v)
|
|
return v
|
|
}
|
|
|
|
func getAgentAttributes(a *Attributes, d destinationSet) map[string]interface{} {
|
|
v := make(map[string]interface{})
|
|
json.Unmarshal([]byte(agentAttributesStringJSON(a, d)), &v)
|
|
return v
|
|
}
|
|
|
|
// RequestAgentAttributes gathers agent attributes out of the request.
|
|
func RequestAgentAttributes(a *Attributes, r *http.Request) {
|
|
a.Agent.RequestMethod = r.Method
|
|
|
|
h := r.Header
|
|
if nil == h {
|
|
return
|
|
}
|
|
a.Agent.RequestAcceptHeader = h.Get("Accept")
|
|
a.Agent.RequestContentType = h.Get("Content-Type")
|
|
a.Agent.RequestHeadersHost = h.Get("Host")
|
|
a.Agent.RequestHeadersUserAgent = h.Get("User-Agent")
|
|
a.Agent.RequestHeadersReferer = SafeURLFromString(h.Get("Referer"))
|
|
|
|
if cl := h.Get("Content-Length"); "" != cl {
|
|
if x, err := strconv.Atoi(cl); nil == err {
|
|
a.Agent.RequestContentLength = x
|
|
}
|
|
}
|
|
}
|
|
|
|
// ResponseHeaderAttributes gather agent attributes from the response headers.
|
|
func ResponseHeaderAttributes(a *Attributes, h http.Header) {
|
|
if nil == h {
|
|
return
|
|
}
|
|
a.Agent.ResponseHeadersContentType = h.Get("Content-Type")
|
|
if val := h.Get("Content-Length"); "" != val {
|
|
if x, err := strconv.Atoi(val); nil == err {
|
|
a.Agent.ResponseHeadersContentLength = x
|
|
}
|
|
}
|
|
}
|
|
|
|
var (
|
|
// statusCodeLookup avoids a strconv.Itoa call.
|
|
statusCodeLookup = map[int]string{
|
|
100: "100", 101: "101",
|
|
200: "200", 201: "201", 202: "202", 203: "203", 204: "204", 205: "205", 206: "206",
|
|
300: "300", 301: "301", 302: "302", 303: "303", 304: "304", 305: "305", 307: "307",
|
|
400: "400", 401: "401", 402: "402", 403: "403", 404: "404", 405: "405", 406: "406",
|
|
407: "407", 408: "408", 409: "409", 410: "410", 411: "411", 412: "412", 413: "413",
|
|
414: "414", 415: "415", 416: "416", 417: "417", 418: "418", 428: "428", 429: "429",
|
|
431: "431", 451: "451",
|
|
500: "500", 501: "501", 502: "502", 503: "503", 504: "504", 505: "505", 511: "511",
|
|
}
|
|
)
|
|
|
|
// ResponseCodeAttribute sets the response code agent attribute.
|
|
func ResponseCodeAttribute(a *Attributes, code int) {
|
|
a.Agent.ResponseCode = statusCodeLookup[code]
|
|
if a.Agent.ResponseCode == "" {
|
|
a.Agent.ResponseCode = strconv.Itoa(code)
|
|
}
|
|
}
|