dynamic_modules: expose current log level getter for HttpFilterConfigHandle in GO SDK
Project: envoyproxy/envoy | Language: C++ (Go/Rust SDKs) | Concepts: api-design, testing-strategy | Difficulty: mid
Link: https://github.com/envoyproxy/envoy/pull/46116
The problem
The Go SDK for Envoy's dynamic modules did not expose methods on the HttpFilterConfigHandle interface to query the current log level or check if a log level is enabled. This made it harder for filter configuration code to adapt to runtime logging changes, and it limited the ability to test logging-related behavior in filters.
Why it's non-trivial
A naive fix might only expose these logging methods at the filter handle level, not the config handle, or might skip updating the interface and mocks, making testing difficult or inconsistent. The solution needs to ensure both API usability and testability, and keep the Go and Rust SDKs consistent.
The approach
1. Expose new methods on the Go interface
The HttpFilterConfigHandle interface in Go is extended with two new methods:
+ // GetLogLevel returns the current effective log level of the host environment's logging
+ // mechanism. The returned level reflects runtime changes, for example those applied via the
+ // admin API.
+ GetLogLevel() LogLevel
+
+ // IsLogLevelEnabled reports whether the given log level is enabled by the host environment's
+ // logging mechanism. It can be used to skip expensive work that is only needed when a message
+ // at the given level would actually be logged.
+ IsLogLevelEnabled(level LogLevel) bool
This adds the ability for config handles to query log level and check if a level is enabled, matching the filter handle API.
2. Implement the methods in the Go ABI layer
The actual implementation calls through to the C ABI:
func (h *dymConfigHandle) GetLogLevel() shared.LogLevel {
return shared.LogLevel(C.envoy_dynamic_module_callback_get_log_level())
}
func (h *dymConfigHandle) IsLogLevelEnabled(level shared.LogLevel) bool {
return bool(C.envoy_dynamic_module_callback_log_enabled(
(C.envoy_dynamic_module_type_log_level)(uint32(level)),
))
}
These methods bridge Go to the underlying C API, ensuring the Go SDK reflects runtime log level changes.
3. Update mocks for testing
The Go mocks are updated to allow tests to verify logging behavior:
// GetLogLevel mocks base method.
func (m *MockHttpFilterConfigHandle) GetLogLevel() shared.LogLevel {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLogLevel")
ret0, _ := ret[0].(shared.LogLevel)
return ret0
}
// IsLogLevelEnabled mocks base method.
func (m *MockHttpFilterConfigHandle) IsLogLevelEnabled(level shared.LogLevel) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsLogLevelEnabled", level)
ret0, _ := ret[0].(bool)
return ret0
}
This allows unit tests to control and assert log level behavior.
4. Integration tests in Go and Rust
The integration tests are updated to verify the new methods work as expected, both at filter and config handle levels:
// In Go test:
headers.Set("x-log-level", strconv.FormatUint(uint64(p.handle.GetLogLevel()), 10))
headers.Set("x-log-info-enabled", strconv.FormatBool(p.handle.IsLogLevelEnabled(shared.LogLevelInfo)))
headers.Set("x-log-error-enabled", strconv.FormatBool(p.handle.IsLogLevelEnabled(shared.LogLevelError)))
// filter config handle
headers.Set("x-config-log-level", strconv.FormatUint(uint64(p.configLogLevel), 10))
headers.Set("x-config-log-info-enabled", strconv.FormatBool(p.configInfoEnabled))
headers.Set("x-config-log-error-enabled", strconv.FormatBool(p.configErrorEnabled))
The test filter sets headers based on both the filter and config handle log level APIs, ensuring both are exercised.
Rust tests are similarly updated for parity.
From the review thread
HusseinKabbout: A thought that came to my mind is why do we even put the logging methods in the handle interfaces. The Golang Filter API exposes these methods directly. They are not really bound by anything either since they just call the C interface.
wbpcode: Mainly for better testbility.
Takeaways
- Exposing logging state through interfaces, rather than only global functions, improves testability by allowing mocking and dependency injection.
- When adding new interface methods, always update mocks and integration tests to ensure the new API is both tested and testable.
- Keeping SDKs for multiple languages in sync (here, Go and Rust) helps prevent subtle API drift and surprises for users.