 131868bdca
			
		
	
	131868bdca
	
	
	
		
			
			Major security, observability, and configuration improvements:
## Security Hardening
- Implemented configurable CORS (no more wildcards)
- Added comprehensive auth middleware for admin endpoints
- Enhanced webhook HMAC validation
- Added input validation and rate limiting
- Security headers and CSP policies
## Configuration Management
- Made N8N webhook URL configurable (WHOOSH_N8N_BASE_URL)
- Replaced all hardcoded endpoints with environment variables
- Added feature flags for LLM vs heuristic composition
- Gitea fetch hardening with EAGER_FILTER and FULL_RESCAN options
## API Completeness
- Implemented GetCouncilComposition function
- Added GET /api/v1/councils/{id} endpoint
- Council artifacts API (POST/GET /api/v1/councils/{id}/artifacts)
- /admin/health/details endpoint with component status
- Database lookup for repository URLs (no hardcoded fallbacks)
## Observability & Performance
- Added OpenTelemetry distributed tracing with goal/pulse correlation
- Performance optimization database indexes
- Comprehensive health monitoring
- Enhanced logging and error handling
## Infrastructure
- Production-ready P2P discovery (replaces mock implementation)
- Removed unused Redis configuration
- Enhanced Docker Swarm integration
- Added migration files for performance indexes
## Code Quality
- Comprehensive input validation
- Graceful error handling and failsafe fallbacks
- Backwards compatibility maintained
- Following security best practices
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
		
	
		
			
				
	
	
		
			340 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			340 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright The OpenTelemetry Authors
 | |
| //
 | |
| // Licensed under the Apache License, Version 2.0 (the "License");
 | |
| // you may not use this file except in compliance with the License.
 | |
| // You may obtain a copy of the License at
 | |
| //
 | |
| //     http://www.apache.org/licenses/LICENSE-2.0
 | |
| //
 | |
| // Unless required by applicable law or agreed to in writing, software
 | |
| // distributed under the License is distributed on an "AS IS" BASIS,
 | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| // See the License for the specific language governing permissions and
 | |
| // limitations under the License.
 | |
| 
 | |
| package jaeger // import "go.opentelemetry.io/otel/exporters/jaeger"
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log"
 | |
| 	"net/http"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/go-logr/logr"
 | |
| 	"github.com/go-logr/stdr"
 | |
| 
 | |
| 	gen "go.opentelemetry.io/otel/exporters/jaeger/internal/gen-go/jaeger"
 | |
| 	"go.opentelemetry.io/otel/exporters/jaeger/internal/third_party/thrift/lib/go/thrift"
 | |
| )
 | |
| 
 | |
| // batchUploader send a batch of spans to Jaeger.
 | |
| type batchUploader interface {
 | |
| 	upload(context.Context, *gen.Batch) error
 | |
| 	shutdown(context.Context) error
 | |
| }
 | |
| 
 | |
| // EndpointOption configures a Jaeger endpoint.
 | |
| type EndpointOption interface {
 | |
| 	newBatchUploader() (batchUploader, error)
 | |
| }
 | |
| 
 | |
| type endpointOptionFunc func() (batchUploader, error)
 | |
| 
 | |
| func (fn endpointOptionFunc) newBatchUploader() (batchUploader, error) {
 | |
| 	return fn()
 | |
| }
 | |
| 
 | |
| // WithAgentEndpoint configures the Jaeger exporter to send spans to a Jaeger agent
 | |
| // over compact thrift protocol. This will use the following environment variables for
 | |
| // configuration if no explicit option is provided:
 | |
| //
 | |
| // - OTEL_EXPORTER_JAEGER_AGENT_HOST is used for the agent address host
 | |
| // - OTEL_EXPORTER_JAEGER_AGENT_PORT is used for the agent address port
 | |
| //
 | |
| // The passed options will take precedence over any environment variables and default values
 | |
| // will be used if neither are provided.
 | |
| func WithAgentEndpoint(options ...AgentEndpointOption) EndpointOption {
 | |
| 	return endpointOptionFunc(func() (batchUploader, error) {
 | |
| 		cfg := agentEndpointConfig{
 | |
| 			agentClientUDPParams{
 | |
| 				AttemptReconnecting: true,
 | |
| 				Host:                envOr(envAgentHost, "localhost"),
 | |
| 				Port:                envOr(envAgentPort, "6831"),
 | |
| 			},
 | |
| 		}
 | |
| 		for _, opt := range options {
 | |
| 			cfg = opt.apply(cfg)
 | |
| 		}
 | |
| 
 | |
| 		client, err := newAgentClientUDP(cfg.agentClientUDPParams)
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		return &agentUploader{client: client}, nil
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // AgentEndpointOption configures a Jaeger agent endpoint.
 | |
| type AgentEndpointOption interface {
 | |
| 	apply(agentEndpointConfig) agentEndpointConfig
 | |
| }
 | |
| 
 | |
| type agentEndpointConfig struct {
 | |
| 	agentClientUDPParams
 | |
| }
 | |
| 
 | |
| type agentEndpointOptionFunc func(agentEndpointConfig) agentEndpointConfig
 | |
| 
 | |
| func (fn agentEndpointOptionFunc) apply(cfg agentEndpointConfig) agentEndpointConfig {
 | |
| 	return fn(cfg)
 | |
| }
 | |
| 
 | |
| // WithAgentHost sets a host to be used in the agent client endpoint.
 | |
| // This option overrides any value set for the
 | |
| // OTEL_EXPORTER_JAEGER_AGENT_HOST environment variable.
 | |
| // If this option is not passed and the env var is not set, "localhost" will be used by default.
 | |
| func WithAgentHost(host string) AgentEndpointOption {
 | |
| 	return agentEndpointOptionFunc(func(o agentEndpointConfig) agentEndpointConfig {
 | |
| 		o.Host = host
 | |
| 		return o
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // WithAgentPort sets a port to be used in the agent client endpoint.
 | |
| // This option overrides any value set for the
 | |
| // OTEL_EXPORTER_JAEGER_AGENT_PORT environment variable.
 | |
| // If this option is not passed and the env var is not set, "6831" will be used by default.
 | |
| func WithAgentPort(port string) AgentEndpointOption {
 | |
| 	return agentEndpointOptionFunc(func(o agentEndpointConfig) agentEndpointConfig {
 | |
| 		o.Port = port
 | |
| 		return o
 | |
| 	})
 | |
| }
 | |
| 
 | |
| var emptyLogger = logr.Logger{}
 | |
| 
 | |
| // WithLogger sets a logger to be used by agent client.
 | |
| // WithLogger and WithLogr will overwrite each other.
 | |
| func WithLogger(logger *log.Logger) AgentEndpointOption {
 | |
| 	return WithLogr(stdr.New(logger))
 | |
| }
 | |
| 
 | |
| // WithLogr sets a logr.Logger to be used by agent client.
 | |
| // WithLogr and WithLogger will overwrite each other.
 | |
| func WithLogr(logger logr.Logger) AgentEndpointOption {
 | |
| 	return agentEndpointOptionFunc(func(o agentEndpointConfig) agentEndpointConfig {
 | |
| 		o.Logger = logger
 | |
| 		return o
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // WithDisableAttemptReconnecting sets option to disable reconnecting udp client.
 | |
| func WithDisableAttemptReconnecting() AgentEndpointOption {
 | |
| 	return agentEndpointOptionFunc(func(o agentEndpointConfig) agentEndpointConfig {
 | |
| 		o.AttemptReconnecting = false
 | |
| 		return o
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // WithAttemptReconnectingInterval sets the interval between attempts to re resolve agent endpoint.
 | |
| func WithAttemptReconnectingInterval(interval time.Duration) AgentEndpointOption {
 | |
| 	return agentEndpointOptionFunc(func(o agentEndpointConfig) agentEndpointConfig {
 | |
| 		o.AttemptReconnectInterval = interval
 | |
| 		return o
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // WithMaxPacketSize sets the maximum UDP packet size for transport to the Jaeger agent.
 | |
| func WithMaxPacketSize(size int) AgentEndpointOption {
 | |
| 	return agentEndpointOptionFunc(func(o agentEndpointConfig) agentEndpointConfig {
 | |
| 		o.MaxPacketSize = size
 | |
| 		return o
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // WithCollectorEndpoint defines the full URL to the Jaeger HTTP Thrift collector. This will
 | |
| // use the following environment variables for configuration if no explicit option is provided:
 | |
| //
 | |
| // - OTEL_EXPORTER_JAEGER_ENDPOINT is the HTTP endpoint for sending spans directly to a collector.
 | |
| // - OTEL_EXPORTER_JAEGER_USER is the username to be sent as authentication to the collector endpoint.
 | |
| // - OTEL_EXPORTER_JAEGER_PASSWORD is the password to be sent as authentication to the collector endpoint.
 | |
| //
 | |
| // The passed options will take precedence over any environment variables.
 | |
| // If neither values are provided for the endpoint, the default value of "http://localhost:14268/api/traces" will be used.
 | |
| // If neither values are provided for the username or the password, they will not be set since there is no default.
 | |
| func WithCollectorEndpoint(options ...CollectorEndpointOption) EndpointOption {
 | |
| 	return endpointOptionFunc(func() (batchUploader, error) {
 | |
| 		cfg := collectorEndpointConfig{
 | |
| 			endpoint:   envOr(envEndpoint, "http://localhost:14268/api/traces"),
 | |
| 			username:   envOr(envUser, ""),
 | |
| 			password:   envOr(envPassword, ""),
 | |
| 			httpClient: http.DefaultClient,
 | |
| 		}
 | |
| 
 | |
| 		for _, opt := range options {
 | |
| 			cfg = opt.apply(cfg)
 | |
| 		}
 | |
| 
 | |
| 		return &collectorUploader{
 | |
| 			endpoint:   cfg.endpoint,
 | |
| 			username:   cfg.username,
 | |
| 			password:   cfg.password,
 | |
| 			httpClient: cfg.httpClient,
 | |
| 		}, nil
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // CollectorEndpointOption configures a Jaeger collector endpoint.
 | |
| type CollectorEndpointOption interface {
 | |
| 	apply(collectorEndpointConfig) collectorEndpointConfig
 | |
| }
 | |
| 
 | |
| type collectorEndpointConfig struct {
 | |
| 	// endpoint for sending spans directly to a collector.
 | |
| 	endpoint string
 | |
| 
 | |
| 	// username to be used for authentication with the collector endpoint.
 | |
| 	username string
 | |
| 
 | |
| 	// password to be used for authentication with the collector endpoint.
 | |
| 	password string
 | |
| 
 | |
| 	// httpClient to be used to make requests to the collector endpoint.
 | |
| 	httpClient *http.Client
 | |
| }
 | |
| 
 | |
| type collectorEndpointOptionFunc func(collectorEndpointConfig) collectorEndpointConfig
 | |
| 
 | |
| func (fn collectorEndpointOptionFunc) apply(cfg collectorEndpointConfig) collectorEndpointConfig {
 | |
| 	return fn(cfg)
 | |
| }
 | |
| 
 | |
| // WithEndpoint is the URL for the Jaeger collector that spans are sent to.
 | |
| // This option overrides any value set for the
 | |
| // OTEL_EXPORTER_JAEGER_ENDPOINT environment variable.
 | |
| // If this option is not passed and the environment variable is not set,
 | |
| // "http://localhost:14268/api/traces" will be used by default.
 | |
| func WithEndpoint(endpoint string) CollectorEndpointOption {
 | |
| 	return collectorEndpointOptionFunc(func(o collectorEndpointConfig) collectorEndpointConfig {
 | |
| 		o.endpoint = endpoint
 | |
| 		return o
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // WithUsername sets the username to be used in the authorization header sent for all requests to the collector.
 | |
| // This option overrides any value set for the
 | |
| // OTEL_EXPORTER_JAEGER_USER environment variable.
 | |
| // If this option is not passed and the environment variable is not set, no username will be set.
 | |
| func WithUsername(username string) CollectorEndpointOption {
 | |
| 	return collectorEndpointOptionFunc(func(o collectorEndpointConfig) collectorEndpointConfig {
 | |
| 		o.username = username
 | |
| 		return o
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // WithPassword sets the password to be used in the authorization header sent for all requests to the collector.
 | |
| // This option overrides any value set for the
 | |
| // OTEL_EXPORTER_JAEGER_PASSWORD environment variable.
 | |
| // If this option is not passed and the environment variable is not set, no password will be set.
 | |
| func WithPassword(password string) CollectorEndpointOption {
 | |
| 	return collectorEndpointOptionFunc(func(o collectorEndpointConfig) collectorEndpointConfig {
 | |
| 		o.password = password
 | |
| 		return o
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // WithHTTPClient sets the http client to be used to make request to the collector endpoint.
 | |
| func WithHTTPClient(client *http.Client) CollectorEndpointOption {
 | |
| 	return collectorEndpointOptionFunc(func(o collectorEndpointConfig) collectorEndpointConfig {
 | |
| 		o.httpClient = client
 | |
| 		return o
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // agentUploader implements batchUploader interface sending batches to
 | |
| // Jaeger through the UDP agent.
 | |
| type agentUploader struct {
 | |
| 	client *agentClientUDP
 | |
| }
 | |
| 
 | |
| var _ batchUploader = (*agentUploader)(nil)
 | |
| 
 | |
| func (a *agentUploader) shutdown(ctx context.Context) error {
 | |
| 	done := make(chan error, 1)
 | |
| 	go func() {
 | |
| 		done <- a.client.Close()
 | |
| 	}()
 | |
| 
 | |
| 	select {
 | |
| 	case <-ctx.Done():
 | |
| 		// Prioritize not blocking the calling thread and just leak the
 | |
| 		// spawned goroutine to close the client.
 | |
| 		return ctx.Err()
 | |
| 	case err := <-done:
 | |
| 		return err
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (a *agentUploader) upload(ctx context.Context, batch *gen.Batch) error {
 | |
| 	return a.client.EmitBatch(ctx, batch)
 | |
| }
 | |
| 
 | |
| // collectorUploader implements batchUploader interface sending batches to
 | |
| // Jaeger through the collector http endpoint.
 | |
| type collectorUploader struct {
 | |
| 	endpoint   string
 | |
| 	username   string
 | |
| 	password   string
 | |
| 	httpClient *http.Client
 | |
| }
 | |
| 
 | |
| var _ batchUploader = (*collectorUploader)(nil)
 | |
| 
 | |
| func (c *collectorUploader) shutdown(ctx context.Context) error {
 | |
| 	// The Exporter will cancel any active exports and will prevent all
 | |
| 	// subsequent exports, so nothing to do here.
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (c *collectorUploader) upload(ctx context.Context, batch *gen.Batch) error {
 | |
| 	body, err := serialize(batch)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	req, err := http.NewRequestWithContext(ctx, "POST", c.endpoint, body)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if c.username != "" && c.password != "" {
 | |
| 		req.SetBasicAuth(c.username, c.password)
 | |
| 	}
 | |
| 	req.Header.Set("Content-Type", "application/x-thrift")
 | |
| 
 | |
| 	resp, err := c.httpClient.Do(req)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	_, _ = io.Copy(io.Discard, resp.Body)
 | |
| 	if err = resp.Body.Close(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
 | |
| 		return fmt.Errorf("failed to upload traces; HTTP status code: %d", resp.StatusCode)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func serialize(obj thrift.TStruct) (*bytes.Buffer, error) {
 | |
| 	buf := thrift.NewTMemoryBuffer()
 | |
| 	if err := obj.Write(context.Background(), thrift.NewTBinaryProtocolConf(buf, &thrift.TConfiguration{})); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return buf.Buffer, nil
 | |
| }
 |