diff --git a/config/config.go b/config/config.go index eac588e..5255cfe 100644 --- a/config/config.go +++ b/config/config.go @@ -135,6 +135,20 @@ type Config struct { // New services should leave this false (the default). Set to true only if you // have existing OpenTracing instrumentation that hasn't been migrated to OTEL. OTLPUseOpenTracingBridge bool `envconfig:"OTLP_USE_OPENTRACING_BRIDGE" default:"false"` + + // Throughput tuning + + // DisableHTTPCompression disables gzip/zstd compression for HTTP gateway responses + DisableHTTPCompression bool `envconfig:"DISABLE_HTTP_COMPRESSION" default:"false"` + // HTTPCompressionMinSize is the minimum response body size (bytes) before compression is applied. + // Responses smaller than this are sent uncompressed. Applies to both gzip and zstd. + HTTPCompressionMinSize int `envconfig:"HTTP_COMPRESSION_MIN_SIZE" default:"256"` + // ResponseTimeLogLevel sets the log level for per-request response time logging. + // Valid values: "debug", "info", "warn", "error". Invalid values default to "info". + ResponseTimeLogLevel string `envconfig:"RESPONSE_TIME_LOG_LEVEL" default:"info"` + // ResponseTimeLogErrorOnly when true, only logs response time for requests that return an error. + // Successful requests are not logged. Default behavior logs all requests. + ResponseTimeLogErrorOnly bool `envconfig:"RESPONSE_TIME_LOG_ERROR_ONLY" default:"false"` } // Validate checks the configuration for common misconfigurations and returns @@ -165,6 +179,9 @@ func (c Config) Validate() []string { c.HealthcheckWaitDurationInSeconds >= c.ShutdownDurationInSeconds { warnings = append(warnings, "HealthcheckWaitDurationInSeconds should be less than ShutdownDurationInSeconds") } + if c.HTTPCompressionMinSize < 0 { + warnings = append(warnings, "HTTPCompressionMinSize is negative; this may cause unexpected behavior") + } return warnings } diff --git a/core.go b/core.go index f6ffae8..bb3cba8 100644 --- a/core.go +++ b/core.go @@ -111,6 +111,11 @@ func (c *cb) processConfig() { if !c.config.DisableAutoMaxProcs { SetupAutoMaxProcs() } + // Auto-disable NewRelic when no license key is configured to avoid + // interceptor overhead for services that don't use NR. + if !c.config.DisableNewRelic && strings.TrimSpace(c.config.NewRelicLicenseKey) == "" { + c.config.DisableNewRelic = true + } if !c.config.DisableNewRelic { err := SetupNewRelic(nrName, c.config.NewRelicLicenseKey, c.config.NewRelicDistributedTracing) if err != nil { @@ -121,7 +126,7 @@ func (c *cb) processConfig() { SetupEnvironment(c.config.Environment) SetupReleaseName(c.config.ReleaseName) SetupHystrixPrometheus() - ConfigureInterceptors(c.config.DoNotLogGRPCReflection, c.config.TraceHeaderName) + ConfigureInterceptors(c.config.DoNotLogGRPCReflection, c.config.TraceHeaderName, c.config.ResponseTimeLogLevel, c.config.ResponseTimeLogErrorOnly) if !c.config.DisableSignalHandler { dur := time.Second * 10 if c.config.ShutdownDurationInSeconds > 0 { @@ -390,7 +395,14 @@ func (c *cb) initHTTP(ctx context.Context) (*http.Server, error) { // Start HTTP server (and proxy calls to gRPC server endpoint) gatewayAddr := fmt.Sprintf("%s:%d", c.config.ListenHost, c.config.HTTPPort) promHandler := promhttp.Handler() - gzipHandler := gzhttp.GzipHandler(tracingWrapper(mux)) + gzipHandler := http.Handler(tracingWrapper(mux)) + if !c.config.DisableHTTPCompression { + wrapper, err := gzhttp.NewWrapper(gzhttp.MinSize(c.config.HTTPCompressionMinSize)) + if err != nil { + return nil, fmt.Errorf("failed to create compression handler: %w", err) + } + gzipHandler = wrapper(gzipHandler) + } gwServer := &http.Server{ Addr: gatewayAddr, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/core_coverage_test.go b/core_coverage_test.go index 56792b8..87c7071 100644 --- a/core_coverage_test.go +++ b/core_coverage_test.go @@ -1165,5 +1165,5 @@ func TestSetupOpenTelemetry_MissingServiceName(t *testing.T) { } func TestConfigureInterceptors_BothBranches(t *testing.T) { - ConfigureInterceptors(true, "X-My-Trace") + ConfigureInterceptors(true, "X-My-Trace", "info", false) } diff --git a/go.mod b/go.mod index fe2e10a..919b84e 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,11 @@ go 1.25.8 require ( github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5 - github.com/go-coldbrew/errors v0.2.6 + github.com/go-coldbrew/errors v0.2.9 github.com/go-coldbrew/hystrixprometheus v0.1.2 - github.com/go-coldbrew/interceptors v0.1.13 - github.com/go-coldbrew/log v0.2.8 - github.com/go-coldbrew/options v0.2.6 + github.com/go-coldbrew/interceptors v0.1.15 + github.com/go-coldbrew/log v0.2.9 + github.com/go-coldbrew/options v0.2.7 github.com/go-coldbrew/tracing v0.2.0 github.com/golang/protobuf v1.5.4 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 diff --git a/go.sum b/go.sum index 5069128..bb6c212 100644 --- a/go.sum +++ b/go.sum @@ -171,16 +171,16 @@ github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1 github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= -github.com/go-coldbrew/errors v0.2.6 h1:rNOI+XcxuyrN/t3e7zDBVUVzcYBNmhyjatsnJg9MDZA= -github.com/go-coldbrew/errors v0.2.6/go.mod h1:jFXeN7Q74fggbIEZu/68vhRX1ob2zfc+2sy0osVfBlY= +github.com/go-coldbrew/errors v0.2.9 h1:5M7ZUd9tU+YPN+nQPmqiqRcgXWXdjd1I1+97cs/kcvI= +github.com/go-coldbrew/errors v0.2.9/go.mod h1:/IWIHHt4tHdOBydTFUdwPSmtNSKAxxMyndwXX9ITLf8= github.com/go-coldbrew/hystrixprometheus v0.1.2 h1:WSt4FtYr8xNDKgdGWYpMfXGFIK7zdDSBwDSbpuPhBHI= github.com/go-coldbrew/hystrixprometheus v0.1.2/go.mod h1:OrNRHHxZagpmQXNp//oHKOemGSU0ScOqEcJgeKbJ+wg= -github.com/go-coldbrew/interceptors v0.1.13 h1:YVHOldoe3I1VqtGfAwn0jhpiPmSfA/irVpNJOCFfICc= -github.com/go-coldbrew/interceptors v0.1.13/go.mod h1:brTTe9j2BSpavK0zDqU2cVvIf8LAdMyu+7vNeVsL5Vk= -github.com/go-coldbrew/log v0.2.8 h1:aF+vw23zMyh5S9vhhofERiaPpSDyeJH1Tv1CYREn/a0= -github.com/go-coldbrew/log v0.2.8/go.mod h1:RKvGzMZMt7FpQ9u36adkDigRxkOvRj1diwgAgRJZH4E= -github.com/go-coldbrew/options v0.2.6 h1:Nr93v7PbO+EYLHhzA8biGumaTTSHLHqTYLg70n/foXE= -github.com/go-coldbrew/options v0.2.6/go.mod h1:Os4pZwIgMHES079iOKXTlzcipWXbxw0OhsAN5D9m2mM= +github.com/go-coldbrew/interceptors v0.1.15 h1:oLEhYiQGmZK4mVg4et+T+dqCdhhJp/kV+ob5Q/D7kSQ= +github.com/go-coldbrew/interceptors v0.1.15/go.mod h1:ixchtwMi+V2CY0AsESmX7FQlgZJYGT9Xt04ddGz2M5w= +github.com/go-coldbrew/log v0.2.9 h1:LpuIOVlH6PUcq/ugO+8tK3JpmwdDwpOr2pG0fTt/dX0= +github.com/go-coldbrew/log v0.2.9/go.mod h1:vYRIAbHVLcMjBqXexjYiVehRxgEfr07O6ZXBvg1DvmQ= +github.com/go-coldbrew/options v0.2.7 h1:hWUgVx+snbjafEcJ01OLFBOfAV4a28IHcsHc5/vVkQE= +github.com/go-coldbrew/options v0.2.7/go.mod h1:8JlmgVJXFoY1KiDLsyMmR//q1U1aBItCexvTrVT2Y60= github.com/go-coldbrew/tracing v0.2.0 h1:WGfdp5PNunOGfjTZGXPFaip3G5qOOMP622JFYA90ML4= github.com/go-coldbrew/tracing v0.2.0/go.mod h1:phF8WDsadDKK20lgB0Zv2/ocVIrCbVziMd3MMxqr+aU= github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= diff --git a/initializers.go b/initializers.go index f611dee..321e15f 100644 --- a/initializers.go +++ b/initializers.go @@ -321,19 +321,24 @@ func SetupHystrixPrometheus() { }) } -// ConfigureInterceptors configures the interceptors package with the provided -// DoNotLogGRPCReflection is a boolean that indicates whether to log the grpc.reflection.v1alpha.ServerReflection service calls in logs -// traceHeaderName is the name of the header to use for tracing (e.g. X-Trace-Id) - if empty, defaults to X-Trace-Id -func ConfigureInterceptors(DoNotLogGRPCReflection bool, traceHeaderName string) { +// ConfigureInterceptors configures the interceptors package with the provided settings. +func ConfigureInterceptors(DoNotLogGRPCReflection bool, traceHeaderName string, responseTimeLogLevel string, responseTimeLogErrorOnly bool) { if DoNotLogGRPCReflection { - interceptors.FilterMethods = append( - interceptors.FilterMethods, - "grpc.reflection.v1alpha.ServerReflection", - ) + methods := append(interceptors.FilterMethods, "grpc.reflection.v1alpha.ServerReflection") //nolint:staticcheck // FilterMethods read is fine, using SetFilterMethods to write + interceptors.SetFilterMethods(context.Background(), methods) } if traceHeaderName != "" { notifier.SetTraceHeaderName(traceHeaderName) } + if responseTimeLogLevel != "" { + level, err := loggers.ParseLevel(responseTimeLogLevel) + if err != nil { + log.Warn(context.Background(), "msg", "invalid RESPONSE_TIME_LOG_LEVEL, defaulting to info", "value", responseTimeLogLevel, "err", err) + level = loggers.InfoLevel + } + interceptors.SetResponseTimeLogLevel(context.Background(), level) + } + interceptors.SetResponseTimeLogErrorOnly(responseTimeLogErrorOnly) } // SetupAutoMaxProcs sets up the GOMAXPROCS to match Linux container CPU quota