From 2cf601a0ca8db22abcaee8fbe778496a52ad3708 Mon Sep 17 00:00:00 2001 From: Rohit Agrawal Date: Thu, 9 Jul 2026 01:46:59 -0700 Subject: [PATCH] dynamic_modules: expose current log level getter Signed-off-by: Rohit Agrawal --- ...ynamic_modules__added-log-level-getter.rst | 4 ++ source/extensions/dynamic_modules/abi/abi.h | 9 +++++ source/extensions/dynamic_modules/abi_impl.cc | 6 +++ .../dynamic_modules/sdk/go/abi/internal.go | 10 +++++ .../dynamic_modules/sdk/go/shared/http.go | 10 +++++ .../sdk/go/shared/mocks/mock_http.go | 28 ++++++++++++++ .../dynamic_modules/sdk/rust/src/lib.rs | 14 +++++++ .../dynamic_modules/sdk/rust/src/lib_test.rs | 34 +++++++++++++++++ test/common/stats/tag_producer_impl_test.cc | 6 +-- test/extensions/dynamic_modules/BUILD | 1 + .../dynamic_modules/abi_impl_test.cc | 28 ++++++++++++++ test/extensions/dynamic_modules/http/BUILD | 1 + .../dynamic_modules/http/integration_test.cc | 38 +++++++++++++++++++ .../http_integration_test.go | 35 +++++++++++++++++ .../test_data/rust/http_integration_test.rs | 27 +++++++++++++ 15 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 changelogs/current/new_features/dynamic_modules__added-log-level-getter.rst diff --git a/changelogs/current/new_features/dynamic_modules__added-log-level-getter.rst b/changelogs/current/new_features/dynamic_modules__added-log-level-getter.rst new file mode 100644 index 0000000000000..0fcf607441aa3 --- /dev/null +++ b/changelogs/current/new_features/dynamic_modules__added-log-level-getter.rst @@ -0,0 +1,4 @@ +Added ``envoy_dynamic_module_callback_get_log_level`` ABI callback that returns the current effective +log level of the dynamic modules logging stream. Exposed in the Rust SDK as ``get_log_level`` (with +``is_log_enabled`` to check a specific level) and on the Go HTTP filter handle as ``GetLogLevel`` and +``IsLogLevelEnabled``. diff --git a/source/extensions/dynamic_modules/abi/abi.h b/source/extensions/dynamic_modules/abi/abi.h index d15de88621066..f6b86c09ce534 100644 --- a/source/extensions/dynamic_modules/abi/abi.h +++ b/source/extensions/dynamic_modules/abi/abi.h @@ -495,6 +495,15 @@ void envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level level */ bool envoy_dynamic_module_callback_log_enabled(envoy_dynamic_module_type_log_level level); +/** + * envoy_dynamic_module_callback_get_log_level is called by the module to get the current effective + * log level for the dynamic modules Id. This allows the module to align its own verbosity with the + * level configured on the Envoy side, including changes applied at runtime via the admin API. + * + * @return the current effective log level as envoy_dynamic_module_type_log_level. + */ +envoy_dynamic_module_type_log_level envoy_dynamic_module_callback_get_log_level(); + // --------------------------------- Threading ----------------------------------- /** diff --git a/source/extensions/dynamic_modules/abi_impl.cc b/source/extensions/dynamic_modules/abi_impl.cc index 9d63966aa271f..e167cefca8ab7 100644 --- a/source/extensions/dynamic_modules/abi_impl.cc +++ b/source/extensions/dynamic_modules/abi_impl.cc @@ -67,6 +67,12 @@ void envoy_dynamic_module_callback_log(envoy_dynamic_module_type_log_level level } } +envoy_dynamic_module_type_log_level envoy_dynamic_module_callback_get_log_level() { + // The ABI log level enum mirrors spdlog::level::level_enum, so the cast is a direct mapping. + return static_cast( + Envoy::Logger::Registry::getLog(Envoy::Logger::Id::dynamic_modules).level()); +} + uint32_t envoy_dynamic_module_callback_get_concurrency() { using namespace Envoy; // The previous `ASSERT_IS_MAIN_OR_TEST_THREAD` is compiled out under NDEBUG and the diff --git a/source/extensions/dynamic_modules/sdk/go/abi/internal.go b/source/extensions/dynamic_modules/sdk/go/abi/internal.go index 374d37f4b4033..d5a27ea924f3f 100644 --- a/source/extensions/dynamic_modules/sdk/go/abi/internal.go +++ b/source/extensions/dynamic_modules/sdk/go/abi/internal.go @@ -1351,6 +1351,16 @@ func (h *dymHttpFilterHandle) Log(level shared.LogLevel, format string, args ... hostLog(level, format, args) } +func (h *dymHttpFilterHandle) GetLogLevel() shared.LogLevel { + return shared.LogLevel(C.envoy_dynamic_module_callback_get_log_level()) +} + +func (h *dymHttpFilterHandle) IsLogLevelEnabled(level shared.LogLevel) bool { + return bool(C.envoy_dynamic_module_callback_log_enabled( + (C.envoy_dynamic_module_type_log_level)(uint32(level)), + )) +} + func (h *dymHttpFilterHandle) HttpCallout( cluster string, headers [][2]string, body []byte, timeoutMs uint64, cb shared.HttpCalloutCallback) (shared.HttpCalloutInitResult, uint64) { diff --git a/source/extensions/dynamic_modules/sdk/go/shared/http.go b/source/extensions/dynamic_modules/sdk/go/shared/http.go index c4f1a580f958a..80097a49f4929 100644 --- a/source/extensions/dynamic_modules/sdk/go/shared/http.go +++ b/source/extensions/dynamic_modules/sdk/go/shared/http.go @@ -432,6 +432,16 @@ type HttpFilterHandle interface { // Log will log the given message via the host environment's logging mechanism. Log(level LogLevel, format string, args ...any) + // 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 + // HttpCallout performs an HTTP call to an external service. The call is asynchronous; the // response, or an error, is delivered to the provided callback. // diff --git a/source/extensions/dynamic_modules/sdk/go/shared/mocks/mock_http.go b/source/extensions/dynamic_modules/sdk/go/shared/mocks/mock_http.go index 56eea616bfb3b..7c5bf1e16a775 100644 --- a/source/extensions/dynamic_modules/sdk/go/shared/mocks/mock_http.go +++ b/source/extensions/dynamic_modules/sdk/go/shared/mocks/mock_http.go @@ -860,6 +860,20 @@ func (mr *MockHttpFilterHandleMockRecorder) GetFilterStateTyped(key any) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFilterStateTyped", reflect.TypeOf((*MockHttpFilterHandle)(nil).GetFilterStateTyped), key) } +// GetLogLevel mocks base method. +func (m *MockHttpFilterHandle) GetLogLevel() shared.LogLevel { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLogLevel") + ret0, _ := ret[0].(shared.LogLevel) + return ret0 +} + +// GetLogLevel indicates an expected call of GetLogLevel. +func (mr *MockHttpFilterHandleMockRecorder) GetLogLevel() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogLevel", reflect.TypeOf((*MockHttpFilterHandle)(nil).GetLogLevel)) +} + // GetMetadataBool mocks base method. func (m *MockHttpFilterHandle) GetMetadataBool(source shared.MetadataSourceType, metadataNamespace, key string) (bool, bool) { m.ctrl.T.Helper() @@ -1118,6 +1132,20 @@ func (mr *MockHttpFilterHandleMockRecorder) IncrementGaugeValue(id, value any, t return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementGaugeValue", reflect.TypeOf((*MockHttpFilterHandle)(nil).IncrementGaugeValue), varargs...) } +// IsLogLevelEnabled mocks base method. +func (m *MockHttpFilterHandle) IsLogLevelEnabled(level shared.LogLevel) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsLogLevelEnabled", level) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsLogLevelEnabled indicates an expected call of IsLogLevelEnabled. +func (mr *MockHttpFilterHandleMockRecorder) IsLogLevelEnabled(level any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsLogLevelEnabled", reflect.TypeOf((*MockHttpFilterHandle)(nil).IsLogLevelEnabled), level) +} + // Log mocks base method. func (m *MockHttpFilterHandle) Log(level shared.LogLevel, format string, args ...any) { m.ctrl.T.Helper() diff --git a/source/extensions/dynamic_modules/sdk/rust/src/lib.rs b/source/extensions/dynamic_modules/sdk/rust/src/lib.rs index 6e7f9b6d96b5a..2f4c1968950c6 100644 --- a/source/extensions/dynamic_modules/sdk/rust/src/lib.rs +++ b/source/extensions/dynamic_modules/sdk/rust/src/lib.rs @@ -408,6 +408,20 @@ macro_rules! envoy_log { }; } +/// Get the current effective log level of the dynamic modules logging stream. This can be used to +/// align in-module verbosity with the level configured on the Envoy side, including changes applied +/// at runtime via the admin API. +pub fn get_log_level() -> abi::envoy_dynamic_module_type_log_level { + unsafe { abi::envoy_dynamic_module_callback_get_log_level() } +} + +/// Check whether the given log level is enabled for the dynamic modules logging stream. This can be +/// used to skip expensive work that is only needed when a message at the given level would actually +/// be logged. +pub fn is_log_enabled(level: abi::envoy_dynamic_module_type_log_level) -> bool { + unsafe { abi::envoy_dynamic_module_callback_log_enabled(level) } +} + /// Guard macro that ensures each factory `OnceLock` is registered by exactly one module. /// /// When the same module is re-initialized (for example, static modules loaded multiple times diff --git a/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs b/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs index 2b19e10599b87..246b11aedeb65 100644 --- a/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs +++ b/source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs @@ -14,6 +14,40 @@ fn test_loggers() { envoy_log_error!("message with an argument: {}", "argument"); } +// Mock storage backing the log level callbacks so the unit tests can exercise the SDK wrappers +// without the Envoy host symbols. +static MOCK_LOG_LEVEL: std::sync::Mutex = + std::sync::Mutex::new(abi::envoy_dynamic_module_type_log_level::Info); + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_get_log_level( +) -> abi::envoy_dynamic_module_type_log_level { + *MOCK_LOG_LEVEL.lock().unwrap() +} + +#[no_mangle] +pub extern "C" fn envoy_dynamic_module_callback_log_enabled( + level: abi::envoy_dynamic_module_type_log_level, +) -> bool { + // A level is enabled when it is at or above the currently configured level. + (*MOCK_LOG_LEVEL.lock().unwrap() as u32) <= (level as u32) +} + +#[test] +fn test_log_level_callbacks() { + use abi::envoy_dynamic_module_type_log_level as Level; + + *MOCK_LOG_LEVEL.lock().unwrap() = Level::Warn; + assert_eq!(get_log_level(), Level::Warn); + assert!(!is_log_enabled(Level::Info)); + assert!(is_log_enabled(Level::Warn)); + assert!(is_log_enabled(Level::Error)); + + *MOCK_LOG_LEVEL.lock().unwrap() = Level::Trace; + assert_eq!(get_log_level(), Level::Trace); + assert!(is_log_enabled(Level::Trace)); +} + #[test] fn test_envoy_dynamic_module_on_http_filter_config_new_impl() { struct TestHttpFilterConfig; diff --git a/test/common/stats/tag_producer_impl_test.cc b/test/common/stats/tag_producer_impl_test.cc index f44fedf97cd0f..fad0eee5f68f3 100644 --- a/test/common/stats/tag_producer_impl_test.cc +++ b/test/common/stats/tag_producer_impl_test.cc @@ -1,4 +1,5 @@ #include +#include #include "envoy/config/metrics/v3/stats.pb.h" @@ -8,7 +9,6 @@ #include "test/test_common/logging.h" #include "test/test_common/utility.h" -#include "absl/types/optional.h" #include "gtest/gtest.h" namespace Envoy { @@ -32,13 +32,13 @@ class TagProducerTest : public testing::Test { } } - absl::optional findTag(const TagVector& tags, const std::string& name) { + std::optional findTag(const TagVector& tags, const std::string& name) { for (const auto& tag : tags) { if (tag.name_ == name) { return tag.value_; } } - return absl::nullopt; + return std::nullopt; } envoy::config::metrics::v3::StatsConfig stats_config_; diff --git a/test/extensions/dynamic_modules/BUILD b/test/extensions/dynamic_modules/BUILD index 747b3feacb705..dca225395b0bd 100644 --- a/test/extensions/dynamic_modules/BUILD +++ b/test/extensions/dynamic_modules/BUILD @@ -54,6 +54,7 @@ envoy_cc_test( size = "large", srcs = ["abi_impl_test.cc"], deps = [ + "//source/common/common:minimal_logger_lib", "//source/extensions/dynamic_modules:abi_impl", "//test/mocks/server:server_factory_context_mocks", "//test/test_common:utility_lib", diff --git a/test/extensions/dynamic_modules/abi_impl_test.cc b/test/extensions/dynamic_modules/abi_impl_test.cc index 94221f3b524c1..15a9c71ae11a0 100644 --- a/test/extensions/dynamic_modules/abi_impl_test.cc +++ b/test/extensions/dynamic_modules/abi_impl_test.cc @@ -1,5 +1,6 @@ #include +#include "source/common/common/logger.h" #include "source/extensions/dynamic_modules/abi/abi.h" #include "test/mocks/server/server_factory_context.h" @@ -96,6 +97,33 @@ TEST(CommonAbiImplTest, GetConcurrencyBeforeServerContextFailsClosed) { "context was initialized"); } +// ============================================================================= +// Log Level Tests +// ============================================================================= + +// Verifies that `get_log_level` reflects the level configured on the dynamic modules logger for +// every level in the enum. +TEST(CommonAbiImplTest, GetLogLevelReflectsConfiguredLevel) { + auto& logger = Logger::Registry::getLog(Logger::Id::dynamic_modules); + const spdlog::level::level_enum original_level = logger.level(); + + const std::pair cases[] = { + {spdlog::level::trace, envoy_dynamic_module_type_log_level_Trace}, + {spdlog::level::debug, envoy_dynamic_module_type_log_level_Debug}, + {spdlog::level::info, envoy_dynamic_module_type_log_level_Info}, + {spdlog::level::warn, envoy_dynamic_module_type_log_level_Warn}, + {spdlog::level::err, envoy_dynamic_module_type_log_level_Error}, + {spdlog::level::critical, envoy_dynamic_module_type_log_level_Critical}, + {spdlog::level::off, envoy_dynamic_module_type_log_level_Off}, + }; + for (const auto& [spdlog_level, abi_level] : cases) { + logger.set_level(spdlog_level); + EXPECT_EQ(abi_level, envoy_dynamic_module_callback_get_log_level()); + } + + logger.set_level(original_level); +} + // ============================================================================= // Function Registry Tests // ============================================================================= diff --git a/test/extensions/dynamic_modules/http/BUILD b/test/extensions/dynamic_modules/http/BUILD index 4306b0de1f834..898343dd3630d 100644 --- a/test/extensions/dynamic_modules/http/BUILD +++ b/test/extensions/dynamic_modules/http/BUILD @@ -151,6 +151,7 @@ envoy_cc_test( }, rbe_pool = "6gig", deps = [ + "//source/common/common:minimal_logger_lib", "//source/extensions/dynamic_modules:abi_impl", "//source/extensions/filters/http/dynamic_modules:abi_impl", "//source/extensions/filters/http/dynamic_modules:factory_registration", diff --git a/test/extensions/dynamic_modules/http/integration_test.cc b/test/extensions/dynamic_modules/http/integration_test.cc index 2b1eea7764b04..7d780955da1a9 100644 --- a/test/extensions/dynamic_modules/http/integration_test.cc +++ b/test/extensions/dynamic_modules/http/integration_test.cc @@ -1,6 +1,7 @@ #include "envoy/extensions/filters/http/dynamic_modules/v3/dynamic_modules.pb.h" #include "source/common/common/base64.h" +#include "source/common/common/logger.h" #include "test/extensions/dynamic_modules/util.h" #include "test/integration/http_integration.h" @@ -191,6 +192,43 @@ TEST_P(DynamicModulesIntegrationTest, UpstreamConnectionId) { EXPECT_EQ("200", response->headers().Status()->value().getStringView()); } +TEST_P(DynamicModulesIntegrationTest, LogLevel) { + if (GetParam() == "cpp") { + GTEST_SKIP() << "the log_level filter is only in the rust and go test modules"; + } + + // Pin the dynamic modules logger to a known level so the assertions are deterministic, and + // restore it afterwards to avoid affecting other tests. + auto& logger = Logger::Registry::getLog(Logger::Id::dynamic_modules); + const spdlog::level::level_enum original_level = logger.level(); + logger.set_level(spdlog::level::warn); + + initializeFilter("log_level"); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/test/long/url"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = sendRequestAndWaitForResponse(request_headers, 0, default_response_headers_, 0); + + logger.set_level(original_level); + + EXPECT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().Status()->value().getStringView()); + // Warn is index 3 in the ABI log level enum. + EXPECT_EQ( + "3", + response->headers().get(Http::LowerCaseString("x-log-level"))[0]->value().getStringView()); + // Info is below the configured level so it is disabled, Error is above it so it is enabled. + EXPECT_EQ("false", response->headers() + .get(Http::LowerCaseString("x-log-info-enabled"))[0] + ->value() + .getStringView()); + EXPECT_EQ("true", response->headers() + .get(Http::LowerCaseString("x-log-error-enabled"))[0] + ->value() + .getStringView()); +} + TEST_P(DynamicModulesIntegrationTest, HeaderCallbacks) { runHeaderCallbacksTest(false); } TEST_P(DynamicModulesIntegrationTest, HeaderCallbacksWithUpstreamFilter) { diff --git a/test/extensions/dynamic_modules/test_data/go/http_integration_test/http_integration_test.go b/test/extensions/dynamic_modules/test_data/go/http_integration_test/http_integration_test.go index 2dd1f00a70d7c..4adf8b6a98077 100644 --- a/test/extensions/dynamic_modules/test_data/go/http_integration_test/http_integration_test.go +++ b/test/extensions/dynamic_modules/test_data/go/http_integration_test/http_integration_test.go @@ -37,6 +37,7 @@ func init() { "http_config_stream": &HttpConfigStreamConfigFactory{}, "http_struct_config": &HttpStructConfigFactory{}, "list_metadata_callbacks": &ListMetadataCallbacksConfigFactory{}, + "log_level": &LogLevelConfigFactory{}, }) } @@ -1477,3 +1478,37 @@ func (f *ListMetadataCallbacksFilter) OnResponseHeaders(headers shared.HeaderMap return shared.HeadersStatusContinue } + +// ----------------------------------------------------------------------------- +// LogLevel +// ----------------------------------------------------------------------------- + +type LogLevelConfigFactory struct { + shared.EmptyHttpFilterConfigFactory +} + +func (f *LogLevelConfigFactory) Create(handle shared.HttpFilterConfigHandle, + config []byte) (shared.HttpFilterFactory, error) { + return &LogLevelFilterFactory{}, nil +} + +type LogLevelFilterFactory struct { + shared.EmptyHttpFilterFactory +} + +func (f *LogLevelFilterFactory) Create(handle shared.HttpFilterHandle) shared.HttpFilter { + return &LogLevelFilter{handle: handle} +} + +type LogLevelFilter struct { + shared.EmptyHttpFilter + handle shared.HttpFilterHandle +} + +func (p *LogLevelFilter) OnResponseHeaders(headers shared.HeaderMap, + endOfStream bool) shared.HeadersStatus { + 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))) + return shared.HeadersStatusContinue +} diff --git a/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs b/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs index 7050ce0d8cfde..f9b8dbc2a4bf9 100644 --- a/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs +++ b/test/extensions/dynamic_modules/test_data/rust/http_integration_test.rs @@ -155,6 +155,7 @@ fn new_http_filter_config_fn( "list_metadata_callbacks" => Some(Box::new(ListMetadataCallbacksFilterConfig {})), "filter_state_object_recreate" => Some(Box::new(FilterStateObjectRecreateFilterConfig {})), "upstream_connection_id" => Some(Box::new(UpstreamConnectionIdFilterConfig {})), + "log_level" => Some(Box::new(LogLevelFilterConfig {})), _ => panic!("Unknown filter name: {name}"), } } @@ -438,6 +439,32 @@ impl HttpFilter for UpstreamConnectionIdFilter { } } +struct LogLevelFilterConfig {} + +impl HttpFilterConfig for LogLevelFilterConfig { + fn new_http_filter(&self, _envoy: &mut EHF) -> Box> { + Box::new(LogLevelFilter {}) + } +} + +struct LogLevelFilter {} + +impl HttpFilter for LogLevelFilter { + fn on_response_headers( + &mut self, + envoy_filter: &mut EHF, + _end_of_stream: bool, + ) -> envoy_dynamic_module_type_on_http_filter_response_headers_status { + let level = (get_log_level() as u32).to_string(); + envoy_filter.set_response_header("x-log-level", level.as_bytes()); + let info_enabled = is_log_enabled(envoy_dynamic_module_type_log_level::Info).to_string(); + envoy_filter.set_response_header("x-log-info-enabled", info_enabled.as_bytes()); + let error_enabled = is_log_enabled(envoy_dynamic_module_type_log_level::Error).to_string(); + envoy_filter.set_response_header("x-log-error-enabled", error_enabled.as_bytes()); + envoy_dynamic_module_type_on_http_filter_response_headers_status::Continue + } +} + struct HeadersHttpFilterConfig { headers_to_add: String, }