Files
bifrost/plugins/otel/http.go
Beyhan Oğur 880f412e2c first commit
2026-04-26 21:52:23 +03:00

115 lines
3.2 KiB
Go

package otel
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
collectorpb "go.opentelemetry.io/proto/otlp/collector/trace/v1"
"google.golang.org/protobuf/proto"
)
// OtelClientHTTP is the implementation of the OpenTelemetry client for HTTP
type OtelClientHTTP struct {
client *http.Client
endpoint string
headers map[string]string
}
// NewOtelClientHTTP creates a new OpenTelemetry client for HTTP
func NewOtelClientHTTP(endpoint string, headers map[string]string, tlsCACert string, insecureMode bool) (*OtelClientHTTP, error) {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.MaxIdleConns = 100
transport.MaxIdleConnsPerHost = 10
transport.IdleConnTimeout = 120 * time.Second
// TLS priority: custom CA > system roots > insecure
if tlsCACert != "" {
// Validate the CA cert path to prevent path traversal attacks
if err := validateCACertPath(tlsCACert); err != nil {
return nil, err
}
caCert, err := os.ReadFile(tlsCACert)
if err != nil {
return nil, fmt.Errorf("fail to load provided CA cert: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("fail to add provided CA cert")
}
transport.TLSClientConfig = &tls.Config{
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
}
} else if insecureMode {
transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true, // #nosec G402
MinVersion: tls.VersionTLS12,
}
} else {
// Use system root CAs with MinVersion
transport.TLSClientConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
}
}
return &OtelClientHTTP{client: &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}, endpoint: endpoint, headers: headers}, nil
}
// Emit sends a trace to the OpenTelemetry collector
func (c *OtelClientHTTP) Emit(ctx context.Context, rs []*ResourceSpan) error {
payload, err := proto.Marshal(&collectorpb.ExportTraceServiceRequest{ResourceSpans: rs})
if err != nil {
logger.Error("[otel] failed to marshal trace: %v", err)
return err
}
var body bytes.Buffer
body.Write(payload)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, &body)
if err != nil {
logger.Error("[otel] failed to create request: %v", err)
return err
}
req.Header.Set("Content-Type", "application/x-protobuf")
if c.headers != nil {
for key, value := range c.headers {
if strings.ToLower(key) == "content-type" {
continue
}
req.Header.Set(key, value)
}
}
resp, err := c.client.Do(req)
if err != nil {
logger.Error("[otel] failed to send request to %s: %v", c.endpoint, err)
return err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
// Discard the body to avoid leaking memory
_, _ = io.Copy(io.Discard, resp.Body)
logger.Error("[otel] collector at %s returned status %s", c.endpoint, resp.Status)
return fmt.Errorf("collector returned %s", resp.Status)
}
logger.Debug("[otel] successfully sent trace to %s, status: %s", c.endpoint, resp.Status)
return nil
}
// Close closes the HTTP client
func (c *OtelClientHTTP) Close() error {
if c.client != nil {
c.client.CloseIdleConnections()
}
return nil
}