Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

contrib/internal/httptrace: add support for inferred proxy spans #3052

Merged
merged 30 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6fb5400
add working draft inferred proxy spans
jordan-wong Dec 19, 2024
9762bfd
remove unnecessary setting of env vars in tests
jordan-wong Dec 19, 2024
63c8418
fix small logic error with env var check
jordan-wong Dec 19, 2024
5f16ca4
clean up print statements
jordan-wong Dec 20, 2024
10ed7f7
change print statements to log lines, make code more readable
jordan-wong Dec 20, 2024
923d28b
More cleanup
jordan-wong Dec 20, 2024
b356a70
add fallback value for service name using tracer ServiceName
jordan-wong Jan 6, 2025
7c93173
rewrite env var check with config
zarirhamza Jan 9, 2025
8da09a0
contrib/internal/httptrace: Update StartRequestSpan to infer AWS APIG…
zarirhamza Jan 10, 2025
ff31ebf
refactor code for readability
zarirhamza Jan 14, 2025
ce40b43
adds span links for inferred spans
zarirhamza Jan 15, 2025
a9aeec2
fix conflicts with spanlinks
zarirhamza Jan 15, 2025
43ff23f
Merge branch 'main' into add-inferred-proxy-spans
zarirhamza Jan 15, 2025
1ec1be9
fix copyright
zarirhamza Jan 15, 2025
92fe98b
compare http and apigw spans in tests
zarirhamza Jan 15, 2025
68be243
fix propagation tests
zarirhamza Jan 15, 2025
8be56b3
refactor
rarguelloF Jan 15, 2025
78c83fa
add new test
rarguelloF Jan 15, 2025
aceef66
Merge branch 'main' into add-inferred-proxy-spans
zarirhamza Jan 15, 2025
5510462
fix errors and refactor
zarirhamza Jan 15, 2025
6122e7d
refactor
zarirhamza Jan 15, 2025
a4d8959
address changes
zarirhamza Jan 19, 2025
2bb7737
Merge branch 'main' into add-inferred-proxy-spans
zarirhamza Jan 19, 2025
a7641c7
Merge branch 'main' into add-inferred-proxy-spans
rarguelloF Jan 21, 2025
16d7388
removes unnnecessary errors
zarirhamza Jan 21, 2025
4394d26
fix errors
zarirhamza Jan 21, 2025
d64e3f8
fix errors again
zarirhamza Jan 21, 2025
4a22660
adds assertions to test service and errors
zarirhamza Jan 21, 2025
78b3d4b
improve performance by isolating inferred span operations
zarirhamza Jan 22, 2025
39087f1
Merge branch 'main' into add-inferred-proxy-spans
zarirhamza Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions contrib/internal/httptrace/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const (
envTraceClientIPEnabled = "DD_TRACE_CLIENT_IP_ENABLED"
// envServerErrorStatuses is the name of the env var used to specify error status codes on http server spans
envServerErrorStatuses = "DD_TRACE_HTTP_SERVER_ERROR_STATUSES"

//used for enabling inferred span tracing
inferredProxyServicesEnabled = "DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED"
)

// defaultQueryStringRegexp is the regexp used for query string obfuscation if `envQueryStringRegexp` is empty.
Expand Down
15 changes: 14 additions & 1 deletion contrib/internal/httptrace/httptrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer.
if cfg.Tags == nil {
cfg.Tags = make(map[string]interface{})
}

cfg.Tags[ext.SpanType] = ext.SpanTypeWeb
cfg.Tags[ext.HTTPMethod] = r.Method
cfg.Tags[ext.HTTPURL] = urlFromRequest(r)
Expand All @@ -50,9 +51,21 @@ func StartRequestSpan(r *http.Request, opts ...ddtrace.StartSpanOption) (tracer.
if r.Host != "" {
cfg.Tags["http.host"] = r.Host
}
if spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header)); err == nil {

spanctx, err := tracer.Extract(tracer.HTTPHeadersCarrier(r.Header))
is_inferred_proxy_set := false

if internal.BoolEnv(inferredProxyServicesEnabled, false) {
if inferred_proxy_span_ctx := tryCreateInferredProxySpan(r.Header, spanctx); inferred_proxy_span_ctx != nil {
cfg.Parent = inferred_proxy_span_ctx
is_inferred_proxy_set = true
}
}

if err != nil && !is_inferred_proxy_set {
cfg.Parent = spanctx
}

for k, v := range ipTags {
cfg.Tags[k] = v
}
Expand Down
201 changes: 201 additions & 0 deletions contrib/internal/httptrace/httptrace_api_gateway_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package httptrace

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal/normalizer"

"github.com/stretchr/testify/assert"
)

var appListener *httptest.Server
zarirhamza marked this conversation as resolved.
Show resolved Hide resolved
var inferredHeaders = map[string]string{
"x-dd-proxy": "aws-apigateway",
"x-dd-proxy-request-time-ms": "1729780025473",
"x-dd-proxy-path": "/test",
"x-dd-proxy-httpmethod": "GET",
"x-dd-proxy-domain-name": "example.com",
"x-dd-proxy-stage": "dev",
}

// mock the aws server
func loadTest(t *testing.T) {
// Set environment variables
t.Setenv("DD_SERVICE", "aws-server")
t.Setenv("DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED", "true")

// set up http server
mux := http.NewServeMux()

// set routes
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/error" {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"message": "ERROR"})
} else {
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "OK"})
}
})
appListener = httptest.NewServer(mux)

}
func cleanupTest() {
// close server
if appListener != nil {
appListener.Close()
}
}

func TestInferredProxySpans(t *testing.T) {

t.Run("should create parent and child spans for a 200", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
loadTest(t)
defer cleanupTest()

client := &http.Client{}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/", appListener.URL), nil)

assert := assert.New(t)
assert.NoError(err)

for k, v := range inferredHeaders {
req.Header.Set(k, v)
}

sp, _ := StartRequestSpan(req)
resp, err := client.Do(req)
FinishRequestSpan(sp, resp.StatusCode, nil)

spans := mt.FinishedSpans()

assert.NoError(err)
assert.Equal(http.StatusOK, resp.StatusCode)

assert.Equal(2, len(spans))
gateway_span := spans[0]
web_req_span := spans[1]
assert.Equal("aws.apigateway", gateway_span.OperationName())
zarirhamza marked this conversation as resolved.
Show resolved Hide resolved
assert.Equal("http.request", web_req_span.OperationName())
assert.True(web_req_span.ParentID() == gateway_span.SpanID())
for _, arg := range inferredHeaders {
header, tag := normalizer.HeaderTag(arg)

// Default to an empty string if the tag does not exist
gateway_span_tags, exists := gateway_span.Tags()[tag]
if !exists {
gateway_span_tags = ""
}
expected_tags := strings.Join(req.Header.Values(header), ",")
// compare expected and actual values
assert.Equal(expected_tags, gateway_span_tags)
}

assert.Equal(2, len(spans))

})

t.Run("should create parent and child spans for error", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
loadTest(t)
defer cleanupTest()

client := &http.Client{}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/error", appListener.URL), nil)
assert := assert.New(t)
assert.NoError(err)
for k, v := range inferredHeaders {
req.Header.Set(k, v)
}

sp, _ := StartRequestSpan(req)
resp, err := client.Do(req)
FinishRequestSpan(sp, resp.StatusCode, nil)

assert.NoError(err)
assert.Equal(http.StatusInternalServerError, resp.StatusCode)

spans := mt.FinishedSpans()
assert.Equal(2, len(spans))
gateway_span := spans[0]
web_req_span := spans[1]
assert.Equal("aws.apigateway", gateway_span.OperationName())
assert.Equal("http.request", web_req_span.OperationName())
assert.True(web_req_span.ParentID() == gateway_span.SpanID())
for _, arg := range inferredHeaders {
header, tag := normalizer.HeaderTag(arg)

// Default to an empty string if the tag does not exist
gateway_span_tags, exists := gateway_span.Tags()[tag]
if !exists {
gateway_span_tags = ""
}
expected_tags := strings.Join(req.Header.Values(header), ",")
// compare expected and actual values
assert.Equal(expected_tags, gateway_span_tags)
}
assert.Equal(2, len(spans))

})

t.Run("should not create API Gateway spanif headers are missing", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
loadTest(t)
defer cleanupTest()

client := &http.Client{}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/no-aws-headers", appListener.URL), nil)
assert := assert.New(t)
assert.NoError(err)

sp, _ := StartRequestSpan(req)
resp, err := client.Do(req)
FinishRequestSpan(sp, resp.StatusCode, nil)
assert.NoError(err)
assert.Equal(http.StatusOK, resp.StatusCode)

spans := mt.FinishedSpans()
assert.Equal(1, len(spans))
assert.Equal("http.request", spans[0].OperationName())

})
t.Run("should not create API Gateway span if x-dd-proxy is missing", func(t *testing.T) {
mt := mocktracer.Start()
defer mt.Stop()
loadTest(t)
defer cleanupTest()

client := &http.Client{}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/no-aws-headers", appListener.URL), nil)
assert := assert.New(t)
assert.NoError(err)

for k, v := range inferredHeaders {
if k != "x-dd-proxy" {
req.Header.Set(k, v)
}
}

sp, _ := StartRequestSpan(req)
resp, err := client.Do(req)
FinishRequestSpan(sp, resp.StatusCode, nil)

assert.NoError(err)
assert.Equal(http.StatusOK, resp.StatusCode)

spans := mt.FinishedSpans()
assert.Equal(1, len(spans))
assert.Equal("http.request", spans[0].OperationName())

})
}
133 changes: 133 additions & 0 deletions contrib/internal/httptrace/inferred_proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package httptrace

import (
"net/http"
"strconv"
"time"

"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
"gopkg.in/DataDog/dd-trace-go.v1/internal"
"gopkg.in/DataDog/dd-trace-go.v1/internal/log"
)

const (
PROXY_HEADER_SYSTEM = "X-Dd-Proxy"
//PROXY_HEADER_START_TIME_MS = "x-dd-proxy-request-time-ms"
PROXY_HEADER_START_TIME_MS = "X-Dd-Proxy-Request-Time-Ms"
PROXY_HEADER_PATH = "X-Dd-Proxy-Path"
PROXY_HEADER_HTTPMETHOD = "X-Dd-Proxy-Httpmethod"
PROXY_HEADER_DOMAIN = "X-Dd-Proxy-Domain-Name"
PROXY_HEADER_STAGE = "X-Dd-Proxy-Stage"
zarirhamza marked this conversation as resolved.
Show resolved Hide resolved
)

type ProxyDetails struct {
SpanName string `json:"spanName"`
zarirhamza marked this conversation as resolved.
Show resolved Hide resolved
Component string `json:"component"`
}
zarirhamza marked this conversation as resolved.
Show resolved Hide resolved

var (
supportedProxies = map[string]ProxyDetails{
"aws-apigateway": {
SpanName: "aws.apigateway",
Component: "aws-apigateway",
},
}
)

type ProxyContext struct {
RequestTime string `json:"requestTime"`
zarirhamza marked this conversation as resolved.
Show resolved Hide resolved
Method string `json:"method"`
Path string `json:"path"`
Stage string `json:"stage"`
DomainName string `json:"domainName"`
ProxySystemName string `json:"proxySystemName"`
}
zarirhamza marked this conversation as resolved.
Show resolved Hide resolved

func extractInferredProxyContext(headers http.Header) *ProxyContext {
zarirhamza marked this conversation as resolved.
Show resolved Hide resolved
//proxyContent := make(map[string][]string)

_, exists := headers[PROXY_HEADER_START_TIME_MS]
if !exists {
log.Debug("Proxy header start time does not exist")
return nil
}

proxyHeaderSystem, exists := headers[PROXY_HEADER_SYSTEM]
if !exists {
log.Debug("Proxy header system does not exist")
return nil
}
if _, ok := supportedProxies[proxyHeaderSystem[0]]; !ok {
log.Debug("Unsupported Proxy header system")
return nil
}

// Q: is it possible to have multiple values for any of these http headers??
zarirhamza marked this conversation as resolved.
Show resolved Hide resolved
return &ProxyContext{
RequestTime: headers[PROXY_HEADER_START_TIME_MS][0],
Method: headers[PROXY_HEADER_HTTPMETHOD][0],
Path: headers[PROXY_HEADER_PATH][0],
Stage: headers[PROXY_HEADER_STAGE][0],
DomainName: headers[PROXY_HEADER_DOMAIN][0],
ProxySystemName: headers[PROXY_HEADER_SYSTEM][0],
}

}

func tryCreateInferredProxySpan(headers http.Header, parent ddtrace.SpanContext) ddtrace.SpanContext {
if headers == nil {
log.Debug("Headers do not exist")
return nil

}
if !internal.BoolEnv(inferredProxyServicesEnabled, false) {
log.Debug("The inferred proxy services are not enabled")
return nil
}

requestProxyContext := extractInferredProxyContext(headers)
if requestProxyContext == nil {
log.Debug("Unabole to extract inferred proxy context")
return nil
}

proxySpanInfo := supportedProxies[requestProxyContext.ProxySystemName]
log.Debug(`Successfully extracted inferred span info ${proxyContext} for proxy: ${proxyContext.proxySystemName}`)

// Parse Time string to Time Type
millis, err := strconv.ParseInt(requestProxyContext.RequestTime, 10, 64)
if err != nil {
log.Debug("Error parsing time string: %v", err)
return nil
}

// Convert milliseconds to seconds and nanoseconds
seconds := millis / 1000
nanoseconds := (millis % 1000) * int64(time.Millisecond)

// Create time.Time from Unix timestamp
parsedTime := time.Unix(seconds, nanoseconds)

config := ddtrace.StartSpanConfig{
Parent: parent,
//StartTime: requestProxyContext.RequestTime,
StartTime: parsedTime,
Tags: map[string]interface{}{
"service": requestProxyContext.DomainName,
jordan-wong marked this conversation as resolved.
Show resolved Hide resolved
"HTTP_METHOD": requestProxyContext.Method,
"PATH": requestProxyContext.Path,
"STAGE": requestProxyContext.Stage,
"DOMAIN_NAME": requestProxyContext.DomainName,
"PROXY_SYSTEM_NAME": requestProxyContext.ProxySystemName,
},
}

span := tracer.StartSpan(proxySpanInfo.SpanName, tracer.StartTime(config.StartTime), tracer.ChildOf(config.Parent), tracer.Tag("service", config.Tags["service"]))
defer span.Finish()
for k, v := range config.Tags {
span.SetTag(k, v)
}

return span.Context()
}
Loading