Skip to content

Commit 4f683d0

Browse files
fix(ddtrace/tracer): Implement Serverless Service Representation (#4043)
Co-authored-by: rithikanarayan <[email protected]> Co-authored-by: rithika.narayan <[email protected]>
1 parent d6f7d92 commit 4f683d0

File tree

4 files changed

+234
-8
lines changed

4 files changed

+234
-8
lines changed

ddtrace/tracer/option.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,10 @@ type config struct {
333333

334334
// llmobs contains the LLM Observability config
335335
llmobs llmobsconfig.Config
336+
337+
// isLambdaFunction, if true, indicates we are in a lambda function
338+
// It is set by checking for a nonempty LAMBDA_FUNCTION_NAME env var.
339+
isLambdaFunction bool
336340
}
337341

338342
// orchestrionConfig contains Orchestrion configuration.
@@ -463,10 +467,13 @@ func newConfig(opts ...StartOption) (*config, error) {
463467
// TODO: should we track the origin of these tags individually?
464468
c.globalTags.cfgOrigin = telemetry.OriginEnvVar
465469
}
466-
if _, ok := env.Lookup("AWS_LAMBDA_FUNCTION_NAME"); ok {
470+
if v, ok := env.Lookup("AWS_LAMBDA_FUNCTION_NAME"); ok {
467471
// AWS_LAMBDA_FUNCTION_NAME being set indicates that we're running in an AWS Lambda environment.
468472
// See: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html
469473
c.logToStdout = true
474+
if v != "" {
475+
c.isLambdaFunction = true
476+
}
470477
}
471478
c.logStartup = internal.BoolEnv("DD_TRACE_STARTUP_LOGS", true)
472479
c.runtimeMetrics = internal.BoolVal(getDDorOtelConfig("metrics"), false)

ddtrace/tracer/spancontext.go

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ func (t *trace) finishedOne(s *Span) {
536536
return
537537
}
538538
tc := tr.TracerConf()
539-
setPeerService(s, tc.PeerServiceDefaults, tc.PeerServiceMappings)
539+
setPeerService(s, tc)
540540

541541
// attach the _dd.base_service tag only when the globally configured service name is different from the
542542
// span service name.
@@ -614,13 +614,24 @@ func (t *trace) finishChunk(tr *tracer, ch *chunk) {
614614

615615
// setPeerService sets the peer.service, _dd.peer.service.source, and _dd.peer.service.remapped_from
616616
// tags as applicable for the given span.
617-
func setPeerService(s *Span, peerServiceDefaults bool, peerServiceMappings map[string]string) {
617+
func setPeerService(s *Span, tc TracerConf) {
618+
spanKind := s.meta[ext.SpanKind]
619+
isOutboundRequest := spanKind == ext.SpanKindClient || spanKind == ext.SpanKindProducer
620+
618621
if _, ok := s.meta[ext.PeerService]; ok { // peer.service already set on the span
619622
s.setMeta(keyPeerServiceSource, ext.PeerService)
623+
} else if isServerless(tc) {
624+
// Set peerService only in outbound Lambda requests
625+
if isOutboundRequest {
626+
if ps := deriveAWSPeerService(s.meta); ps != "" {
627+
s.setMeta(ext.PeerService, ps)
628+
s.setMeta(keyPeerServiceSource, ext.PeerService)
629+
} else {
630+
log.Debug("Unable to set peer.service tag for serverless span %q", s.name)
631+
}
632+
}
620633
} else { // no peer.service currently set
621-
spanKind := s.meta[ext.SpanKind]
622-
isOutboundRequest := spanKind == ext.SpanKindClient || spanKind == ext.SpanKindProducer
623-
shouldSetDefaultPeerService := isOutboundRequest && peerServiceDefaults
634+
shouldSetDefaultPeerService := isOutboundRequest && tc.PeerServiceDefaults
624635
if !shouldSetDefaultPeerService {
625636
return
626637
}
@@ -633,12 +644,59 @@ func setPeerService(s *Span, peerServiceDefaults bool, peerServiceMappings map[s
633644
}
634645
// Overwrite existing peer.service value if remapped by the user
635646
ps := s.meta[ext.PeerService]
636-
if to, ok := peerServiceMappings[ps]; ok {
647+
if to, ok := tc.PeerServiceMappings[ps]; ok {
637648
s.setMeta(keyPeerServiceRemappedFrom, ps)
638649
s.setMeta(ext.PeerService, to)
639650
}
640651
}
641652

653+
/*
654+
checks if we are in a serverless environment
655+
656+
TODO add checks for Azure functions and other serverless environments
657+
*/
658+
func isServerless(tc TracerConf) bool {
659+
return tc.isLambdaFunction
660+
}
661+
662+
/*
663+
deriveAWSPeerService returns the host name of the
664+
outbound aws service call based on the span metadata,
665+
or an empty string if it cannot be determined.
666+
667+
The mapping is as follows:
668+
- eventbridge: events.<region>.amazonaws.com
669+
- sqs: sqs.<region>.amazonaws.com
670+
- sns: sns.<region>.amazonaws.com
671+
- kinesis: kinesis.<region>.amazonaws.com
672+
- dynamodb: dynamodb.<region>.amazonaws.com
673+
- s3: <bucket>.s3.<region>.amazonaws.com (if Bucket param present)
674+
s3.<region>.amazonaws.com (otherwise)
675+
*/
676+
func deriveAWSPeerService(sm map[string]string) string {
677+
service, region := sm[ext.AWSService], sm[ext.AWSRegion]
678+
if service == "" || region == "" {
679+
return ""
680+
}
681+
682+
s := strings.ToLower(service)
683+
switch s {
684+
685+
case "s3":
686+
if bucket := sm[ext.S3BucketName]; bucket != "" {
687+
return bucket + ".s3." + region + ".amazonaws.com"
688+
}
689+
return "s3." + region + ".amazonaws.com"
690+
691+
case "eventbridge":
692+
return "events." + region + ".amazonaws.com"
693+
694+
case "sqs", "sns", "dynamodb", "kinesis":
695+
return s + "." + region + ".amazonaws.com"
696+
}
697+
return ""
698+
}
699+
642700
// setPeerServiceFromSource sets peer.service from the sources determined
643701
// by the tags on the span. It returns the source tag name that it used for
644702
// the peer.service value, or the empty string if no valid source tag was available.

ddtrace/tracer/spancontext_test.go

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"math"
1313
"strconv"
14+
"strings"
1415
"sync"
1516
"testing"
1617
"time"
@@ -529,6 +530,160 @@ func TestSpanPeerService(t *testing.T) {
529530
wantPeerServiceSource: "bucketname",
530531
wantPeerServiceRemappedFrom: "",
531532
},
533+
{
534+
name: "AWS-No-Service",
535+
spanOpts: []StartSpanOption{
536+
Tag("span.kind", "client"),
537+
Tag("region", "us-east-2"),
538+
Tag("db.system", "db-system"),
539+
Tag("db.name", "db-name"),
540+
},
541+
peerServiceDefaultsEnabled: true,
542+
peerServiceMappings: nil,
543+
wantPeerService: "",
544+
wantPeerServiceSource: "",
545+
wantPeerServiceRemappedFrom: "",
546+
},
547+
{
548+
name: "AWS-No-Region",
549+
spanOpts: []StartSpanOption{
550+
Tag("span.kind", "client"),
551+
Tag("aws_service", "S3"),
552+
Tag("db.system", "db-system"),
553+
Tag("db.name", "db-name"),
554+
},
555+
peerServiceDefaultsEnabled: true,
556+
peerServiceMappings: nil,
557+
wantPeerService: "",
558+
wantPeerServiceSource: "",
559+
wantPeerServiceRemappedFrom: "",
560+
},
561+
{
562+
name: "AWS-Nonexistent-Service-And-Region",
563+
spanOpts: []StartSpanOption{
564+
Tag("span.kind", "client"),
565+
Tag("aws_service", "notarealservice"),
566+
Tag("region", "notarealregion"),
567+
Tag("db.system", "db-system"),
568+
Tag("db.name", "db-name"),
569+
},
570+
peerServiceDefaultsEnabled: true,
571+
peerServiceMappings: nil,
572+
wantPeerService: "",
573+
wantPeerServiceSource: "",
574+
wantPeerServiceRemappedFrom: "",
575+
},
576+
{
577+
name: "AWS-No-Outbound-Request",
578+
spanOpts: []StartSpanOption{
579+
Tag("aws_service", "S3"),
580+
Tag("db.system", "db-system"),
581+
Tag("db.name", "db-name"),
582+
},
583+
peerServiceDefaultsEnabled: true,
584+
peerServiceMappings: nil,
585+
wantPeerService: "",
586+
wantPeerServiceSource: "",
587+
wantPeerServiceRemappedFrom: "",
588+
},
589+
{
590+
name: "AWS-S3",
591+
spanOpts: []StartSpanOption{
592+
Tag("span.kind", "client"),
593+
Tag("aws_service", "S3"),
594+
Tag("region", "us-east-2"),
595+
Tag("bucketname", "some-bucket"),
596+
Tag("db.system", "db-system"),
597+
Tag("db.name", "db-name"),
598+
},
599+
peerServiceDefaultsEnabled: true,
600+
peerServiceMappings: nil,
601+
wantPeerService: "some-bucket.s3.us-east-2.amazonaws.com",
602+
wantPeerServiceSource: "peer.service",
603+
wantPeerServiceRemappedFrom: "",
604+
},
605+
{
606+
name: "AWS-S3-No-Bucket",
607+
spanOpts: []StartSpanOption{
608+
Tag("span.kind", "client"),
609+
Tag("aws_service", "S3"),
610+
Tag("region", "us-east-2"),
611+
Tag("db.system", "db-system"),
612+
Tag("db.name", "db-name"),
613+
},
614+
peerServiceDefaultsEnabled: true,
615+
peerServiceMappings: nil,
616+
wantPeerService: "s3.us-east-2.amazonaws.com",
617+
wantPeerServiceSource: "peer.service",
618+
wantPeerServiceRemappedFrom: "",
619+
},
620+
{
621+
name: "AWS-DynamoDB",
622+
spanOpts: []StartSpanOption{
623+
Tag("span.kind", "client"),
624+
Tag("aws_service", "DynamoDB"),
625+
Tag("region", "us-east-2"),
626+
Tag("db.system", "db-system"),
627+
Tag("db.name", "db-name"),
628+
},
629+
peerServiceDefaultsEnabled: true,
630+
peerServiceMappings: nil,
631+
wantPeerService: "dynamodb.us-east-2.amazonaws.com",
632+
wantPeerServiceSource: "peer.service",
633+
wantPeerServiceRemappedFrom: "",
634+
},
635+
{
636+
name: "AWS-Kinesis",
637+
spanOpts: []StartSpanOption{
638+
Tag("span.kind", "client"),
639+
Tag("aws_service", "Kinesis"),
640+
Tag("region", "us-east-2"),
641+
},
642+
peerServiceDefaultsEnabled: true,
643+
peerServiceMappings: nil,
644+
wantPeerService: "kinesis.us-east-2.amazonaws.com",
645+
wantPeerServiceSource: "peer.service",
646+
wantPeerServiceRemappedFrom: "",
647+
},
648+
{
649+
name: "AWS-SNS",
650+
spanOpts: []StartSpanOption{
651+
Tag("span.kind", "client"),
652+
Tag("aws_service", "SNS"),
653+
Tag("region", "us-east-2"),
654+
},
655+
peerServiceDefaultsEnabled: true,
656+
peerServiceMappings: nil,
657+
wantPeerService: "sns.us-east-2.amazonaws.com",
658+
wantPeerServiceSource: "peer.service",
659+
wantPeerServiceRemappedFrom: "",
660+
},
661+
{
662+
name: "AWS-SQS",
663+
spanOpts: []StartSpanOption{
664+
Tag("span.kind", "client"),
665+
Tag("aws_service", "SQS"),
666+
Tag("region", "us-east-2"),
667+
},
668+
peerServiceDefaultsEnabled: true,
669+
peerServiceMappings: nil,
670+
wantPeerService: "sqs.us-east-2.amazonaws.com",
671+
wantPeerServiceSource: "peer.service",
672+
wantPeerServiceRemappedFrom: "",
673+
},
674+
{
675+
name: "AWS-Events",
676+
spanOpts: []StartSpanOption{
677+
Tag("span.kind", "client"),
678+
Tag("aws_service", "EventBridge"),
679+
Tag("region", "us-east-2"),
680+
},
681+
peerServiceDefaultsEnabled: true,
682+
peerServiceMappings: nil,
683+
wantPeerService: "events.us-east-2.amazonaws.com",
684+
wantPeerServiceSource: "peer.service",
685+
wantPeerServiceRemappedFrom: "",
686+
},
532687
{
533688
name: "DBClient",
534689
spanOpts: []StartSpanOption{
@@ -658,7 +813,11 @@ func TestSpanPeerService(t *testing.T) {
658813
}
659814
}
660815
t.Run(tc.name, func(t *testing.T) {
661-
tracer, transport, flush, stop, err := startTestTracer(t)
816+
if strings.Contains(tc.name, "AWS-") {
817+
t.Setenv("AWS_LAMBDA_FUNCTION_NAME", "test_name")
818+
}
819+
820+
tracer, transport, flush, stop, err := startTestTracer(t, WithLambdaMode(false))
662821
assert.Nil(t, err)
663822
defer stop()
664823

ddtrace/tracer/tracer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type TracerConf struct { //nolint:revive
5353
VersionTag string
5454
ServiceTag string
5555
TracingAsTransport bool
56+
isLambdaFunction bool
5657
}
5758

5859
// Tracer specifies an implementation of the Datadog tracer which allows starting
@@ -954,6 +955,7 @@ func (t *tracer) TracerConf() TracerConf {
954955
VersionTag: t.config.version,
955956
ServiceTag: t.config.serviceName,
956957
TracingAsTransport: t.config.tracingAsTransport,
958+
isLambdaFunction: t.config.isLambdaFunction,
957959
}
958960
}
959961

0 commit comments

Comments
 (0)