-
Notifications
You must be signed in to change notification settings - Fork 936
feat: add tracing for sql #1610
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
06ca51b
f8c3ab7
93ec818
b839175
25d5cb1
3eeda21
69ca227
af7c5ef
46d9217
2005d4c
cc5e0b0
5b9b654
a2ddd77
4f401e1
2cb35aa
a4c4683
0f5519b
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 |
---|---|---|
|
@@ -6,24 +6,23 @@ import ( | |
|
||
"github.com/go-chi/chi/middleware" | ||
"github.com/go-chi/chi/v5" | ||
"go.opentelemetry.io/otel/attribute" | ||
"go.opentelemetry.io/otel/codes" | ||
sdktrace "go.opentelemetry.io/otel/sdk/trace" | ||
semconv "go.opentelemetry.io/otel/semconv/v1.10.0" | ||
) | ||
|
||
// HTTPMW adds tracing to http routes. | ||
func HTTPMW(tracerProvider *sdktrace.TracerProvider, name string) func(http.Handler) http.Handler { | ||
return func(next http.Handler) http.Handler { | ||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { | ||
// do not trace if exporter has not be initialized | ||
if tracerProvider == nil { | ||
next.ServeHTTP(rw, r) | ||
return | ||
} | ||
|
||
// start span with default span name. Span name will be updated to "method route" format once request finishes. | ||
_, span := tracerProvider.Tracer(name).Start(r.Context(), fmt.Sprintf("%s %s", r.Method, r.RequestURI)) | ||
ctx, span := tracerProvider.Tracer(name).Start(r.Context(), fmt.Sprintf("%s %s", r.Method, r.RequestURI)) | ||
defer span.End() | ||
r = r.WithContext(ctx) | ||
|
||
wrw := middleware.NewWrapResponseWriter(rw, r.ProtoMajor) | ||
|
||
|
@@ -35,34 +34,21 @@ func HTTPMW(tracerProvider *sdktrace.TracerProvider, name string) func(http.Hand | |
if route != "" { | ||
span.SetName(fmt.Sprintf("%s %s", r.Method, route)) | ||
} | ||
span.SetAttributes(attribute.KeyValue{ | ||
Key: "http.method", | ||
Value: attribute.StringValue(r.Method), | ||
}) | ||
span.SetAttributes(attribute.KeyValue{ | ||
Key: "http.route", | ||
Value: attribute.StringValue(route), | ||
}) | ||
span.SetAttributes(attribute.KeyValue{ | ||
Key: "http.path", | ||
Value: attribute.StringValue(r.URL.EscapedPath()), | ||
}) | ||
span.SetName(fmt.Sprintf("%s %s", r.Method, route)) | ||
span.SetAttributes(semconv.NetAttributesFromHTTPRequest("tcp", r)...) | ||
span.SetAttributes(semconv.EndUserAttributesFromHTTPRequest(r)...) | ||
span.SetAttributes(semconv.HTTPServerAttributesFromHTTPRequest("", route, r)...) | ||
span.SetAttributes(semconv.HTTPRouteKey.String(route)) | ||
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. Nice change with the |
||
|
||
// set the status code | ||
status := wrw.Status() | ||
// 0 status means one has not yet been sent in which case net/http library will write StatusOK | ||
if status == 0 { | ||
status = http.StatusOK | ||
} | ||
span.SetAttributes(attribute.KeyValue{ | ||
Key: "http.status_code", | ||
Value: attribute.IntValue(status), | ||
}) | ||
|
||
// if 5XX we set the span to "error" status | ||
if status >= 500 { | ||
span.SetStatus(codes.Error, fmt.Sprintf("%d: %s", status, http.StatusText(status))) | ||
} | ||
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(status)) | ||
spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCode(status) | ||
span.SetStatus(spanStatus, spanMessage) | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package tracing | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/nhatthm/otelsql" | ||
semconv "go.opentelemetry.io/otel/semconv/v1.7.0" | ||
"go.opentelemetry.io/otel/trace" | ||
"golang.org/x/xerrors" | ||
) | ||
|
||
// Postgres driver will register a new tracing sql driver and return the driver name. | ||
func PostgresDriver(tp trace.TracerProvider, service string) (string, error) { | ||
// Register the otelsql wrapper for the provided postgres driver. | ||
driverName, err := otelsql.Register("postgres", | ||
otelsql.WithDefaultAttributes( | ||
semconv.ServiceNameKey.String(service), | ||
), | ||
otelsql.TraceQueryWithoutArgs(), | ||
otelsql.WithSystem(semconv.DBSystemPostgreSQL), | ||
otelsql.WithTracerProvider(tp), | ||
otelsql.WithSpanNameFormatter(formatPostgresSpan), | ||
) | ||
if err != nil { | ||
return "", xerrors.Errorf("registering postgres tracing driver: %w", err) | ||
} | ||
|
||
return driverName, nil | ||
} | ||
|
||
func formatPostgresSpan(ctx context.Context, op string) string { | ||
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 add the 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 just copying the format I've seen before with other database tracing tools (New Relic) and also mimicing the format of the http spans. Since an http span is I don't love the prefix because I already can see where the span is coming from by the instrumentation library (see the screenshots in description), but I also don't know if this is the case on other tools like Jaeger, which I'd like to setup eventually. I'm down to change it if we find it hard to parse as we use it, does that sound ok? 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. Yeah, that sounds perfectly reasonable. 👍🏻 |
||
const qPrefix = "-- name: " | ||
q := otelsql.QueryFromContext(ctx) | ||
if q == "" || !strings.HasPrefix(q, qPrefix) { | ||
return strings.ToUpper(op) | ||
} | ||
|
||
// Remove the qPrefix and then grab the method name. | ||
// We expect the first line of the query to be in | ||
// the format "-- name: GetAPIKeyByID :one". | ||
s := strings.SplitN(strings.TrimPrefix(q, qPrefix), " ", 2)[0] | ||
return fmt.Sprintf("%s %s", strings.ToUpper(op), s) | ||
} |
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.
potential bug: If
tracing.PostgresDriver
encounters an error here,sqlDriver
will be the empty string; I believe that would cause us to fail to connect to the DB. We should probably fall back to the original driver name in this case and degrade gracefully.