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

Add span.context.destination.* #664

Merged
merged 1 commit into from Dec 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 33 additions & 1 deletion internal/apmhttputil/remoteaddr.go
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
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
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
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
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
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
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
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
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