-
Notifications
You must be signed in to change notification settings - Fork 536
feat(tracer): Implement OTLP Traces Export #4600
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
Changes from all commits
8f7f60f
3e3fec8
c559f8f
cb6908e
a85fedf
33a9a37
3e51fdd
5e252be
51f72a2
b71eec7
f48d44f
f4360b4
49d0937
a88b7b2
d475920
8188bbe
af909c3
9246084
a934fdd
2cfa217
7bba29d
89cb7e6
da1efd4
49053c1
378639f
97a2bf9
ffbea4e
9882338
5072b2a
78e160f
f0c6f00
5ca4d0e
6deec12
996c4d7
697c2ba
d7749bb
dcd8a10
6dbab08
d0765c8
acc9688
490d6f1
c07bf3c
90dccdd
99ef3c8
ddfc0e2
c371138
ac556e6
cd63872
12d75b3
af69c6b
35cf849
6480f53
e8aab3a
4223bd3
037eec3
ca17d6a
63bc9ea
f9ae432
91fac2a
7c12988
f577ca2
ceea3dd
b3a521d
8d6156e
6ec6405
df091b3
92de62c
016cb71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| // Unless explicitly stated otherwise all files in this repository are licensed | ||
| // under the Apache License Version 2.0. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
| // Copyright 2016 Datadog, Inc. | ||
|
|
||
| package tracer | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| ) | ||
|
|
||
| // otlpTransport sends protobuf-encoded OTLP payloads over HTTP. | ||
| // It is the OTLP counterpart to httpTransport (which handles Datadog-protocol traffic). | ||
| type otlpTransport struct { | ||
| client *http.Client | ||
| endpoint string | ||
| customHeaders map[string]string | ||
| } | ||
|
|
||
| func newOTLPTransport(client *http.Client, endpoint string, customHeaders map[string]string) *otlpTransport { | ||
| return &otlpTransport{ | ||
| client: client, | ||
| endpoint: endpoint, | ||
| customHeaders: customHeaders, | ||
| } | ||
| } | ||
|
|
||
| // send posts a protobuf-encoded payload to the configured OTLP endpoint. | ||
| func (t *otlpTransport) send(data []byte) error { | ||
|
kakkoyun marked this conversation as resolved.
|
||
| req, err := http.NewRequest("POST", t.endpoint, bytes.NewReader(data)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use a context to be able to cancel the request in case there are timeouts?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure. We do have a client-set timeout (set when we initialize the client; value comes from tracer's httpClientTimeout config). The agent transport uses the same, context-less approach |
||
| if err != nil { | ||
| return fmt.Errorf("cannot create http request: %w", err) | ||
| } | ||
| req.Header.Set("Content-Type", "application/x-protobuf") | ||
| for header, value := range t.customHeaders { | ||
| req.Header.Set(header, value) | ||
| } | ||
| resp, err := t.client.Do(req) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer func() { | ||
| _, _ = io.Copy(io.Discard, resp.Body) | ||
| resp.Body.Close() | ||
| }() | ||
| if code := resp.StatusCode; code >= 400 { | ||
| return fmt.Errorf("HTTP %d: %s", code, http.StatusText(code)) | ||
| } | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| // Unless explicitly stated otherwise all files in this repository are licensed | ||
| // under the Apache License Version 2.0. | ||
| // This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
| // Copyright 2016 Datadog, Inc. | ||
|
|
||
| package tracer | ||
|
|
||
| import ( | ||
| "io" | ||
| "net" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "sync/atomic" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestOTLPTransportSendSuccess(t *testing.T) { | ||
| var received []byte | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| received, _ = io.ReadAll(r.Body) | ||
| w.WriteHeader(http.StatusOK) | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| tr := newOTLPTransport(srv.Client(), srv.URL, nil) | ||
| err := tr.send([]byte("hello")) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, []byte("hello"), received) | ||
| } | ||
|
|
||
| func TestOTLPTransportSendHeaders(t *testing.T) { | ||
| var gotHeaders http.Header | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| gotHeaders = r.Header.Clone() | ||
| w.WriteHeader(http.StatusOK) | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| tr := newOTLPTransport(srv.Client(), srv.URL, map[string]string{ | ||
| "Api-Key": "secret", | ||
| "X-Custom": "value", | ||
| }) | ||
| err := tr.send([]byte("data")) | ||
| require.NoError(t, err) | ||
|
|
||
| assert.Equal(t, "application/x-protobuf", gotHeaders.Get("Content-Type"), "default Content-Type must be set") | ||
| assert.Equal(t, "secret", gotHeaders.Get("Api-Key")) | ||
| assert.Equal(t, "value", gotHeaders.Get("X-Custom")) | ||
| } | ||
|
|
||
| func TestOTLPTransportSendHTTPMethod(t *testing.T) { | ||
| var gotMethod string | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| gotMethod = r.Method | ||
| w.WriteHeader(http.StatusOK) | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| tr := newOTLPTransport(srv.Client(), srv.URL, nil) | ||
| err := tr.send([]byte("data")) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "POST", gotMethod) | ||
| } | ||
|
|
||
| func TestOTLPTransportSendErrorStatus(t *testing.T) { | ||
| tests := []struct { | ||
| code int | ||
| text string | ||
| }{ | ||
| {http.StatusBadRequest, "Bad Request"}, | ||
| {http.StatusUnauthorized, "Unauthorized"}, | ||
| {http.StatusInternalServerError, "Internal Server Error"}, | ||
| {http.StatusServiceUnavailable, "Service Unavailable"}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.text, func(t *testing.T) { | ||
| srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.WriteHeader(tt.code) | ||
| })) | ||
| defer srv.Close() | ||
|
|
||
| tr := newOTLPTransport(srv.Client(), srv.URL, nil) | ||
| err := tr.send([]byte("data")) | ||
| require.Error(t, err) | ||
| assert.Contains(t, err.Error(), tt.text) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestOTLPTransportSendConnectionError(t *testing.T) { | ||
| tr := newOTLPTransport(http.DefaultClient, "http://127.0.0.1:0/nonexistent", nil) | ||
| err := tr.send([]byte("data")) | ||
| require.Error(t, err) | ||
| } | ||
|
|
||
| func TestOTLPTransportConnectionReuse(t *testing.T) { | ||
| var connCount int64 | ||
| srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| w.Write([]byte("response body that must be drained")) | ||
| })) | ||
| srv.Config.ConnState = func(_ net.Conn, state http.ConnState) { | ||
| if state == http.StateNew { | ||
| atomic.AddInt64(&connCount, 1) | ||
| } | ||
| } | ||
| srv.Start() | ||
| defer srv.Close() | ||
|
|
||
| tr := newOTLPTransport(srv.Client(), srv.URL, nil) | ||
| for range 5 { | ||
| require.NoError(t, tr.send([]byte("data"))) | ||
| } | ||
| assert.Equal(t, int64(1), atomic.LoadInt64(&connCount), | ||
| "expected a single connection to be reused across sends") | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍