115 lines
3.2 KiB
Go
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
|
|
}
|