Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ For full documentation, visit https://docs.coldbrew.cloud
- [func AddWorkerRunOptions\(opts ...workers.RunOption\)](<#AddWorkerRunOptions>)
- [func InitializeVTProto\(\)](<#InitializeVTProto>)
- [func OTELMeterProvider\(\) otelmetric.MeterProvider](<#OTELMeterProvider>)
- [func RegisterHTTPMarshaler\(mime string, m runtime.Marshaler\)](<#RegisterHTTPMarshaler>)
- [func RegisterServeMuxOption\(opt runtime.ServeMuxOption\)](<#RegisterServeMuxOption>)
- [func SetOTELGRPCClientOptions\(opts ...otelgrpc.Option\)](<#SetOTELGRPCClientOptions>)
- [func SetOTELGRPCServerOptions\(opts ...otelgrpc.Option\)](<#SetOTELGRPCServerOptions>)
- [func SetOTELOptions\(opts grpcotel.Options\)](<#SetOTELOptions>)
Expand Down Expand Up @@ -153,8 +155,35 @@ func OTELMeterProvider() otelmetric.MeterProvider

OTELMeterProvider returns the global OTel MeterProvider. This is a convenience accessor for code that needs the interface type.

<a name="RegisterHTTPMarshaler"></a>
## func [RegisterHTTPMarshaler](<https://github.com/go-coldbrew/core/blob/main/serve_mux_options.go#L40>)

```go
func RegisterHTTPMarshaler(mime string, m runtime.Marshaler)
```

RegisterHTTPMarshaler registers a runtime.Marshaler for the given MIME type on the HTTP gateway. Equivalent to RegisterServeMuxOption\(runtime.WithMarshalerOption\(mime, m\)\).

To override the gateway's default fallback for unregistered Content\-Types \(which is protojson via runtime.JSONPb\), register for runtime.MIMEWildcard.

Must be called before core.Run\(\). Not safe for concurrent registration.

<a name="RegisterServeMuxOption"></a>
## func [RegisterServeMuxOption](<https://github.com/go-coldbrew/core/blob/main/serve_mux_options.go#L28>)

```go
func RegisterServeMuxOption(opt runtime.ServeMuxOption)
```

RegisterServeMuxOption appends a runtime.ServeMuxOption that initHTTP passes to runtime.NewServeMux. Registered options are applied AFTER core's built\-ins \(the incoming\-header matcher derived from HTTPHeaderPrefixes, the application/proto and application/protobuf marshalers, and the span\-route middleware\), so:

- Last\-write\-wins options — WithMarshalerOption for a given MIME, WithErrorHandler, WithRoutingErrorHandler, WithIncomingHeaderMatcher — can intentionally override core's defaults. Overriding the incoming header matcher disables the HTTPHeaderPrefixes wiring; reimplement it yourself if you still need that behavior.
- Additive options — WithMiddlewares, WithMetadata, WithForwardResponseOption — stack with core's.

Must be called before core.Run\(\) \(typically from a service's PreStart hook\). Not safe for concurrent registration.

<a name="SetOTELGRPCClientOptions"></a>
## func [SetOTELGRPCClientOptions](<https://github.com/go-coldbrew/core/blob/main/core.go#L623>)
## func [SetOTELGRPCClientOptions](<https://github.com/go-coldbrew/core/blob/main/core.go#L624>)

```go
func SetOTELGRPCClientOptions(opts ...otelgrpc.Option)
Expand All @@ -163,7 +192,7 @@ func SetOTELGRPCClientOptions(opts ...otelgrpc.Option)
Deprecated: Use SetOTELOptions instead. Only applies when OTEL\_USE\_LEGACY\_INSTRUMENTATION=true.

<a name="SetOTELGRPCServerOptions"></a>
## func [SetOTELGRPCServerOptions](<https://github.com/go-coldbrew/core/blob/main/core.go#L617>)
## func [SetOTELGRPCServerOptions](<https://github.com/go-coldbrew/core/blob/main/core.go#L618>)

```go
func SetOTELGRPCServerOptions(opts ...otelgrpc.Option)
Expand All @@ -172,7 +201,7 @@ func SetOTELGRPCServerOptions(opts ...otelgrpc.Option)
Deprecated: Use SetOTELOptions instead. Only applies when OTEL\_USE\_LEGACY\_INSTRUMENTATION=true.

<a name="SetOTELOptions"></a>
## func [SetOTELOptions](<https://github.com/go-coldbrew/core/blob/main/core.go#L630>)
## func [SetOTELOptions](<https://github.com/go-coldbrew/core/blob/main/core.go#L631>)

```go
func SetOTELOptions(opts grpcotel.Options)
Expand Down Expand Up @@ -331,7 +360,7 @@ type CB interface {
```

<a name="New"></a>
### func [New](<https://github.com/go-coldbrew/core/blob/main/core.go#L1030>)
### func [New](<https://github.com/go-coldbrew/core/blob/main/core.go#L1031>)

```go
func New(c config.Config) CB
Expand Down
19 changes: 19 additions & 0 deletions compression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package core

import (
"net/http"

"github.com/go-coldbrew/core/config"
"github.com/klauspost/compress/gzhttp"
)

// newHTTPCompressionWrapper builds the gzhttp wrapper used by initHTTP. It
// negotiates gzip and (unless disabled) zstd from Accept-Encoding. Pulled out
// so it can be tested without standing up the full gateway.
func newHTTPCompressionWrapper(cfg config.Config) (func(http.Handler) http.HandlerFunc, error) {
return gzhttp.NewWrapper(
gzhttp.MinSize(cfg.HTTPCompressionMinSize),
gzhttp.EnableZstd(!cfg.DisableZstdCompression),
gzhttp.PreferZstd(!cfg.DisableZstdCompression && cfg.PreferZstd),
)
}
101 changes: 101 additions & 0 deletions compression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package core

import (
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/go-coldbrew/core/config"
)

func TestNewHTTPCompressionWrapper_NegotiatesEncoding(t *testing.T) {
body := strings.Repeat("payload-", 256) // ~2KiB, well above the 256-byte default

cases := []struct {
name string
cfg config.Config
acceptEncoding string
wantEncoding string
}{
{
name: "zstd-preferred-default",
cfg: config.Config{HTTPCompressionMinSize: 256, PreferZstd: true},
acceptEncoding: "gzip, zstd",
wantEncoding: "zstd",
},
{
name: "client-only-gzip",
cfg: config.Config{HTTPCompressionMinSize: 256, PreferZstd: true},
acceptEncoding: "gzip",
wantEncoding: "gzip",
},
{
name: "zstd-disabled-falls-back-to-gzip",
cfg: config.Config{HTTPCompressionMinSize: 256, DisableZstdCompression: true, PreferZstd: true},
acceptEncoding: "gzip, zstd",
wantEncoding: "gzip",
},
{
name: "no-accept-encoding-no-compression",
cfg: config.Config{HTTPCompressionMinSize: 256, PreferZstd: true},
acceptEncoding: "",
wantEncoding: "",
},
{
name: "prefer-zstd-false-picks-gzip-when-equal-q",
cfg: config.Config{HTTPCompressionMinSize: 256, PreferZstd: false},
acceptEncoding: "gzip, zstd",
wantEncoding: "gzip",
},
}

handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(body))
})

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
wrapper, err := newHTTPCompressionWrapper(tc.cfg)
if err != nil {
t.Fatalf("newHTTPCompressionWrapper: %v", err)
}
wrapped := wrapper(handler)

req := httptest.NewRequest(http.MethodGet, "/", nil)
if tc.acceptEncoding != "" {
req.Header.Set("Accept-Encoding", tc.acceptEncoding)
}
rec := httptest.NewRecorder()
wrapped.ServeHTTP(rec, req)

got := rec.Header().Get("Content-Encoding")
if got != tc.wantEncoding {
t.Fatalf("Content-Encoding = %q, want %q", got, tc.wantEncoding)
}
})
}
}

func TestNewHTTPCompressionWrapper_BelowMinSize(t *testing.T) {
cfg := config.Config{HTTPCompressionMinSize: 256, PreferZstd: true}
wrapper, err := newHTTPCompressionWrapper(cfg)
if err != nil {
t.Fatalf("newHTTPCompressionWrapper: %v", err)
}

wrapped := wrapper(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte("tiny"))
}))

req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Accept-Encoding", "gzip, zstd")
rec := httptest.NewRecorder()
wrapped.ServeHTTP(rec, req)

if got := rec.Header().Get("Content-Encoding"); got != "" {
t.Fatalf("Content-Encoding = %q, want empty (body below MinSize)", got)
}
}
13 changes: 10 additions & 3 deletions config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import "github.com/go-coldbrew/core/config"


<a name="Config"></a>
## type [Config](<https://github.com/go-coldbrew/core/blob/main/config/config.go#L13-L204>)
## type [Config](<https://github.com/go-coldbrew/core/blob/main/config/config.go#L13-L211>)

Config is the configuration for the Coldbrew server It is populated from environment variables and has sensible defaults for all fields so that you can just use it as is without any configuration The following environment variables are supported and can be used to override the defaults for the fields

Expand Down Expand Up @@ -248,6 +248,13 @@ type Config struct {
// 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" env:"HTTP_COMPRESSION_MIN_SIZE" default:"256"`
// DisableZstdCompression disables zstd compression on the HTTP gateway. When false
// (default), zstd is offered alongside gzip and selected via Accept-Encoding
// negotiation. Has no effect if DisableHTTPCompression is true.
DisableZstdCompression bool `envconfig:"DISABLE_ZSTD_COMPRESSION" env:"DISABLE_ZSTD_COMPRESSION" default:"false"`
// PreferZstd causes the HTTP gateway to prefer zstd over gzip when a client
// advertises both in Accept-Encoding. Default true. Ignored if zstd is disabled.
PreferZstd bool `envconfig:"PREFER_ZSTD" env:"PREFER_ZSTD" default:"true"`
// 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" env:"RESPONSE_TIME_LOG_LEVEL" default:"info"`
Expand All @@ -262,7 +269,7 @@ type Config struct {
```

<a name="Config.Validate"></a>
### func \(Config\) [Validate](<https://github.com/go-coldbrew/core/blob/main/config/config.go#L209>)
### func \(Config\) [Validate](<https://github.com/go-coldbrew/core/blob/main/config/config.go#L216>)

```go
func (c Config) Validate() []string
Expand All @@ -271,7 +278,7 @@ func (c Config) Validate() []string
Validate checks the configuration for common misconfigurations and returns a list of warning messages. It does not return an error to avoid breaking existing services — warnings are meant to be logged at startup.

<a name="Config.ValidateStrict"></a>
### func \(Config\) [ValidateStrict](<https://github.com/go-coldbrew/core/blob/main/config/config.go#L300>)
### func \(Config\) [ValidateStrict](<https://github.com/go-coldbrew/core/blob/main/config/config.go#L307>)

```go
func (c Config) ValidateStrict() []error
Expand Down
7 changes: 7 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ type Config struct {
// 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" env:"HTTP_COMPRESSION_MIN_SIZE" default:"256"`
// DisableZstdCompression disables zstd compression on the HTTP gateway. When false
// (default), zstd is offered alongside gzip and selected via Accept-Encoding
// negotiation. Has no effect if DisableHTTPCompression is true.
DisableZstdCompression bool `envconfig:"DISABLE_ZSTD_COMPRESSION" env:"DISABLE_ZSTD_COMPRESSION" default:"false"`
// PreferZstd causes the HTTP gateway to prefer zstd over gzip when a client
// advertises both in Accept-Encoding. Default true. Ignored if zstd is disabled.
PreferZstd bool `envconfig:"PREFER_ZSTD" env:"PREFER_ZSTD" default:"true"`
// 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" env:"RESPONSE_TIME_LOG_LEVEL" default:"info"`
Expand Down
5 changes: 3 additions & 2 deletions core.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"github.com/go-coldbrew/workers"
grpcprom "github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/klauspost/compress/gzhttp"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/otel"
Expand Down Expand Up @@ -469,6 +468,8 @@ func (c *cb) initHTTP(ctx context.Context) (*http.Server, error) {
)
}

muxOpts = append(muxOpts, registeredServeMuxOptions()...)

mux := runtime.NewServeMux(muxOpts...)

creds := c.creds
Expand Down Expand Up @@ -522,7 +523,7 @@ func (c *cb) initHTTP(ctx context.Context) (*http.Server, error) {
promHandler := promhttp.Handler()
gzipHandler := http.Handler(tracingWrapper(mux))
if !c.config.DisableHTTPCompression {
wrapper, err := gzhttp.NewWrapper(gzhttp.MinSize(c.config.HTTPCompressionMinSize))
wrapper, err := newHTTPCompressionWrapper(c.config)
if err != nil {
return nil, fmt.Errorf("failed to create compression handler: %w", err)
}
Expand Down
48 changes: 48 additions & 0 deletions serve_mux_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package core

import "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"

// httpServeMuxOptions accumulates options registered via
// RegisterServeMuxOption / RegisterHTTPMarshaler. Init-only — not safe for
// concurrent registration. Matches the existing config-function pattern in
// the interceptors package; the alternative (mutex/atomic) would diverge
// from the rest of the codebase for no real benefit at startup.
var httpServeMuxOptions []runtime.ServeMuxOption

// RegisterServeMuxOption appends a runtime.ServeMuxOption that initHTTP
// passes to runtime.NewServeMux. Registered options are applied AFTER core's
// built-ins (the incoming-header matcher derived from HTTPHeaderPrefixes,
// the application/proto and application/protobuf marshalers, and the
// span-route middleware), so:
//
// - Last-write-wins options — WithMarshalerOption for a given MIME,
// WithErrorHandler, WithRoutingErrorHandler, WithIncomingHeaderMatcher —
// can intentionally override core's defaults. Overriding the incoming
// header matcher disables the HTTPHeaderPrefixes wiring; reimplement it
// yourself if you still need that behavior.
// - Additive options — WithMiddlewares, WithMetadata,
// WithForwardResponseOption — stack with core's.
//
// Must be called before core.Run() (typically from a service's PreStart
// hook). Not safe for concurrent registration.
func RegisterServeMuxOption(opt runtime.ServeMuxOption) {
httpServeMuxOptions = append(httpServeMuxOptions, opt)
}

// RegisterHTTPMarshaler registers a runtime.Marshaler for the given MIME
// type on the HTTP gateway. Equivalent to
// RegisterServeMuxOption(runtime.WithMarshalerOption(mime, m)).
//
// To override the gateway's default fallback for unregistered Content-Types
// (which is protojson via runtime.JSONPb), register for runtime.MIMEWildcard.
//
// Must be called before core.Run(). Not safe for concurrent registration.
func RegisterHTTPMarshaler(mime string, m runtime.Marshaler) {
RegisterServeMuxOption(runtime.WithMarshalerOption(mime, m))
}

// registeredServeMuxOptions returns a defensive copy so callers (initHTTP,
// tests) can append to the result without mutating the package-level slice.
func registeredServeMuxOptions() []runtime.ServeMuxOption {
return append([]runtime.ServeMuxOption(nil), httpServeMuxOptions...)
}
Loading
Loading