Skip to content

Commit

Permalink
Add span.context.destination.*
Browse files Browse the repository at this point in the history
Add span.context fields:

 - destination.address
 - destination.port
 - destination.service.type
 - destination.service.name
 - destination.service.resource

Implement support for (client) HTTP,
Elasticsearch, PostgreSQL and MySQL
(via apmsql or GORM) spans.
  • Loading branch information
axw committed Dec 17, 2019
1 parent 545a860 commit 564672e
Show file tree
Hide file tree
Showing 29 changed files with 702 additions and 21 deletions.
34 changes: 33 additions & 1 deletion internal/apmhttputil/remoteaddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,42 @@ package apmhttputil

import (
"net/http"
"strconv"
)

// RemoteAddr returns the remote (peer) socket address for the HTTP request.
// RemoteAddr returns the remote (peer) socket address for req,
// a server HTTP request.
func RemoteAddr(req *http.Request) string {
remoteAddr, _ := splitHost(req.RemoteAddr)
return remoteAddr
}

// DestinationAddr returns the destination server address and port
// for req, a client HTTP request.
//
// If req.URL.Host contains a port it will be returned, and otherwise
// the default port according to req.URL.Scheme will be returned. If
// the included port is not a valid integer, or no port is included
// and the scheme is unknown, the returned port value will be zero.
func DestinationAddr(req *http.Request) (string, int) {
host, strport := splitHost(req.URL.Host)
var port int
if strport != "" {
port, _ = strconv.Atoi(strport)
} else {
port = SchemeDefaultPort(req.URL.Scheme)
}
return host, port
}

// SchemeDefaultPort returns the default port for the given URI scheme,
// if known, or 0 otherwise.
func SchemeDefaultPort(scheme string) int {
switch scheme {
case "http":
return 80
case "https":
return 443
}
return 0
}
23 changes: 23 additions & 0 deletions internal/apmhttputil/remoteaddr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ package apmhttputil_test

import (
"net/http"
"net/url"
"testing"

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

"go.elastic.co/apm/internal/apmhttputil"
)
Expand All @@ -35,3 +37,24 @@ func TestRemoteAddr(t *testing.T) {
req.Header.Set("X-Real-IP", "127.1.2.3")
assert.Equal(t, "::1", apmhttputil.RemoteAddr(req))
}

func TestDestinationAddr(t *testing.T) {
test := func(u, expectAddr string, expectPort int) {
t.Run(u, func(t *testing.T) {
url, err := url.Parse(u)
require.NoError(t, err)

addr, port := apmhttputil.DestinationAddr(&http.Request{URL: url})
assert.Equal(t, expectAddr, addr)
assert.Equal(t, expectPort, port)
})
}
test("http://127.0.0.1:80", "127.0.0.1", 80)
test("http://127.0.0.1", "127.0.0.1", 80)
test("https://127.0.0.1:443", "127.0.0.1", 443)
test("https://127.0.0.1", "127.0.0.1", 443)
test("https://[::1]", "::1", 443)
test("https://[::1]:1234", "::1", 1234)
test("gopher://gopher.invalid:70", "gopher.invalid", 70)
test("gopher://gopher.invalid", "gopher.invalid", 0) // default unknown
}
3 changes: 3 additions & 0 deletions internal/apmhttputil/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ func splitHost(in string) (host, port string) {
}
host, port, err := net.SplitHostPort(in)
if err != nil {
if n := len(in); n > 1 && in[0] == '[' && in[n-1] == ']' {
in = in[1 : n-1]
}
return in, ""
}
return host, port
Expand Down
1 change: 1 addition & 0 deletions internal/apmschema/jsonschema/request.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"type": ["boolean", "null"]
},
"remote_address": {
"description": "The network address sending the request. Should be obtained through standard APIs and not parsed from any headers like 'Forwarded'.",
"type": ["string", "null"]
}
}
Expand Down
37 changes: 37 additions & 0 deletions internal/apmschema/jsonschema/spans/span.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,43 @@
"type": ["object", "null"],
"description": "Any other arbitrary data captured by the agent, optionally provided by the user",
"properties": {
"destination": {
"type": ["object", "null"],
"description": "An object containing contextual data about the destination for spans",
"properties": {
"address": {
"type": ["string", "null"],
"description": "Destination network address: hostname (e.g. 'localhost'), FQDN (e.g. 'elastic.co'), IPv4 (e.g. '127.0.0.1') or IPv6 (e.g. '::1')",
"maxLength": 1024
},
"port": {
"type": ["integer", "null"],
"description": "Destination network port (e.g. 443)"
},
"service": {
"description": "Destination service context",
"type": ["object", "null"],
"properties": {
"type": {
"description": "Type of the destination service (e.g. 'db', 'elasticsearch'). Should typically be the same as span.type.",
"type": ["string", "null"],
"maxLength": 1024
},
"name": {
"description": "Identifier for the destination service (e.g. 'http://elastic.co', 'elasticsearch', 'rabbitmq')",
"type": ["string", "null"],
"maxLength": 1024
},
"resource": {
"description": "Identifier for the destination service resource being operated on (e.g. 'http://elastic.co:80', 'elasticsearch', 'rabbitmq/queue_name')",
"type": ["string", "null"],
"maxLength": 1024
}
},
"required": ["type", "name", "resource"]
}
}
},
"db": {
"type": ["object", "null"],
"description": "An object containing contextual data for database spans",
Expand Down
89 changes: 89 additions & 0 deletions model/marshal_fastjson.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,9 @@ type Span struct {

// SpanContext holds contextual information relating to the span.
type SpanContext struct {
// Destination holds information about a destination service.
Destination *DestinationSpanContext `json:"destination,omitempty"`

// Database holds contextual information for database
// operation spans.
Database *DatabaseSpanContext `json:"db,omitempty"`
Expand All @@ -274,6 +277,34 @@ type SpanContext struct {
Tags IfaceMap `json:"tags,omitempty"`
}

// DestinationSpanContext holds contextual information about the destination
// for a span that relates to an operation involving an external service.
type DestinationSpanContext struct {
// Address holds the network address of the destination service.
// This may be a hostname, FQDN, or (IPv4 or IPv6) network address.
Address string `json:"address,omitempty"`

// Port holds the network port for the destination service.
Port int `json:"port,omitempty"`

// Service holds additional destination service context.
Service *DestinationServiceSpanContext `json:"service,omitempty"`
}

// DestinationServiceSpanContext holds contextual information about a
// destination service,.
type DestinationServiceSpanContext struct {
// Type holds the destination service type.
Type string `json:"type,omitempty"`

// Name holds the destination service name.
Name string `json:"name,omitempty"`

// Resource identifies the destination service
// resource, e.g. a URI or message queue name.
Resource string `json:"resource,omitempty"`
}

// DatabaseSpanContext holds contextual information for database
// operation spans.
type DatabaseSpanContext struct {
Expand Down
5 changes: 5 additions & 0 deletions modelwriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ func (w *modelWriter) buildModelSpan(out *model.Span, span *Span, sd *SpanData)
out.Duration = sd.Duration.Seconds() * 1000
out.Context = sd.Context.build()

// Copy the span type to context.destination.service.type.
if out.Context != nil && out.Context.Destination != nil && out.Context.Destination.Service != nil {
out.Context.Destination.Service.Type = out.Type
}

w.modelStacktrace = appendModelStacktraceFrames(w.modelStacktrace, sd.stacktrace)
out.Stacktrace = w.modelStacktrace
w.setStacktraceContext(out.Stacktrace)
Expand Down
4 changes: 4 additions & 0 deletions module/apmelasticsearch/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
ctx = apm.ContextWithSpan(ctx, span)
req = apmhttp.RequestWithContext(ctx, req)
span.Context.SetHTTPRequest(req)
span.Context.SetDestinationService(apm.DestinationServiceSpanContext{
Name: "elasticsearch",
Resource: "elasticsearch",
})
span.Context.SetDatabase(apm.DatabaseSpanContext{
Type: "elasticsearch",
Statement: statement,
Expand Down
32 changes: 32 additions & 0 deletions module/apmelasticsearch/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,38 @@ func TestStatementBodyGzipContentEncoding(t *testing.T) {
}, spans[0].Context.Database)
}

func TestDestination(t *testing.T) {
var rt roundTripperFunc = func(req *http.Request) (*http.Response, error) {
return httptest.NewRecorder().Result(), nil
}
client := &http.Client{Transport: apmelasticsearch.WrapRoundTripper(rt)}

test := func(url, destinationAddr string, destinationPort int) {
req, err := http.NewRequest("GET", url, nil)
require.NoError(t, err)
_, spans, _ := apmtest.WithTransaction(func(ctx context.Context) {
resp, err := client.Do(req.WithContext(ctx))
assert.NoError(t, err)
resp.Body.Close()
})
require.Len(t, spans, 1)
assert.Equal(t, &model.DestinationSpanContext{
Address: destinationAddr,
Port: destinationPort,
Service: &model.DestinationServiceSpanContext{
Type: "db",
Name: "elasticsearch",
Resource: "elasticsearch",
},
}, spans[0].Context.Destination)
}
test("http://host:9200/_search", "host", 9200)
test("http://host:80/_search", "host", 80)
test("http://127.0.0.1:9200/_search", "127.0.0.1", 9200)
test("http://[2001:db8::1]:9200/_search", "2001:db8::1", 9200)
test("http://[2001:db8::1]:80/_search", "2001:db8::1", 80)
}

type errorReadCloser struct {
readError error
closed bool
Expand Down
Loading

0 comments on commit 564672e

Please sign in to comment.