Skip to content

Commit

Permalink
added support to enable opt-in for forwarding exit spans without an e…
Browse files Browse the repository at this point in the history
…ntry span (#684)

added support for opt-in exit spans without an entry span.
  • Loading branch information
Angith authored Mar 25, 2024
1 parent 98c5b1d commit 4b7a126
Show file tree
Hide file tree
Showing 10 changed files with 560 additions and 30 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ col.StartSpan("log.go", []ot.StartSpanOption{

This log can then be visualized in the dashboard under Analytics/Logs. You can add a filter by service name. In our example, the service name is "My Go App".

### Opt-in Exit Spans

Go tracer support the opt-in feature for the exit spans. When enabled, the collector can start capturing exit spans, even without an entry span. This capability is particularly useful for scenarios like cronjobs and other background tasks, enabling the users to tailor the tracing according to their specific requirements. By setting the `INSTANA_ALLOW_ROOT_EXIT_SPAN` variable, users can choose whether the tracer should start a trace with an exit span or not. The environment variable can have 2 values. (1: Tracer should record exit spans for the outgoing calls, when it has no active entry span. 0 or any other values: Tracer should not start a trace with an exit span).

```bash
export INSTANA_ALLOW_ROOT_EXIT_SPAN=1
```

### Complete Example

[Basic Usage](./example/basic_usage/main.go)
Expand Down
20 changes: 12 additions & 8 deletions instrumentation_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,23 +182,27 @@ func RoundTripper(sensor TracerLogger, original http.RoundTripper) http.RoundTri
}
return tracingRoundTripper(func(req *http.Request) (*http.Response, error) {
ctx := req.Context()
parentSpan, ok := SpanFromContext(ctx)
if !ok {
// don't trace the exit call if there was no entry span provided
return original.RoundTrip(req)
}

sanitizedURL := cloneURL(req.URL)
sanitizedURL.RawQuery = ""
sanitizedURL.User = nil

span := sensor.Tracer().StartSpan("http",
opts := []ot.StartSpanOption{
ext.SpanKindRPCClient,
ot.ChildOf(parentSpan.Context()),
ot.Tags{
"http.url": sanitizedURL.String(),
"http.method": req.Method,
})
},
}

tracer := sensor.Tracer()
parentSpan, ok := SpanFromContext(ctx)
if ok {
tracer = parentSpan.Tracer()
opts = append(opts, ot.ChildOf(parentSpan.Context()))
}

span := tracer.StartSpan("http", opts...)
defer span.Finish()

// clone the request since the RoundTrip should not modify the original one
Expand Down
41 changes: 39 additions & 2 deletions instrumentation_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

Expand Down Expand Up @@ -580,8 +581,10 @@ func TestRoundTripper_WithoutParentSpan(t *testing.T) {
defer instana.ShutdownSensor()

rt := instana.RoundTripper(s, testRoundTripper(func(req *http.Request) (*http.Response, error) {
assert.Empty(t, req.Header.Get(instana.FieldT))
assert.Empty(t, req.Header.Get(instana.FieldS))
// These fields will be present, as an exit span would be created
// However the exit spans will not be recorded, as they are discarded before sending to the agent.
assert.NotEmpty(t, req.Header.Get(instana.FieldT))
assert.NotEmpty(t, req.Header.Get(instana.FieldS))

return &http.Response{
Status: http.StatusText(http.StatusNotImplemented),
Expand All @@ -596,6 +599,40 @@ func TestRoundTripper_WithoutParentSpan(t *testing.T) {
assert.Empty(t, recorder.GetQueuedSpans())
}

func TestRoundTripper_AllowRootExitSpan(t *testing.T) {

os.Setenv("INSTANA_ALLOW_ROOT_EXIT_SPAN", "1")
defer func() {
os.Unsetenv("INSTANA_ALLOW_ROOT_EXIT_SPAN")
}()

recorder := instana.NewTestRecorder()
s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{AgentClient: alwaysReadyClient{}}, recorder))
defer instana.ShutdownSensor()

rt := instana.RoundTripper(s, testRoundTripper(func(req *http.Request) (*http.Response, error) {
// These fields will be present as an exit span would be created
assert.NotEmpty(t, req.Header.Get(instana.FieldT))
assert.NotEmpty(t, req.Header.Get(instana.FieldS))

return &http.Response{
Status: http.StatusText(http.StatusNotImplemented),
StatusCode: http.StatusNotImplemented,
}, nil
}))

resp, err := rt.RoundTrip(httptest.NewRequest("GET", "http://example.com/hello", nil))
require.NoError(t, err)
assert.Equal(t, http.StatusNotImplemented, resp.StatusCode)

// the spans are present in the recorder as INSTANA_ALLOW_ROOT_EXIT_SPAN is configured
spans := recorder.GetQueuedSpans()
require.Len(t, spans, 1)
span := spans[0]
assert.Equal(t, 0, span.Ec)
assert.EqualValues(t, instana.ExitSpanKind, span.Kind)
}

func TestRoundTripper_Error(t *testing.T) {
serverErr := errors.New("something went wrong")

Expand Down
36 changes: 32 additions & 4 deletions instrumentation_sql_go1.10_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
)

func TestWrapSQLConnector_Exec(t *testing.T) {

recorder := instana.NewTestRecorder()
s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{
Service: "go-sensor-test",
Expand All @@ -27,7 +28,13 @@ func TestWrapSQLConnector_Exec(t *testing.T) {

db := sql.OpenDB(instana.WrapSQLConnector(s, "connection string", sqlConnector{}))

res, err := db.Exec("TEST QUERY")
pSpan := s.Tracer().StartSpan("parent-span")
ctx := context.Background()
if pSpan != nil {
ctx = instana.ContextWithSpan(ctx, pSpan)
}

res, err := db.ExecContext(ctx, "TEST QUERY")
require.NoError(t, err)

lastID, err := res.LastInsertId()
Expand Down Expand Up @@ -60,18 +67,25 @@ func TestWrapSQLConnector_Exec(t *testing.T) {
}

func TestWrapSQLConnector_Exec_Error(t *testing.T) {

recorder := instana.NewTestRecorder()
s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{
Service: "go-sensor-test",
AgentClient: alwaysReadyClient{},
}, recorder))
defer instana.ShutdownSensor()

pSpan := s.Tracer().StartSpan("parent-span")
ctx := context.Background()
if pSpan != nil {
ctx = instana.ContextWithSpan(ctx, pSpan)
}

db := sql.OpenDB(instana.WrapSQLConnector(s, "connection string", sqlConnector{
Error: errors.New("something went wrong"),
}))

_, err := db.Exec("TEST QUERY")
_, err := db.ExecContext(ctx, "TEST QUERY")
assert.Error(t, err)

spans := recorder.GetQueuedSpans()
Expand Down Expand Up @@ -101,16 +115,23 @@ func TestWrapSQLConnector_Exec_Error(t *testing.T) {
}

func TestWrapSQLConnector_Query(t *testing.T) {

recorder := instana.NewTestRecorder()
s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{
Service: "go-sensor-test",
AgentClient: alwaysReadyClient{},
}, recorder))
defer instana.ShutdownSensor()

pSpan := s.Tracer().StartSpan("parent-span")
ctx := context.Background()
if pSpan != nil {
ctx = instana.ContextWithSpan(ctx, pSpan)
}

db := sql.OpenDB(instana.WrapSQLConnector(s, "connection string", sqlConnector{}))

res, err := db.Query("TEST QUERY")
res, err := db.QueryContext(ctx, "TEST QUERY")
require.NoError(t, err)

cols, err := res.Columns()
Expand Down Expand Up @@ -143,6 +164,7 @@ func TestWrapSQLConnector_Query(t *testing.T) {
}

func TestWrapSQLConnector_Query_Error(t *testing.T) {

recorder := instana.NewTestRecorder()
s := instana.NewSensorWithTracer(instana.NewTracerWithEverything(&instana.Options{
Service: "go-sensor-test",
Expand All @@ -155,7 +177,13 @@ func TestWrapSQLConnector_Query_Error(t *testing.T) {
Error: dbErr,
}))

_, err := db.Query("TEST QUERY")
pSpan := s.Tracer().StartSpan("parent-span")
ctx := context.Background()
if pSpan != nil {
ctx = instana.ContextWithSpan(ctx, pSpan)
}

_, err := db.QueryContext(ctx, "TEST QUERY")
assert.Error(t, err)

spans := recorder.GetQueuedSpans()
Expand Down
Loading

0 comments on commit 4b7a126

Please sign in to comment.