mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: make trace provider in loadtest, add tracing to sdk (#4939)
This commit is contained in:
+118
-11
@@ -12,7 +12,15 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/propagation"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.11.0"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/coderd/tracing"
|
||||
|
||||
"cdr.dev/slog"
|
||||
)
|
||||
|
||||
// These cookies are Coder-specific. If a new one is added or changed, the name
|
||||
@@ -30,6 +38,13 @@ const (
|
||||
BypassRatelimitHeader = "X-Coder-Bypass-Ratelimit"
|
||||
)
|
||||
|
||||
var loggableMimeTypes = map[string]struct{}{
|
||||
"application/json": {},
|
||||
"text/plain": {},
|
||||
// lots of webserver error pages are HTML
|
||||
"text/html": {},
|
||||
}
|
||||
|
||||
// New creates a Coder client for the provided URL.
|
||||
func New(serverURL *url.URL) *Client {
|
||||
return &Client{
|
||||
@@ -45,9 +60,35 @@ type Client struct {
|
||||
SessionToken string
|
||||
URL *url.URL
|
||||
|
||||
// Logger can be provided to log requests. Request method, URL and response
|
||||
// status code will be logged by default.
|
||||
Logger slog.Logger
|
||||
// LogBodies determines whether the request and response bodies are logged
|
||||
// to the provided Logger. This is useful for debugging or testing.
|
||||
LogBodies bool
|
||||
|
||||
// BypassRatelimits is an optional flag that can be set by the site owner to
|
||||
// disable ratelimit checks for the client.
|
||||
BypassRatelimits bool
|
||||
|
||||
// PropagateTracing is an optional flag that can be set to propagate tracing
|
||||
// spans to the Coder API. This is useful for seeing the entire request
|
||||
// from end-to-end.
|
||||
PropagateTracing bool
|
||||
}
|
||||
|
||||
func (c *Client) Clone() *Client {
|
||||
hc := *c.HTTPClient
|
||||
u := *c.URL
|
||||
return &Client{
|
||||
HTTPClient: &hc,
|
||||
SessionToken: c.SessionToken,
|
||||
URL: &u,
|
||||
Logger: c.Logger,
|
||||
LogBodies: c.LogBodies,
|
||||
BypassRatelimits: c.BypassRatelimits,
|
||||
PropagateTracing: c.PropagateTracing,
|
||||
}
|
||||
}
|
||||
|
||||
type RequestOption func(*http.Request)
|
||||
@@ -63,30 +104,46 @@ func WithQueryParam(key, value string) RequestOption {
|
||||
}
|
||||
}
|
||||
|
||||
// Request performs an HTTP request with the body provided.
|
||||
// The caller is responsible for closing the response body.
|
||||
// Request performs a HTTP request with the body provided. The caller is
|
||||
// responsible for closing the response body.
|
||||
func (c *Client) Request(ctx context.Context, method, path string, body interface{}, opts ...RequestOption) (*http.Response, error) {
|
||||
ctx, span := tracing.StartSpanWithName(ctx, tracing.FuncNameSkip(1))
|
||||
defer span.End()
|
||||
|
||||
serverURL, err := c.URL.Parse(path)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("parse url: %w", err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
var r io.Reader
|
||||
if body != nil {
|
||||
if data, ok := body.([]byte); ok {
|
||||
buf = *bytes.NewBuffer(data)
|
||||
r = bytes.NewReader(data)
|
||||
} else {
|
||||
// Assume JSON if not bytes.
|
||||
enc := json.NewEncoder(&buf)
|
||||
buf := bytes.NewBuffer(nil)
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
err = enc.Encode(body)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("encode body: %w", err)
|
||||
}
|
||||
|
||||
r = buf
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, serverURL.String(), &buf)
|
||||
// Copy the request body so we can log it.
|
||||
var reqBody []byte
|
||||
if r != nil && c.LogBodies {
|
||||
reqBody, err = io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read request body: %w", err)
|
||||
}
|
||||
r = bytes.NewReader(reqBody)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, serverURL.String(), r)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create request: %w", err)
|
||||
}
|
||||
@@ -95,17 +152,61 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac
|
||||
req.Header.Set(BypassRatelimitHeader, "true")
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
if r != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(req)
|
||||
}
|
||||
|
||||
span.SetAttributes(semconv.NetAttributesFromHTTPRequest("tcp", req)...)
|
||||
span.SetAttributes(semconv.HTTPClientAttributesFromHTTPRequest(req)...)
|
||||
|
||||
// Inject tracing headers if enabled.
|
||||
if c.PropagateTracing {
|
||||
tmp := otel.GetTextMapPropagator()
|
||||
hc := propagation.HeaderCarrier(req.Header)
|
||||
tmp.Inject(ctx, hc)
|
||||
}
|
||||
|
||||
ctx = slog.With(ctx,
|
||||
slog.F("method", req.Method),
|
||||
slog.F("url", req.URL.String()),
|
||||
)
|
||||
c.Logger.Debug(ctx, "sdk request", slog.F("body", string(reqBody)))
|
||||
|
||||
resp, err := c.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("do: %w", err)
|
||||
}
|
||||
|
||||
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(resp.StatusCode))
|
||||
span.SetStatus(semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(resp.StatusCode, trace.SpanKindClient))
|
||||
|
||||
// Copy the response body so we can log it if it's a loggable mime type.
|
||||
var respBody []byte
|
||||
if resp.Body != nil && c.LogBodies {
|
||||
mimeType := parseMimeType(resp.Header.Get("Content-Type"))
|
||||
if _, ok := loggableMimeTypes[mimeType]; ok {
|
||||
respBody, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("copy response body for logs: %w", err)
|
||||
}
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("close response body: %w", err)
|
||||
}
|
||||
resp.Body = io.NopCloser(bytes.NewReader(respBody))
|
||||
}
|
||||
}
|
||||
|
||||
c.Logger.Debug(ctx, "sdk response",
|
||||
slog.F("status", resp.StatusCode),
|
||||
slog.F("body", string(respBody)),
|
||||
slog.F("trace_id", resp.Header.Get("X-Trace-Id")),
|
||||
slog.F("span_id", resp.Header.Get("X-Span-Id")),
|
||||
)
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
@@ -138,10 +239,7 @@ func readBodyAsError(res *http.Response) error {
|
||||
return xerrors.Errorf("read body: %w", err)
|
||||
}
|
||||
|
||||
mimeType, _, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
mimeType = strings.TrimSpace(strings.Split(contentType, ";")[0])
|
||||
}
|
||||
mimeType := parseMimeType(contentType)
|
||||
if mimeType != "application/json" {
|
||||
if len(resp) > 1024 {
|
||||
resp = append(resp[:1024], []byte("...")...)
|
||||
@@ -238,3 +336,12 @@ type closeFunc func() error
|
||||
func (c closeFunc) Close() error {
|
||||
return c()
|
||||
}
|
||||
|
||||
func parseMimeType(contentType string) string {
|
||||
mimeType, _, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
mimeType = strings.TrimSpace(strings.Split(contentType, ";")[0])
|
||||
}
|
||||
|
||||
return mimeType
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user