Files
opentf/internal/httpclient/client.go
Martin Atkins 0503163e28 tracing: Centralize our OpenTelemetry package imports
OpenTelemetry has various Go packages split across several Go modules that
often need to be carefully upgraded together. And in particular, we are
using the "semconv" package in conjunction with the OpenTelemetry SDK's
"resource" package in a way that requires that they both agree on which
version of the OpenTelemetry Semantic Conventions are being followed.

To help avoid "dependency hell" situations when upgrading, this centralizes
all of our direct calls into the OpenTelemetry SDK and tracing API into
packages under internal/tracing, by exposing a few thin wrapper functions
that other packages can use to access the same functionality indirectly.

We only use a relatively small subset of the OpenTelemetry library surface
area, so we don't need too many of these reexports and they should not
represent a significant additional maintenance burden.

For the semconv and resource interaction in particular this also factors
that out into a separate helper function with a unit test, so we should
notice quickly whenever they become misaligned. This complements the
end-to-end test previously added in opentofu/opentofu#3447 to give us
faster feedback about this particular problem, while the end-to-end test
has the broader scope of making sure there aren't any errors at all when
initializing OpenTelemetry tracing.

Finally, this also replaces the constants we previously had in package
traceaddrs with functions that return attribute.KeyValue values directly.
This matches the API style used by the OpenTelemetry semconv packages, and
makes the calls to these helpers from elsewhere in the system a little
more concise.

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-30 13:27:10 -07:00

67 lines
2.7 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package httpclient
import (
"context"
"net/http"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"github.com/opentofu/opentofu/internal/tracing"
"github.com/opentofu/opentofu/version"
)
// New returns the DefaultPooledClient from the cleanhttp
// package that will also send a OpenTofu User-Agent string.
//
// If the given context has an active OpenTelemetry trace span associated with
// it then the returned client is also configured to collect traces for
// outgoing requests. However, those traces will be children of the span
// associated with the context passed _in each individual request_, rather
// than of the span in the context passed to this function; this function
// only checks for the presence of any span as a heuristic for whether the
// caller is in a part of the codebase that has OpenTelemetry plumbing in
// place, and does not actually make use of any information from that span.
func New(ctx context.Context) *http.Client {
cli := cleanhttp.DefaultPooledClient()
cli.Transport = &userAgentRoundTripper{
userAgent: OpenTofuUserAgent(version.Version),
inner: cli.Transport,
}
if span := tracing.SpanFromContext(ctx); span != nil && span.IsRecording() {
// We consider the presence of an active span -- that is, one whose
// presence is going to be reported to a trace collector outside of
// the OpenTofu process -- as sufficient signal that generating
// spans for requests made with the returned client will be useful.
//
// The following has two important implications:
// - Any request made using the returned client will generate an
// OpenTelemetry tracing span using the standard semantic conventions
// for an outgoing HTTP request. Therefore all requests made with
// this client must also be passed a context.Context carrying a
// suitable parent span that the request will be reported as a child
// of.
// - The outgoing request will include trace context metadata using
// the conventions from following specification, which would allow
// the recieving server to contribute its own child spans to the
// trace if it has access to the same collector:
//
// https://www.w3.org/TR/trace-context/
//
// We do this only when there seems to be an active span because
// otherwise each HTTP request without an active trace context will
// cause a separate trace to begin, containing only that HTTP request,
// which would create confusing noise for whoever is consuming the
// traces.
cli.Transport = otelhttp.NewTransport(cli.Transport)
}
return cli
}