github.com/newrelic/go-agent@v3.26.0+incompatible/internal/tracing.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 "errors" 9 "fmt" 10 "net/http" 11 "net/url" 12 "time" 13 14 "github.com/newrelic/go-agent/internal/cat" 15 "github.com/newrelic/go-agent/internal/jsonx" 16 "github.com/newrelic/go-agent/internal/logger" 17 "github.com/newrelic/go-agent/internal/sysinfo" 18 ) 19 20 // MarshalJSON limits the number of decimals. 21 func (p *Priority) MarshalJSON() ([]byte, error) { 22 return []byte(fmt.Sprintf(priorityFormat, *p)), nil 23 } 24 25 // WriteJSON limits the number of decimals. 26 func (p Priority) WriteJSON(buf *bytes.Buffer) { 27 fmt.Fprintf(buf, priorityFormat, p) 28 } 29 30 // TxnEvent represents a transaction. 31 // https://source.datanerd.us/agents/agent-specs/blob/master/Transaction-Events-PORTED.md 32 // https://newrelic.atlassian.net/wiki/display/eng/Agent+Support+for+Synthetics%3A+Forced+Transaction+Traces+and+Analytic+Events 33 type TxnEvent struct { 34 FinalName string 35 Start time.Time 36 Duration time.Duration 37 TotalTime time.Duration 38 Queuing time.Duration 39 Zone ApdexZone 40 Attrs *Attributes 41 DatastoreExternalTotals 42 CrossProcess TxnCrossProcess 43 BetterCAT BetterCAT 44 HasError bool 45 } 46 47 // BetterCAT stores the transaction's priority and all fields related 48 // to a DistributedTracer's Cross-Application Trace. 49 type BetterCAT struct { 50 Enabled bool 51 Priority Priority 52 Sampled bool 53 Inbound *Payload 54 ID string 55 } 56 57 // TraceID returns the trace id. 58 func (e BetterCAT) TraceID() string { 59 if nil != e.Inbound { 60 return e.Inbound.TracedID 61 } 62 return e.ID 63 } 64 65 // TxnData contains the recorded data of a transaction. 66 type TxnData struct { 67 TxnEvent 68 IsWeb bool 69 Name string // Work in progress name. 70 Errors TxnErrors // Lazily initialized. 71 Stop time.Time 72 ApdexThreshold time.Duration 73 74 stamp segmentStamp 75 threadIDCounter uint64 76 77 TraceIDGenerator *TraceIDGenerator 78 LazilyCalculateSampled func() bool 79 SpanEventsEnabled bool 80 rootSpanID string 81 spanEvents []*SpanEvent 82 83 customSegments map[string]*metricData 84 datastoreSegments map[DatastoreMetricKey]*metricData 85 externalSegments map[externalMetricKey]*metricData 86 messageSegments map[MessageMetricKey]*metricData 87 88 TxnTrace 89 90 SlowQueriesEnabled bool 91 SlowQueryThreshold time.Duration 92 SlowQueries *slowQueries 93 94 // These better CAT supportability fields are left outside of 95 // TxnEvent.BetterCAT to minimize the size of transaction event memory. 96 DistributedTracingSupport 97 } 98 99 func (t *TxnData) saveTraceSegment(end segmentEnd, name string, attrs spanAttributeMap, externalGUID string) { 100 attrs = t.Attrs.filterSpanAttributes(attrs, destSegment) 101 t.TxnTrace.witnessNode(end, name, attrs, externalGUID) 102 } 103 104 // Thread contains a segment stack that is used to track segment parenting time 105 // within a single goroutine. 106 type Thread struct { 107 threadID uint64 108 stack []segmentFrame 109 // start and end are used to track the TotalTime this Thread was active. 110 start time.Time 111 end time.Time 112 } 113 114 // RecordActivity indicates that activity happened at this time on this 115 // goroutine which helps track total time. 116 func (thread *Thread) RecordActivity(now time.Time) { 117 if thread.start.IsZero() || now.Before(thread.start) { 118 thread.start = now 119 } 120 if now.After(thread.end) { 121 thread.end = now 122 } 123 } 124 125 // TotalTime returns the amount to time that this thread contributes to the 126 // total time. 127 func (thread *Thread) TotalTime() time.Duration { 128 if thread.start.Before(thread.end) { 129 return thread.end.Sub(thread.start) 130 } 131 return 0 132 } 133 134 // NewThread returns a new Thread to track segments in a new goroutine. 135 func NewThread(txndata *TxnData) *Thread { 136 // Each thread needs a unique ID. 137 txndata.threadIDCounter++ 138 return &Thread{ 139 threadID: txndata.threadIDCounter, 140 } 141 } 142 143 type segmentStamp uint64 144 145 type segmentTime struct { 146 Stamp segmentStamp 147 Time time.Time 148 } 149 150 // SegmentStartTime is embedded into the top level segments (rather than 151 // segmentTime) to minimize the structure sizes to minimize allocations. 152 type SegmentStartTime struct { 153 Stamp segmentStamp 154 Depth int 155 } 156 157 type stringJSONWriter string 158 159 func (s stringJSONWriter) WriteJSON(buf *bytes.Buffer) { 160 jsonx.AppendString(buf, string(s)) 161 } 162 163 // spanAttributeMap is used for span attributes and segment attributes. The 164 // value is a jsonWriter to allow for segment query parameters. 165 type spanAttributeMap map[SpanAttribute]jsonWriter 166 167 func (m *spanAttributeMap) addString(key SpanAttribute, val string) { 168 if "" != val { 169 m.add(key, stringJSONWriter(val)) 170 } 171 } 172 173 func (m *spanAttributeMap) add(key SpanAttribute, val jsonWriter) { 174 if *m == nil { 175 *m = make(spanAttributeMap) 176 } 177 (*m)[key] = val 178 } 179 180 func (m spanAttributeMap) copy() spanAttributeMap { 181 if len(m) == 0 { 182 return nil 183 } 184 cpy := make(spanAttributeMap, len(m)) 185 for k, v := range m { 186 cpy[k] = v 187 } 188 return cpy 189 } 190 191 type segmentFrame struct { 192 segmentTime 193 children time.Duration 194 spanID string 195 attributes spanAttributeMap 196 } 197 198 type segmentEnd struct { 199 start segmentTime 200 stop segmentTime 201 duration time.Duration 202 exclusive time.Duration 203 SpanID string 204 ParentID string 205 threadID uint64 206 attributes spanAttributeMap 207 } 208 209 func (end segmentEnd) spanEvent() *SpanEvent { 210 if "" == end.SpanID { 211 return nil 212 } 213 return &SpanEvent{ 214 GUID: end.SpanID, 215 ParentID: end.ParentID, 216 Timestamp: end.start.Time, 217 Duration: end.duration, 218 Attributes: end.attributes, 219 IsEntrypoint: false, 220 } 221 } 222 223 const ( 224 datastoreProductUnknown = "Unknown" 225 datastoreOperationUnknown = "other" 226 ) 227 228 // HasErrors indicates whether the transaction had errors. 229 func (t *TxnData) HasErrors() bool { 230 return len(t.Errors) > 0 231 } 232 233 func (t *TxnData) time(now time.Time) segmentTime { 234 // Update the stamp before using it so that a 0 stamp can be special. 235 t.stamp++ 236 return segmentTime{ 237 Time: now, 238 Stamp: t.stamp, 239 } 240 } 241 242 // AddAgentSpanAttribute allows attributes to be added to spans. 243 func (thread *Thread) AddAgentSpanAttribute(key SpanAttribute, val string) { 244 if len(thread.stack) > 0 { 245 thread.stack[len(thread.stack)-1].attributes.addString(key, val) 246 } 247 } 248 249 // StartSegment begins a segment. 250 func StartSegment(t *TxnData, thread *Thread, now time.Time) SegmentStartTime { 251 tm := t.time(now) 252 thread.stack = append(thread.stack, segmentFrame{ 253 segmentTime: tm, 254 children: 0, 255 }) 256 257 return SegmentStartTime{ 258 Stamp: tm.Stamp, 259 Depth: len(thread.stack) - 1, 260 } 261 } 262 263 func (t *TxnData) getRootSpanID() string { 264 if "" == t.rootSpanID { 265 t.rootSpanID = t.TraceIDGenerator.GenerateTraceID() 266 } 267 return t.rootSpanID 268 } 269 270 // CurrentSpanIdentifier returns the identifier of the span at the top of the 271 // segment stack. 272 func (t *TxnData) CurrentSpanIdentifier(thread *Thread) string { 273 if 0 == len(thread.stack) { 274 return t.getRootSpanID() 275 } 276 if "" == thread.stack[len(thread.stack)-1].spanID { 277 thread.stack[len(thread.stack)-1].spanID = t.TraceIDGenerator.GenerateTraceID() 278 } 279 return thread.stack[len(thread.stack)-1].spanID 280 } 281 282 func (t *TxnData) saveSpanEvent(e *SpanEvent) { 283 e.Attributes = t.Attrs.filterSpanAttributes(e.Attributes, destSpan) 284 if len(t.spanEvents) < MaxSpanEvents { 285 t.spanEvents = append(t.spanEvents, e) 286 } 287 } 288 289 var ( 290 errMalformedSegment = errors.New("segment identifier malformed: perhaps unsafe code has modified it?") 291 errSegmentOrder = errors.New(`improper segment use: the Transaction must be used ` + 292 `in a single goroutine and segments must be ended in "last started first ended" order: ` + 293 `see https://github.com/newrelic/go-agent/blob/master/GUIDE.md#segments`) 294 ) 295 296 func endSegment(t *TxnData, thread *Thread, start SegmentStartTime, now time.Time) (segmentEnd, error) { 297 if 0 == start.Stamp { 298 return segmentEnd{}, errMalformedSegment 299 } 300 if start.Depth >= len(thread.stack) { 301 return segmentEnd{}, errSegmentOrder 302 } 303 if start.Depth < 0 { 304 return segmentEnd{}, errMalformedSegment 305 } 306 frame := thread.stack[start.Depth] 307 if start.Stamp != frame.Stamp { 308 return segmentEnd{}, errSegmentOrder 309 } 310 311 var children time.Duration 312 for i := start.Depth; i < len(thread.stack); i++ { 313 children += thread.stack[i].children 314 } 315 s := segmentEnd{ 316 stop: t.time(now), 317 start: frame.segmentTime, 318 attributes: frame.attributes, 319 } 320 if s.stop.Time.After(s.start.Time) { 321 s.duration = s.stop.Time.Sub(s.start.Time) 322 } 323 if s.duration > children { 324 s.exclusive = s.duration - children 325 } 326 327 // Note that we expect (depth == (len(t.stack) - 1)). However, if 328 // (depth < (len(t.stack) - 1)), that's ok: could be a panic popped 329 // some stack frames (and the consumer was not using defer). 330 331 if start.Depth > 0 { 332 thread.stack[start.Depth-1].children += s.duration 333 } 334 335 thread.stack = thread.stack[0:start.Depth] 336 337 if t.SpanEventsEnabled && t.LazilyCalculateSampled() { 338 s.SpanID = frame.spanID 339 if "" == s.SpanID { 340 s.SpanID = t.TraceIDGenerator.GenerateTraceID() 341 } 342 // Note that the current span identifier is the parent's 343 // identifier because we've already popped the segment that's 344 // ending off of the stack. 345 s.ParentID = t.CurrentSpanIdentifier(thread) 346 } 347 348 s.threadID = thread.threadID 349 350 thread.RecordActivity(s.start.Time) 351 thread.RecordActivity(s.stop.Time) 352 353 return s, nil 354 } 355 356 // EndBasicSegment ends a basic segment. 357 func EndBasicSegment(t *TxnData, thread *Thread, start SegmentStartTime, now time.Time, name string) error { 358 end, err := endSegment(t, thread, start, now) 359 if nil != err { 360 return err 361 } 362 if nil == t.customSegments { 363 t.customSegments = make(map[string]*metricData) 364 } 365 m := metricDataFromDuration(end.duration, end.exclusive) 366 if data, ok := t.customSegments[name]; ok { 367 data.aggregate(m) 368 } else { 369 // Use `new` in place of &m so that m is not 370 // automatically moved to the heap. 371 cpy := new(metricData) 372 *cpy = m 373 t.customSegments[name] = cpy 374 } 375 376 if t.TxnTrace.considerNode(end) { 377 attributes := end.attributes.copy() 378 t.saveTraceSegment(end, customSegmentMetric(name), attributes, "") 379 } 380 381 if evt := end.spanEvent(); evt != nil { 382 evt.Name = customSegmentMetric(name) 383 evt.Category = spanCategoryGeneric 384 t.saveSpanEvent(evt) 385 } 386 387 return nil 388 } 389 390 // EndExternalParams contains the parameters for EndExternalSegment. 391 type EndExternalParams struct { 392 TxnData *TxnData 393 Thread *Thread 394 Start SegmentStartTime 395 Now time.Time 396 Logger logger.Logger 397 Response *http.Response 398 URL *url.URL 399 Host string 400 Library string 401 Method string 402 } 403 404 // EndExternalSegment ends an external segment. 405 func EndExternalSegment(p EndExternalParams) error { 406 t := p.TxnData 407 end, err := endSegment(t, p.Thread, p.Start, p.Now) 408 if nil != err { 409 return err 410 } 411 412 // Use the Host field if present, otherwise use host in the URL. 413 if p.Host == "" && p.URL != nil { 414 p.Host = p.URL.Host 415 } 416 if p.Host == "" { 417 p.Host = "unknown" 418 } 419 if p.Library == "" { 420 p.Library = "http" 421 } 422 423 var appData *cat.AppDataHeader 424 if p.Response != nil { 425 hdr := HTTPHeaderToAppData(p.Response.Header) 426 appData, err = t.CrossProcess.ParseAppData(hdr) 427 if err != nil { 428 if p.Logger.DebugEnabled() { 429 p.Logger.Debug("failure to parse cross application response header", map[string]interface{}{ 430 "err": err.Error(), 431 "header": hdr, 432 }) 433 } 434 } 435 } 436 437 var crossProcessID string 438 var transactionName string 439 var transactionGUID string 440 if appData != nil { 441 crossProcessID = appData.CrossProcessID 442 transactionName = appData.TransactionName 443 transactionGUID = appData.TransactionGUID 444 } 445 446 key := externalMetricKey{ 447 Host: p.Host, 448 Library: p.Library, 449 Method: p.Method, 450 ExternalCrossProcessID: crossProcessID, 451 ExternalTransactionName: transactionName, 452 } 453 if nil == t.externalSegments { 454 t.externalSegments = make(map[externalMetricKey]*metricData) 455 } 456 t.externalCallCount++ 457 t.externalDuration += end.duration 458 m := metricDataFromDuration(end.duration, end.exclusive) 459 if data, ok := t.externalSegments[key]; ok { 460 data.aggregate(m) 461 } else { 462 // Use `new` in place of &m so that m is not 463 // automatically moved to the heap. 464 cpy := new(metricData) 465 *cpy = m 466 t.externalSegments[key] = cpy 467 } 468 469 if t.TxnTrace.considerNode(end) { 470 attributes := end.attributes.copy() 471 if p.Library == "http" { 472 attributes.addString(spanAttributeHTTPURL, SafeURL(p.URL)) 473 } 474 t.saveTraceSegment(end, key.scopedMetric(), attributes, transactionGUID) 475 } 476 477 if evt := end.spanEvent(); evt != nil { 478 evt.Name = key.scopedMetric() 479 evt.Category = spanCategoryHTTP 480 evt.Kind = "client" 481 evt.Component = p.Library 482 if p.Library == "http" { 483 evt.Attributes.addString(spanAttributeHTTPURL, SafeURL(p.URL)) 484 evt.Attributes.addString(spanAttributeHTTPMethod, p.Method) 485 } 486 t.saveSpanEvent(evt) 487 } 488 489 return nil 490 } 491 492 // EndMessageParams contains the parameters for EndMessageSegment. 493 type EndMessageParams struct { 494 TxnData *TxnData 495 Thread *Thread 496 Start SegmentStartTime 497 Now time.Time 498 Logger logger.Logger 499 DestinationName string 500 Library string 501 DestinationType string 502 DestinationTemp bool 503 } 504 505 // EndMessageSegment ends an external segment. 506 func EndMessageSegment(p EndMessageParams) error { 507 t := p.TxnData 508 end, err := endSegment(t, p.Thread, p.Start, p.Now) 509 if nil != err { 510 return err 511 } 512 513 key := MessageMetricKey{ 514 Library: p.Library, 515 DestinationType: p.DestinationType, 516 DestinationName: p.DestinationName, 517 DestinationTemp: p.DestinationTemp, 518 } 519 520 if nil == t.messageSegments { 521 t.messageSegments = make(map[MessageMetricKey]*metricData) 522 } 523 m := metricDataFromDuration(end.duration, end.exclusive) 524 if data, ok := t.messageSegments[key]; ok { 525 data.aggregate(m) 526 } else { 527 // Use `new` in place of &m so that m is not 528 // automatically moved to the heap. 529 cpy := new(metricData) 530 *cpy = m 531 t.messageSegments[key] = cpy 532 } 533 534 if t.TxnTrace.considerNode(end) { 535 attributes := end.attributes.copy() 536 t.saveTraceSegment(end, key.Name(), attributes, "") 537 } 538 539 if evt := end.spanEvent(); evt != nil { 540 evt.Name = key.Name() 541 evt.Category = spanCategoryGeneric 542 t.saveSpanEvent(evt) 543 } 544 545 return nil 546 } 547 548 // EndDatastoreParams contains the parameters for EndDatastoreSegment. 549 type EndDatastoreParams struct { 550 TxnData *TxnData 551 Thread *Thread 552 Start SegmentStartTime 553 Now time.Time 554 Product string 555 Collection string 556 Operation string 557 ParameterizedQuery string 558 QueryParameters map[string]interface{} 559 Host string 560 PortPathOrID string 561 Database string 562 } 563 564 const ( 565 unknownDatastoreHost = "unknown" 566 unknownDatastorePortPathOrID = "unknown" 567 ) 568 569 var ( 570 // ThisHost is the system hostname. 571 ThisHost = func() string { 572 if h, err := sysinfo.Hostname(); nil == err { 573 return h 574 } 575 return unknownDatastoreHost 576 }() 577 hostsToReplace = map[string]struct{}{ 578 "localhost": {}, 579 "127.0.0.1": {}, 580 "0.0.0.0": {}, 581 "0:0:0:0:0:0:0:1": {}, 582 "::1": {}, 583 "0:0:0:0:0:0:0:0": {}, 584 "::": {}, 585 } 586 ) 587 588 func (t TxnData) slowQueryWorthy(d time.Duration) bool { 589 return t.SlowQueriesEnabled && (d >= t.SlowQueryThreshold) 590 } 591 592 func datastoreSpanAddress(host, portPathOrID string) string { 593 if "" != host && "" != portPathOrID { 594 return host + ":" + portPathOrID 595 } 596 if "" != host { 597 return host 598 } 599 return portPathOrID 600 } 601 602 // EndDatastoreSegment ends a datastore segment. 603 func EndDatastoreSegment(p EndDatastoreParams) error { 604 end, err := endSegment(p.TxnData, p.Thread, p.Start, p.Now) 605 if nil != err { 606 return err 607 } 608 if p.Operation == "" { 609 p.Operation = datastoreOperationUnknown 610 } 611 if p.Product == "" { 612 p.Product = datastoreProductUnknown 613 } 614 if p.Host == "" && p.PortPathOrID != "" { 615 p.Host = unknownDatastoreHost 616 } 617 if p.PortPathOrID == "" && p.Host != "" { 618 p.PortPathOrID = unknownDatastorePortPathOrID 619 } 620 if _, ok := hostsToReplace[p.Host]; ok { 621 p.Host = ThisHost 622 } 623 624 // We still want to create a slowQuery if the consumer has not provided 625 // a Query string (or it has been removed by LASP) since the stack trace 626 // has value. 627 if p.ParameterizedQuery == "" { 628 collection := p.Collection 629 if "" == collection { 630 collection = "unknown" 631 } 632 p.ParameterizedQuery = fmt.Sprintf(`'%s' on '%s' using '%s'`, 633 p.Operation, collection, p.Product) 634 } 635 636 key := DatastoreMetricKey{ 637 Product: p.Product, 638 Collection: p.Collection, 639 Operation: p.Operation, 640 Host: p.Host, 641 PortPathOrID: p.PortPathOrID, 642 } 643 if nil == p.TxnData.datastoreSegments { 644 p.TxnData.datastoreSegments = make(map[DatastoreMetricKey]*metricData) 645 } 646 p.TxnData.datastoreCallCount++ 647 p.TxnData.datastoreDuration += end.duration 648 m := metricDataFromDuration(end.duration, end.exclusive) 649 if data, ok := p.TxnData.datastoreSegments[key]; ok { 650 data.aggregate(m) 651 } else { 652 // Use `new` in place of &m so that m is not 653 // automatically moved to the heap. 654 cpy := new(metricData) 655 *cpy = m 656 p.TxnData.datastoreSegments[key] = cpy 657 } 658 659 scopedMetric := datastoreScopedMetric(key) 660 // errors in QueryParameters must not stop the recording of the segment 661 queryParams, err := vetQueryParameters(p.QueryParameters) 662 663 if p.TxnData.TxnTrace.considerNode(end) { 664 attributes := end.attributes.copy() 665 attributes.addString(spanAttributeDBStatement, p.ParameterizedQuery) 666 attributes.addString(spanAttributeDBInstance, p.Database) 667 attributes.addString(spanAttributePeerAddress, datastoreSpanAddress(p.Host, p.PortPathOrID)) 668 attributes.addString(spanAttributePeerHostname, p.Host) 669 if len(queryParams) > 0 { 670 attributes.add(spanAttributeQueryParameters, queryParams) 671 } 672 p.TxnData.saveTraceSegment(end, scopedMetric, attributes, "") 673 } 674 675 if p.TxnData.slowQueryWorthy(end.duration) { 676 if nil == p.TxnData.SlowQueries { 677 p.TxnData.SlowQueries = newSlowQueries(maxTxnSlowQueries) 678 } 679 p.TxnData.SlowQueries.observeInstance(slowQueryInstance{ 680 Duration: end.duration, 681 DatastoreMetric: scopedMetric, 682 ParameterizedQuery: p.ParameterizedQuery, 683 QueryParameters: queryParams, 684 Host: p.Host, 685 PortPathOrID: p.PortPathOrID, 686 DatabaseName: p.Database, 687 StackTrace: GetStackTrace(), 688 }) 689 } 690 691 if evt := end.spanEvent(); evt != nil { 692 evt.Name = scopedMetric 693 evt.Category = spanCategoryDatastore 694 evt.Kind = "client" 695 evt.Component = p.Product 696 evt.Attributes.addString(spanAttributeDBStatement, p.ParameterizedQuery) 697 evt.Attributes.addString(spanAttributeDBInstance, p.Database) 698 evt.Attributes.addString(spanAttributePeerAddress, datastoreSpanAddress(p.Host, p.PortPathOrID)) 699 evt.Attributes.addString(spanAttributePeerHostname, p.Host) 700 evt.Attributes.addString(spanAttributeDBCollection, p.Collection) 701 p.TxnData.saveSpanEvent(evt) 702 } 703 704 return err 705 } 706 707 // MergeBreakdownMetrics creates segment metrics. 708 func MergeBreakdownMetrics(t *TxnData, metrics *metricTable) { 709 scope := t.FinalName 710 isWeb := t.IsWeb 711 // Custom Segment Metrics 712 for key, data := range t.customSegments { 713 name := customSegmentMetric(key) 714 // Unscoped 715 metrics.add(name, "", *data, unforced) 716 // Scoped 717 metrics.add(name, scope, *data, unforced) 718 } 719 720 // External Segment Metrics 721 for key, data := range t.externalSegments { 722 metrics.add(externalRollupMetric.all, "", *data, forced) 723 metrics.add(externalRollupMetric.webOrOther(isWeb), "", *data, forced) 724 725 hostMetric := externalHostMetric(key) 726 metrics.add(hostMetric, "", *data, unforced) 727 if "" != key.ExternalCrossProcessID && "" != key.ExternalTransactionName { 728 txnMetric := externalTransactionMetric(key) 729 730 // Unscoped CAT metrics 731 metrics.add(externalAppMetric(key), "", *data, unforced) 732 metrics.add(txnMetric, "", *data, unforced) 733 } 734 735 // Scoped External Metric 736 metrics.add(key.scopedMetric(), scope, *data, unforced) 737 } 738 739 // Datastore Segment Metrics 740 for key, data := range t.datastoreSegments { 741 metrics.add(datastoreRollupMetric.all, "", *data, forced) 742 metrics.add(datastoreRollupMetric.webOrOther(isWeb), "", *data, forced) 743 744 product := datastoreProductMetric(key) 745 metrics.add(product.all, "", *data, forced) 746 metrics.add(product.webOrOther(isWeb), "", *data, forced) 747 748 if key.Host != "" && key.PortPathOrID != "" { 749 instance := datastoreInstanceMetric(key) 750 metrics.add(instance, "", *data, unforced) 751 } 752 753 operation := datastoreOperationMetric(key) 754 metrics.add(operation, "", *data, unforced) 755 756 if "" != key.Collection { 757 statement := datastoreStatementMetric(key) 758 759 metrics.add(statement, "", *data, unforced) 760 metrics.add(statement, scope, *data, unforced) 761 } else { 762 metrics.add(operation, scope, *data, unforced) 763 } 764 } 765 // Message Segment Metrics 766 for key, data := range t.messageSegments { 767 metric := key.Name() 768 metrics.add(metric, scope, *data, unforced) 769 metrics.add(metric, "", *data, unforced) 770 } 771 }