diff --git a/api/envoy/extensions/wasm/v3/wasm.proto b/api/envoy/extensions/wasm/v3/wasm.proto index 54ef437cbaf7e..10d2cddb87f57 100644 --- a/api/envoy/extensions/wasm/v3/wasm.proto +++ b/api/envoy/extensions/wasm/v3/wasm.proto @@ -40,7 +40,7 @@ message SanitizationConfig { } // Configuration for a Wasm VM. -// [#next-free-field: 8] +// [#next-free-field: 9] message VmConfig { // An ID which will be used along with a hash of the wasm code (or the name of the registered Null // VM plugin) to determine which VM will be used for the plugin. All plugins which use the same @@ -106,6 +106,19 @@ message VmConfig { // vars just like when you do on native platforms. // Warning: Envoy rejects the configuration if there's conflict of key space. EnvironmentVariables environment_variables = 7; + + // Specifies the log destination for the plugin. + // If not specified, the plugin will log to the default Envoy log + // example: "audit-logs": { file_path: "/var/log/audit.log" } + map log_destination = 8; +} + +message LogDestination { + // Target destination for the log messages from the plugin. + oneof target { + string file_path = 1; + // TODO: Remote logging service + } } message EnvironmentVariables { diff --git a/source/extensions/common/wasm/context.cc b/source/extensions/common/wasm/context.cc index bf32ec324a087..79ea81612e69c 100644 --- a/source/extensions/common/wasm/context.cc +++ b/source/extensions/common/wasm/context.cc @@ -1196,6 +1196,32 @@ WasmResult Context::log(uint32_t level, std::string_view message) { PANIC_DUE_TO_CORRUPT_ENUM; } +WasmResult Context::logWithDestination(uint32_t level, std::string_view message, + std::string_view destination) { + const auto& log_destinations = wasm()->log_destinations(); + // iterate over log_destinations map to check if dest + // destination requested by plugin exists + for (const auto& e : log_destinations) { + if (e.first == destination) { + // write message to the file which is the value of the key if it exists + std::ofstream log_file; + log_file.open(e.second, std::ios::out | std::ios_base::app); + if (!log_file) { + std::stringstream buf; + buf << "Failed to open log file: " << e.second; + log(level, buf.str()); + return WasmResult::InvalidMemoryAccess; + } + log_file << message << std::endl; + log_file.close(); + return WasmResult::Ok; + } + } + log(level, "logWithDestination: destination requested by plugin does not exist: " + + std::string(destination)); // log to default destination i.e proxy logs + return WasmResult::NotFound; +} + uint32_t Context::getLogLevel() { // Like the "log" call above, assume that spdlog level as an int // matches the enum in the SDK diff --git a/source/extensions/common/wasm/context.h b/source/extensions/common/wasm/context.h index 440e1c3e9efcc..16f10bf789ed0 100644 --- a/source/extensions/common/wasm/context.h +++ b/source/extensions/common/wasm/context.h @@ -202,6 +202,8 @@ class Context : public proxy_wasm::ContextBase, uint64_t getMonotonicTimeNanoseconds() override; std::string_view getConfiguration() override; std::pair getStatus() override; + WasmResult logWithDestination(uint32_t level, std::string_view message, + std::string_view destination) override; // State accessors WasmResult getProperty(std::string_view path, std::string* result) override; diff --git a/source/extensions/common/wasm/plugin.cc b/source/extensions/common/wasm/plugin.cc index ae1628981e870..c1ea57787ad58 100644 --- a/source/extensions/common/wasm/plugin.cc +++ b/source/extensions/common/wasm/plugin.cc @@ -52,6 +52,33 @@ WasmConfig::WasmConfig(const envoy::extensions::wasm::v3::PluginConfig& config) } } } + + if (config.vm_config().runtime() == "envoy.wasm.runtime.null" && + !config_.vm_config().log_destination().empty()) { + throw EnvoyException("envoy.extensions.wasm.v3.VmConfig.log_destination must " + "not be set for NullVm."); + } + // Check key duplication. + absl::flat_hash_set keys; + for (const auto& ld : config_.vm_config().log_destination()) { + if (!keys.insert(ld.first).second) { + throw EnvoyException(fmt::format("Key {} is duplicated in " + "envoy.extensions.wasm.v3.VmConfig.log_destination for {}. " + "All the keys must be unique.", + ld.first, config_.name())); + } + } + // Construct merged key-value pairs. Also check for boundary conditions + // (e.g. empty file path). + for (const auto& ld : config_.vm_config().log_destination()) { + if (ld.second.file_path().empty()) { + throw EnvoyException( + fmt::format("Key {} value envoy.extensions.wasm.v3.VmConfig.LogDestination.file_path " + "must not be empty for {}.", + ld.first, config_.name())); + } + log_destinations_[ld.first] = ld.second.file_path(); + } } } // namespace Wasm diff --git a/source/extensions/common/wasm/plugin.h b/source/extensions/common/wasm/plugin.h index 4268c0476e5dd..a192da3127011 100644 --- a/source/extensions/common/wasm/plugin.h +++ b/source/extensions/common/wasm/plugin.h @@ -18,6 +18,7 @@ namespace Wasm { // clang-format off using EnvironmentVariableMap = std::unordered_map; +using LogDestinationMap = std::unordered_map; // clang-format on class WasmConfig { @@ -26,11 +27,13 @@ class WasmConfig { const envoy::extensions::wasm::v3::PluginConfig& config() { return config_; } proxy_wasm::AllowedCapabilitiesMap& allowedCapabilities() { return allowed_capabilities_; } EnvironmentVariableMap& environmentVariables() { return envs_; } + LogDestinationMap& logDestinations() { return log_destinations_; } private: const envoy::extensions::wasm::v3::PluginConfig config_; proxy_wasm::AllowedCapabilitiesMap allowed_capabilities_{}; EnvironmentVariableMap envs_; + LogDestinationMap log_destinations_; }; using WasmConfigPtr = std::unique_ptr; diff --git a/source/extensions/common/wasm/wasm.cc b/source/extensions/common/wasm/wasm.cc index 3ba2275915b07..d3b68848bfe29 100644 --- a/source/extensions/common/wasm/wasm.cc +++ b/source/extensions/common/wasm/wasm.cc @@ -75,10 +75,11 @@ void Wasm::initializeLifecycle(Server::ServerLifecycleNotifier& lifecycle_notifi Wasm::Wasm(WasmConfig& config, absl::string_view vm_key, const Stats::ScopeSharedPtr& scope, Api::Api& api, Upstream::ClusterManager& cluster_manager, Event::Dispatcher& dispatcher) - : WasmBase( - createWasmVm(config.config().vm_config().runtime()), config.config().vm_config().vm_id(), - MessageUtil::anyToBytes(config.config().vm_config().configuration()), - toStdStringView(vm_key), config.environmentVariables(), config.allowedCapabilities()), + : WasmBase(createWasmVm(config.config().vm_config().runtime()), + config.config().vm_config().vm_id(), + MessageUtil::anyToBytes(config.config().vm_config().configuration()), + toStdStringView(vm_key), config.environmentVariables(), config.logDestinations(), + config.allowedCapabilities()), scope_(scope), api_(api), stat_name_pool_(scope_->symbolTable()), custom_stat_namespace_(stat_name_pool_.add(CustomStatNamespace)), cluster_manager_(cluster_manager), dispatcher_(dispatcher), @@ -330,7 +331,8 @@ bool createWasm(const PluginSharedPtr& plugin, const Stats::ScopeSharedPtr& scop code_cache = new std::remove_reference::type; } Stats::ScopeSharedPtr create_wasm_stats_scope = stats_handler.lockAndCreateStats(scope); - // Remove entries older than CODE_CACHE_SECONDS_CACHING_TTL except for our target. + // Remove entries older than CODE_CACHE_SECONDS_CACHING_TTL except for our + // target. for (auto it = code_cache->begin(); it != code_cache->end();) { if (now - it->second.use_time > std::chrono::seconds(CODE_CACHE_SECONDS_CACHING_TTL) && it->first != vm_config.code().remote().sha256()) { @@ -423,8 +425,8 @@ bool createWasm(const PluginSharedPtr& plugin, const Stats::ScopeSharedPtr& scop } stats_handler.onRemoteCacheEntriesChanged(code_cache->size()); } - // NB: xDS currently does not support failing asynchronously, so we fail immediately - // if remote Wasm code is not cached and do a background fill. + // NB: xDS currently does not support failing asynchronously, so we fail + // immediately if remote Wasm code is not cached and do a background fill. if (!vm_config.nack_on_code_cache_miss()) { if (code.empty()) { ENVOY_LOG_TO_LOGGER(Envoy::Logger::Registry::getLog(Envoy::Logger::Id::wasm), trace,