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
Original file line number Diff line number Diff line change
@@ -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``.
9 changes: 9 additions & 0 deletions source/extensions/dynamic_modules/abi/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we deprecate envoy_dynamic_module_callback_log_enabled?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can do a follow-up to deprecate it


/**
* 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 -----------------------------------

/**
Expand Down
6 changes: 6 additions & 0 deletions source/extensions/dynamic_modules/abi_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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_dynamic_module_type_log_level>(
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
Expand Down
10 changes: 10 additions & 0 deletions source/extensions/dynamic_modules/sdk/go/abi/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions source/extensions/dynamic_modules/sdk/go/shared/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions source/extensions/dynamic_modules/sdk/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 34 additions & 0 deletions source/extensions/dynamic_modules/sdk/rust/src/lib_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<abi::envoy_dynamic_module_type_log_level> =
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;
Expand Down
6 changes: 3 additions & 3 deletions test/common/stats/tag_producer_impl_test.cc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <algorithm>
#include <optional>

#include "envoy/config/metrics/v3/stats.pb.h"

Expand All @@ -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 {
Expand All @@ -32,13 +32,13 @@ class TagProducerTest : public testing::Test {
}
}

absl::optional<std::string> findTag(const TagVector& tags, const std::string& name) {
std::optional<std::string> 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_;
Expand Down
1 change: 1 addition & 0 deletions test/extensions/dynamic_modules/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
28 changes: 28 additions & 0 deletions test/extensions/dynamic_modules/abi_impl_test.cc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include <thread>

#include "source/common/common/logger.h"
#include "source/extensions/dynamic_modules/abi/abi.h"

#include "test/mocks/server/server_factory_context.h"
Expand Down Expand Up @@ -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<spdlog::level::level_enum, envoy_dynamic_module_type_log_level> 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
// =============================================================================
Expand Down
1 change: 1 addition & 0 deletions test/extensions/dynamic_modules/http/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 38 additions & 0 deletions test/extensions/dynamic_modules/http/integration_test.cc
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func init() {
"http_config_stream": &HttpConfigStreamConfigFactory{},
"http_struct_config": &HttpStructConfigFactory{},
"list_metadata_callbacks": &ListMetadataCallbacksConfigFactory{},
"log_level": &LogLevelConfigFactory{},
})
}

Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ fn new_http_filter_config_fn<EC: EnvoyHttpFilterConfig, EHF: EnvoyHttpFilter>(
"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}"),
}
}
Expand Down Expand Up @@ -438,6 +439,32 @@ impl<EHF: EnvoyHttpFilter> HttpFilter<EHF> for UpstreamConnectionIdFilter {
}
}

struct LogLevelFilterConfig {}

impl<EHF: EnvoyHttpFilter> HttpFilterConfig<EHF> for LogLevelFilterConfig {
fn new_http_filter(&self, _envoy: &mut EHF) -> Box<dyn HttpFilter<EHF>> {
Box::new(LogLevelFilter {})
}
}

struct LogLevelFilter {}

impl<EHF: EnvoyHttpFilter> HttpFilter<EHF> 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,
}
Expand Down
Loading