mirror of
https://github.com/hanzoai/telemetry.git
synced 2026-10-30 20:01:49 +00:00
`make gotest` failed at cmd/builder with 16 failures, but only one was about
the builder. The other three could not even build a collector — the tree they
compile has been broken since an incomplete sync left calls without their
definitions.
confmap/internal/decoder.go called NewExpandedValueSanitizer and
sanitizeToStr. Both exist under the names they were generated with:
metadata.ConfmapNewExpandedValueSanitizerFeatureGate, and
sanitizeExpanded(data, true) — the string arm of the same helper
sanitize() takes the false arm of, which is what the comment above the
call describes.
service/metadata.yaml never declared telemetry.newPipelineTelemetry, so
mdatagen never generated the gate that 22 call sites use. Declared it as
the CHANGELOG describes it — alpha, v0.123.0, PR #12217 — and added the
generated entry, because mdatagen itself does not build (undefined:
cfggen, still outstanding).
confmap/xconfmap declared WithForceUnmarshaler twice, byte for byte.
otelcol wanted xconfmap.WithUnredacted. MarshalOptions.OpaqueUnredacted and
its hook were both there; only the exported option was missing, in the
package whose own comment says experimental options go there.
receivertest called atomic.AddInt64 on an atomic.Int64.
otelconftelemetry computed metricsHost from the localhost gate and then
hardcoded ptr("localhost"), so the gate did nothing. The compiler said so:
declared and not used.
confighttp used slices.Backward without importing slices.
multi_batcher's Shutdown still ranged over a sync.Map field the struct no
longer has; partitions is a typed LRU now, and every other reader takes
the lock, so this one does too.
otelconf is v0.23.0, the first release where SDK exposes Resource(), which
createResource calls.
The one real builder bug: Config's component lists had no omitempty, so
MarshalYAML wrote connectors: null and converters: null for a distribution
that declares neither. Its doc comment already said it omits zero values.
cmd/builder: 2 packages ok, 0 failures. confmap, service, otelcol, confighttp,
exporterhelper and receiver all build.
404 lines
15 KiB
Go
404 lines
15 KiB
Go
// Copyright The OpenTelemetry Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package confighttp // import "go.opentelemetry.io/collector/config/confighttp"
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/cors"
|
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
"go.opentelemetry.io/otel"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
"golang.org/x/net/http2"
|
|
|
|
"go.opentelemetry.io/collector/component"
|
|
"go.opentelemetry.io/collector/config/configauth"
|
|
"go.opentelemetry.io/collector/config/confighttp/internal"
|
|
"go.opentelemetry.io/collector/config/configmiddleware"
|
|
"go.opentelemetry.io/collector/config/confignet"
|
|
"go.opentelemetry.io/collector/config/configopaque"
|
|
"go.opentelemetry.io/collector/config/configoptional"
|
|
"go.opentelemetry.io/collector/config/configtls"
|
|
"go.opentelemetry.io/collector/extension/extensionauth"
|
|
)
|
|
|
|
const defaultMaxRequestBodySize = 20 * 1024 * 1024 // 20MiB
|
|
|
|
// ServerConfig defines settings for creating an HTTP server.
|
|
type ServerConfig struct {
|
|
// NetAddr holds configuration for the network listener.
|
|
//
|
|
// Transport defaults to "tcp" if unspecified, and only
|
|
// "tcp", "tcp4", "tcp6", and "unix" are valid options.
|
|
NetAddr confignet.AddrConfig `mapstructure:",squash"`
|
|
|
|
// TLS struct exposes TLS server configuration.
|
|
TLS configoptional.Optional[configtls.ServerConfig] `mapstructure:"tls"`
|
|
|
|
// CORS configures the server for HTTP cross-origin resource sharing (CORS).
|
|
CORS configoptional.Optional[CORSConfig] `mapstructure:"cors"`
|
|
|
|
// Auth for this receiver
|
|
Auth configoptional.Optional[AuthConfig] `mapstructure:"auth,omitempty"`
|
|
|
|
// MaxRequestBodySize sets the maximum request body size in bytes. Default: 20MiB.
|
|
MaxRequestBodySize int64 `mapstructure:"max_request_body_size,omitempty"`
|
|
|
|
// IncludeMetadata propagates the client metadata from the incoming requests to the downstream consumers
|
|
IncludeMetadata bool `mapstructure:"include_metadata,omitempty"`
|
|
|
|
// Additional headers attached to each HTTP response sent to the client.
|
|
// Header values are opaque since they may be sensitive.
|
|
ResponseHeaders configopaque.MapList `mapstructure:"response_headers,omitempty"`
|
|
|
|
// CompressionAlgorithms configures the list of compression algorithms the server can accept. Default: ["", "gzip", "zstd", "zlib", "snappy", "deflate"]
|
|
CompressionAlgorithms []string `mapstructure:"compression_algorithms,omitempty"`
|
|
|
|
// ReadTimeout is the maximum duration for reading the entire
|
|
// request, including the body. A zero or negative value means
|
|
// there will be no timeout.
|
|
//
|
|
// Because ReadTimeout does not let Handlers make per-request
|
|
// decisions on each request body's acceptable deadline or
|
|
// upload rate, most users will prefer to use
|
|
// ReadHeaderTimeout. It is valid to use them both.
|
|
ReadTimeout time.Duration `mapstructure:"read_timeout,omitempty"`
|
|
|
|
// ReadHeaderTimeout is the amount of time allowed to read
|
|
// request headers. The connection's read deadline is reset
|
|
// after reading the headers and the Handler can decide what
|
|
// is considered too slow for the body. If ReadHeaderTimeout
|
|
// is zero, the value of ReadTimeout is used. If both are
|
|
// zero, there is no timeout.
|
|
ReadHeaderTimeout time.Duration `mapstructure:"read_header_timeout"`
|
|
|
|
// WriteTimeout is the maximum duration before timing out
|
|
// writes of the response. It is reset whenever a new
|
|
// request's header is read. Like ReadTimeout, it does not
|
|
// let Handlers make decisions on a per-request basis.
|
|
// A zero or negative value means there will be no timeout.
|
|
WriteTimeout time.Duration `mapstructure:"write_timeout"`
|
|
|
|
// IdleTimeout is the maximum amount of time to wait for the
|
|
// next request when keep-alives are enabled. If IdleTimeout
|
|
// is zero, the value of ReadTimeout is used. If both are
|
|
// zero, there is no timeout.
|
|
IdleTimeout time.Duration `mapstructure:"idle_timeout"`
|
|
|
|
// Middlewares are used to add custom functionality to the HTTP server.
|
|
// Middleware handlers are called in the order they appear in this list,
|
|
// with the first middleware becoming the outermost handler.
|
|
Middlewares []configmiddleware.Config `mapstructure:"middlewares,omitempty"`
|
|
|
|
// KeepAlivesEnabled controls whether HTTP keep-alives are enabled.
|
|
// By default, keep-alives are always enabled. Only very resource-constrained environments should disable them.
|
|
KeepAlivesEnabled bool `mapstructure:"keep_alives_enabled,omitempty"`
|
|
}
|
|
|
|
// NewDefaultServerConfig returns ServerConfig type object with default values.
|
|
// We encourage to use this function to create an object of ServerConfig.
|
|
func NewDefaultServerConfig() ServerConfig {
|
|
netAddr := confignet.NewDefaultAddrConfig()
|
|
// We typically want to create a TCP server and listen over a network.
|
|
netAddr.Transport = confignet.TransportTypeTCP
|
|
|
|
return ServerConfig{
|
|
NetAddr: netAddr,
|
|
WriteTimeout: 30 * time.Second,
|
|
ReadHeaderTimeout: 1 * time.Minute,
|
|
IdleTimeout: 1 * time.Minute,
|
|
KeepAlivesEnabled: true,
|
|
}
|
|
}
|
|
|
|
type AuthConfig struct {
|
|
// Auth for this receiver.
|
|
configauth.Config `mapstructure:",squash"`
|
|
|
|
// RequestParameters is a list of parameters that should be extracted from the request and added to the context.
|
|
// When a parameter is found in both the query string and the header, the value from the query string will be used.
|
|
RequestParameters []string `mapstructure:"request_params,omitempty"`
|
|
// prevent unkeyed literal initialization
|
|
_ struct{}
|
|
}
|
|
|
|
// ToListener creates a net.Listener.
|
|
func (sc *ServerConfig) ToListener(ctx context.Context) (net.Listener, error) {
|
|
listener, err := sc.NetAddr.Listen(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if sc.TLS.HasValue() {
|
|
var tlsCfg *tls.Config
|
|
tlsCfg, err = sc.TLS.Get().LoadTLSConfig(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tlsCfg.NextProtos = []string{http2.NextProtoTLS, "http/1.1"}
|
|
listener = tls.NewListener(listener, tlsCfg)
|
|
}
|
|
|
|
return listener, nil
|
|
}
|
|
|
|
// toServerOptions has options that change the behavior of the HTTP server
|
|
// returned by ServerConfig.ToServer().
|
|
type toServerOptions = internal.ToServerOptions
|
|
|
|
// ToServerOption is an option to change the behavior of the HTTP server
|
|
// returned by ServerConfig.ToServer().
|
|
type ToServerOption = internal.ToServerOption
|
|
|
|
// WithErrorHandler overrides the HTTP error handler that gets invoked
|
|
// when there is a failure inside httpContentDecompressor.
|
|
func WithErrorHandler(e func(w http.ResponseWriter, r *http.Request, errorMsg string, statusCode int)) ToServerOption {
|
|
return internal.ToServerOptionFunc(func(opts *toServerOptions) {
|
|
opts.ErrHandler = e
|
|
})
|
|
}
|
|
|
|
// WithDecoder provides support for additional decoders to be configured
|
|
// by the caller.
|
|
func WithDecoder(key string, dec func(body io.ReadCloser) (io.ReadCloser, error)) ToServerOption {
|
|
return internal.ToServerOptionFunc(func(opts *toServerOptions) {
|
|
if opts.Decoders == nil {
|
|
opts.Decoders = map[string]func(body io.ReadCloser) (io.ReadCloser, error){}
|
|
}
|
|
opts.Decoders[key] = dec
|
|
})
|
|
}
|
|
|
|
// ToServer creates an http.Server from settings object.
|
|
//
|
|
// To allow the configuration to reference middleware or authentication extensions,
|
|
// the `extensions` argument should be the output of `host.GetExtensions()`.
|
|
// It may also be `nil` in tests where no such extension is expected to be used.
|
|
func (sc *ServerConfig) ToServer(ctx context.Context, extensions map[component.ID]component.Component, settings component.TelemetrySettings, handler http.Handler, opts ...ToServerOption) (*http.Server, error) {
|
|
serverOpts := &toServerOptions{}
|
|
serverOpts.Apply(opts...)
|
|
|
|
if sc.MaxRequestBodySize <= 0 {
|
|
sc.MaxRequestBodySize = defaultMaxRequestBodySize
|
|
}
|
|
|
|
if sc.CompressionAlgorithms == nil {
|
|
sc.CompressionAlgorithms = defaultCompressionAlgorithms()
|
|
}
|
|
|
|
// Apply middlewares in reverse order so they execute in
|
|
// forward order. The first middleware runs after
|
|
// decompression, below, preceded by Auth, CORS, etc.
|
|
if len(sc.Middlewares) > 0 && extensions == nil {
|
|
return nil, errors.New("middlewares were configured but this component or its host does not support extensions")
|
|
}
|
|
for _, m := range slices.Backward(sc.Middlewares) {
|
|
wrapper, err := m.GetHTTPServerHandler(ctx, extensions)
|
|
// If we failed to get the middleware
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
handler, err = wrapper(ctx, handler)
|
|
// If we failed to construct a wrapper
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
handler = httpContentDecompressor(
|
|
handler,
|
|
sc.MaxRequestBodySize,
|
|
serverOpts.ErrHandler,
|
|
sc.CompressionAlgorithms,
|
|
serverOpts.Decoders,
|
|
)
|
|
|
|
if sc.MaxRequestBodySize > 0 {
|
|
handler = maxRequestBodySizeInterceptor(handler, sc.MaxRequestBodySize)
|
|
}
|
|
|
|
if sc.Auth.HasValue() {
|
|
if extensions == nil {
|
|
return nil, errors.New("authentication was configured but this component or its host does not support extensions")
|
|
}
|
|
|
|
auth := sc.Auth.Get()
|
|
server, err := auth.GetServerAuthenticator(ctx, extensions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
handler = authInterceptor(handler, server, auth.RequestParameters, serverOpts)
|
|
}
|
|
|
|
if sc.CORS.HasValue() && len(sc.CORS.Get().AllowedOrigins) > 0 {
|
|
corsConfig := sc.CORS.Get()
|
|
co := cors.Options{
|
|
AllowedOrigins: corsConfig.AllowedOrigins,
|
|
AllowCredentials: true,
|
|
AllowedHeaders: corsConfig.AllowedHeaders,
|
|
ExposedHeaders: corsConfig.ExposedHeaders,
|
|
MaxAge: corsConfig.MaxAge,
|
|
}
|
|
handler = cors.New(co).Handler(handler)
|
|
}
|
|
if sc.CORS.HasValue() && len(sc.CORS.Get().AllowedOrigins) == 0 && len(sc.CORS.Get().AllowedHeaders) > 0 {
|
|
settings.Logger.Warn("The CORS configuration specifies allowed headers but no allowed origins, and is therefore ignored.")
|
|
}
|
|
|
|
if sc.ResponseHeaders != nil {
|
|
handler = responseHeadersHandler(handler, sc.ResponseHeaders)
|
|
}
|
|
|
|
otelOpts := append(
|
|
[]otelhttp.Option{
|
|
otelhttp.WithTracerProvider(settings.TracerProvider),
|
|
otelhttp.WithPropagators(otel.GetTextMapPropagator()),
|
|
otelhttp.WithSpanNameFormatter(func(_ string, r *http.Request) string {
|
|
// https://opentelemetry.io/docs/specs/semconv/http/http-spans/#name:
|
|
//
|
|
// "HTTP span names SHOULD be {method} {target} if there is a (low-cardinality) target available.
|
|
// If there is no (low-cardinality) {target} available, HTTP span names SHOULD be {method}.
|
|
//
|
|
// The {method} MUST be {http.request.method} if the method represents the original method known
|
|
// to the instrumentation. In other cases (when {http.request.method} is set to _OTHER),
|
|
// {method} MUST be HTTP.
|
|
//
|
|
// Instrumentation MUST NOT default to using URI path as a {target}."
|
|
//
|
|
method := standardizeHTTPMethod(r.Method, "HTTP")
|
|
if r.Pattern != "" {
|
|
return method + " " + r.Pattern
|
|
}
|
|
return method
|
|
}),
|
|
otelhttp.WithMeterProvider(settings.MeterProvider),
|
|
},
|
|
serverOpts.OtelhttpOpts...)
|
|
|
|
// Enable OpenTelemetry observability plugin.
|
|
handler = otelhttp.NewHandler(handler, "", otelOpts...)
|
|
|
|
// wrap the current handler in an interceptor that will add client.Info to the request's context
|
|
handler = &clientInfoHandler{
|
|
next: handler,
|
|
includeMetadata: sc.IncludeMetadata,
|
|
}
|
|
|
|
errorLog, err := zap.NewStdLogAt(settings.Logger, zapcore.ErrorLevel)
|
|
if err != nil {
|
|
return nil, err // If an error occurs while creating the logger, return nil and the error
|
|
}
|
|
|
|
server := &http.Server{
|
|
Handler: handler,
|
|
ReadTimeout: sc.ReadTimeout,
|
|
ReadHeaderTimeout: sc.ReadHeaderTimeout,
|
|
WriteTimeout: sc.WriteTimeout,
|
|
IdleTimeout: sc.IdleTimeout,
|
|
ErrorLog: errorLog,
|
|
}
|
|
|
|
// Set keep-alives enabled/disabled
|
|
server.SetKeepAlivesEnabled(sc.KeepAlivesEnabled)
|
|
|
|
return server, err
|
|
}
|
|
|
|
func responseHeadersHandler(handler http.Handler, headers configopaque.MapList) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
h := w.Header()
|
|
|
|
for k, v := range headers.Iter {
|
|
h.Set(k, string(v))
|
|
}
|
|
|
|
handler.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// CORSConfig configures a receiver for HTTP cross-origin resource sharing (CORS).
|
|
// See the underlying https://github.com/rs/cors package for details.
|
|
type CORSConfig struct {
|
|
// AllowedOrigins sets the allowed values of the Origin header for
|
|
// HTTP/JSON requests to an OTLP receiver. An origin may contain a
|
|
// wildcard (*) to replace 0 or more characters (e.g.,
|
|
// "http://*.domain.com", or "*" to allow any origin).
|
|
AllowedOrigins []string `mapstructure:"allowed_origins,omitempty"`
|
|
|
|
// AllowedHeaders sets what headers will be allowed in CORS requests.
|
|
// The Accept, Accept-Language, Content-Type, and Content-Language
|
|
// headers are implicitly allowed. If no headers are listed,
|
|
// X-Requested-With will also be accepted by default. Include "*" to
|
|
// allow any request header.
|
|
AllowedHeaders []string `mapstructure:"allowed_headers,omitempty"`
|
|
|
|
// ExposedHeaders sets the value of the Access-Control-Expose-Headers response
|
|
// header, indicating which headers are safe to expose to the API of a CORS response.
|
|
ExposedHeaders []string `mapstructure:"exposed_headers,omitempty"`
|
|
|
|
// MaxAge sets the value of the Access-Control-Max-Age response header.
|
|
// Set it to the number of seconds that browsers should cache a CORS
|
|
// preflight response for.
|
|
MaxAge int `mapstructure:"max_age,omitempty"`
|
|
// prevent unkeyed literal initialization
|
|
_ struct{}
|
|
}
|
|
|
|
// NewDefaultCORSConfig creates a default cross-origin resource sharing (CORS) configuration.
|
|
func NewDefaultCORSConfig() CORSConfig {
|
|
return CORSConfig{}
|
|
}
|
|
|
|
func authInterceptor(next http.Handler, server extensionauth.Server, requestParams []string, serverOpts *internal.ToServerOptions) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
sources := r.Header
|
|
query := r.URL.Query()
|
|
for _, param := range requestParams {
|
|
if val, ok := query[param]; ok {
|
|
sources[param] = val
|
|
}
|
|
}
|
|
ctx, err := server.Authenticate(r.Context(), sources)
|
|
if err != nil {
|
|
if serverOpts.ErrHandler != nil {
|
|
serverOpts.ErrHandler(w, r, err.Error(), http.StatusUnauthorized)
|
|
} else {
|
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
func maxRequestBodySizeInterceptor(next http.Handler, maxRecvSize int64) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
r.Body = http.MaxBytesReader(w, r.Body, maxRecvSize)
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// standardizeHTTPMethod returns an upper case HTTP method if well-known, otherwise unknown.
|
|
// Based on https://github.com/open-telemetry/opentelemetry-go-contrib/blob/1530d71edc6d40d0659187d069081b639ef1b394/instrumentation/github.com/emicklei/go-restful/otelrestful/internal/semconv/util.go#L119
|
|
func standardizeHTTPMethod(method, unknown string) string {
|
|
method = strings.ToUpper(method)
|
|
switch method {
|
|
case http.MethodConnect, http.MethodDelete, http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodPatch, http.MethodPost, http.MethodPut, http.MethodTrace:
|
|
return method
|
|
}
|
|
return unknown
|
|
}
|