Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ cmake_install.cmake
install_manifest.txt
compile_commands.json

# Logging
/mlog/

# Ninja
.ninja_deps
.ninja_log
Expand Down
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ add_library(moqx_core STATIC
src/UpstreamProvider.cpp
src/relay/TopNFilter.cpp
src/relay/PropertyRanking.cpp
src/logging/MLogCleaner.cpp
)

target_include_directories(moqx_core
Expand All @@ -110,6 +111,9 @@ target_link_libraries(moqx_core PUBLIC
# FizzAcceptorHandshakeHelper but omits this dep from its cmake config.
Folly::folly_io_async_fdsock_async_fd_socket
moxygen::moxygen_moqclient
moxygen::moxygen_mlog_file_mlogger
moxygen::moxygen_mlog_mlogger_factory
moxygen::moxygen_mlog_sampling_mlogger_factory
)

target_compile_options(moqx_core PRIVATE -Wall -Wextra -Wpedantic)
Expand Down
9 changes: 9 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ admin:
# cert_file: /path/to/cert.pem
# key_file: /path/to/key.pem

logging: # Logging configuration (optional)
mlog: # MoQ-level structured logging
dir: "./mlog" # Output directory for per-session mlog files
sample_rate: 1 # Fraction of sessions to log (0.0–1.0, default: 1.0)
# max_age_days: 7 # Delete .mlog files older than N days (omit = no age limit)
# max_dir_mb: 1024 # Trim directory to N MB by deleting oldest files first (omit = no size limit)
# cleanup_interval_secs: 600 # How often to run cleanup in seconds (default: 600 = 10 min)


# relay_id: "my-relay-1" # Relay identity (optional; random hex string generated if absent)

# listener_defaults: # Default settings inherited by all listeners
Expand Down
44 changes: 44 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,50 @@ upstream routing.

---

## Logging

### `logging.mlog`

Enables MoQ-level (mlog) structured logging of control messages and protocol
events, one file per session.

```yaml
logging:
mlog:
dir: "/var/log/moqx/mlog" # required to enable; empty string disables
sample_rate: 0.01 # log 1% of sessions (default: 1.0 = all)
max_age_days: 7 # delete files older than 7 days
max_dir_mb: 1024 # trim to 1 GB, deleting oldest files first
```

| Field | Type | Default | Description |
|---|---|---|---|
| `dir` | `string` | — (disabled) | Output directory. Each session writes to `<dir>/<dcid>.mlog`. If empty, mlog is disabled. |
| `sample_rate` | `float` | `1.0` | Fraction of sessions to log, in `[0.0, 1.0]`. `1.0` logs all sessions; `0.01` logs ~1%. |
| `max_age_days` | `uint` | — (no limit) | Delete `.mlog` files whose last-write time is older than this many days. Omit to keep all files regardless of age. Must be ≥ 1 if set. |
| `max_dir_mb` | `uint` | — (no limit) | After age-based deletion, if the combined size of remaining `.mlog` files exceeds this value (in MB), the oldest files are deleted until the directory is under the limit. Omit for no size cap. Must be ≥ 1 if set. |

#### Retention behaviour

The two limits are applied together on each cleanup pass (at startup and then
periodically at the configured `cleanup_interval_secs`):

1. **Age** — any `.mlog` file older than `max_age_days` is deleted first.
2. **Size** — if the remaining files still exceed `max_dir_mb`, the oldest
files (by last-write time) are deleted until the directory is under the
limit.

Either limit can be set independently; both are optional.

#### Lifecycle

| Field | Lifecycle |
|---|---|
| `dir`, `sample_rate` | Static — requires restart |
| `max_age_days`, `max_dir_mb`, `cleanup_interval_secs` | Static — requires restart |

---

## Admin Server

The admin server exposes an HTTP management API. It is optional; omit the
Expand Down
5 changes: 5 additions & 0 deletions src/MoqxPicoRelayServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "stats/StatsRegistry.h"
#include <folly/executors/IOThreadPoolExecutor.h>
#include <moxygen/events/MoQExecutor.h>
#include <moxygen/mlog/MLoggerFactory.h>
#include <moxygen/openmoq/transport/pico/MoQPicoQuicEventBaseServer.h>
#include <proxygen/lib/http/webtransport/WebTransport.h>

Expand All @@ -35,6 +36,10 @@ class MoqxPicoRelayServer : public moxygen::MoQPicoQuicEventBaseServer {

void setStatsRegistry(std::shared_ptr<stats::StatsRegistry> registry);

void setMLoggerFactory(std::shared_ptr<moxygen::MLoggerFactory> factory) {
moxygen::MoQServerBase::setMLoggerFactory(std::move(factory));
}

// Preferred entry point: binds the address from the stored ListenerConfig.
void start();

Expand Down
5 changes: 5 additions & 0 deletions src/MoqxRelayServer.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include "stats/StatsRegistry.h"
#include <folly/executors/IOThreadPoolExecutor.h>
#include <moxygen/MoQServer.h>
#include <moxygen/mlog/MLoggerFactory.h>

namespace openmoq::moqx {

Expand All @@ -28,6 +29,10 @@ class MoqxRelayServer : public moxygen::MoQServer {

void setStatsRegistry(std::shared_ptr<stats::StatsRegistry> registry);

void setMLoggerFactory(std::shared_ptr<moxygen::MLoggerFactory> factory) {
moxygen::MoQServerBase::setMLoggerFactory(std::move(factory));
}

// Preferred entry point: binds the address from the stored ListenerConfig.
void start();

Expand Down
13 changes: 12 additions & 1 deletion src/MoqxServerFactory.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#pragma once

#include <memory>
#include <string>

#include "MoqxPicoRelayServer.h"
#include "MoqxRelayContext.h"
Expand All @@ -15,15 +16,19 @@
#include "stats/StatsRegistry.h"
#include <folly/executors/IOThreadPoolExecutor.h>
#include <moxygen/MoQServerBase.h>
#include <moxygen/mlog/MLoggerFactory.h>

namespace openmoq::moqx {

// Creates the appropriate relay server for the given listener config and wires
// stats. Both stack paths accept the same ioExecutor for a uniform call site.
// Optionally wires an mlog factory for per-session logging.
inline std::shared_ptr<moxygen::MoQServerBase> makeRelayServer(
const config::ListenerConfig& listenerCfg,
std::shared_ptr<MoqxRelayContext> context,
std::shared_ptr<folly::IOThreadPoolExecutor> ioExecutor,
std::shared_ptr<stats::StatsRegistry> statsRegistry
std::shared_ptr<stats::StatsRegistry> statsRegistry,
std::shared_ptr<moxygen::MLoggerFactory> mlogFactory = nullptr
) {
if (listenerCfg.quicStack == config::QuicStack::Picoquic) {
auto server = std::make_shared<MoqxPicoRelayServer>(
Expand All @@ -32,11 +37,17 @@ inline std::shared_ptr<moxygen::MoQServerBase> makeRelayServer(
std::move(ioExecutor)
);
server->setStatsRegistry(std::move(statsRegistry));
if (mlogFactory) {
server->setMLoggerFactory(std::move(mlogFactory));
}
return server;
}
auto server =
std::make_shared<MoqxRelayServer>(listenerCfg, std::move(context), std::move(ioExecutor));
server->setStatsRegistry(std::move(statsRegistry));
if (mlogFactory) {
server->setMLoggerFactory(std::move(mlogFactory));
}
return server;
}

Expand Down
13 changes: 13 additions & 0 deletions src/config/Config.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,25 @@ struct AdminConfig {
std::optional<TlsConfig> tls;
};

struct MLogConfig {
std::string dir; // output directory; empty = disabled
float sampleRate{1.0f}; // 1.0 = log all sessions, 0.0 = none
std::optional<uint32_t> maxAgeDays; // delete files older than N days; nullopt = no limit
std::optional<uint64_t> maxDirMb; // trim directory to this size in MB; nullopt = no limit
uint32_t cleanupIntervalSecs{600}; // how often to run cleanup (default 10 min)
};

struct LoggingConfig {
std::optional<MLogConfig> mlog;
};

struct Config {
std::vector<ListenerConfig> listeners;
folly::F14FastMap<std::string, ServiceConfig> services;
std::optional<AdminConfig> admin;
std::string relayID; // always set: from config or randomly generated
uint32_t threads{1};
std::optional<LoggingConfig> logging;
};

} // namespace openmoq::moqx::config
52 changes: 52 additions & 0 deletions src/config/ConfigResolver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "config/loader/ConfigResolver.h"

#include <filesystem>
#include <iomanip>
#include <random>
#include <sstream>
Expand Down Expand Up @@ -685,6 +686,28 @@ folly::Expected<ResolvedConfig, std::string> resolveConfig(const ParsedConfig& c
errors.push_back("threads > 1 is not yet supported");
}

// === Validate logging ===
if (config.logging.value().has_value()) {
const auto& logging = *config.logging.value();
if (logging.mlog.value().has_value()) {
const auto& mlog = *logging.mlog.value();
if (mlog.sample_rate.value().has_value()) {
float rate = *mlog.sample_rate.value();
if (rate < 0.0f || rate > 1.0f) {
errors.push_back(
"logging.mlog.sample_rate must be in [0.0, 1.0], got " + std::to_string(rate)
);
}
}
if (mlog.max_age_days.value().has_value() && *mlog.max_age_days.value() == 0) {
errors.push_back("logging.mlog.max_age_days must be >= 1 if set");
}
if (mlog.max_dir_mb.value().has_value() && *mlog.max_dir_mb.value() == 0) {
errors.push_back("logging.mlog.max_dir_mb must be >= 1 if set");
}
}
}

if (!errors.empty()) {
return folly::makeUnexpected("Config validation failed:\n - " + folly::join("\n - ", errors));
}
Expand Down Expand Up @@ -715,6 +738,34 @@ folly::Expected<ResolvedConfig, std::string> resolveConfig(const ParsedConfig& c
// Resolve relayID: use configured value or generate a random hex string
std::string relayID = config.relay_id.value().value_or(generateRelayID());

// Resolve logging config
std::optional<LoggingConfig> loggingConfig;
if (config.logging.value().has_value()) {
const auto& parsedLogging = *config.logging.value();
LoggingConfig resolved;
if (parsedLogging.mlog.value().has_value()) {
const auto& parsedMlog = *parsedLogging.mlog.value();
MLogConfig mlogConfig;
mlogConfig.dir = parsedMlog.dir.value();
mlogConfig.sampleRate = parsedMlog.sample_rate.value().value_or(1.0f);
mlogConfig.maxAgeDays = parsedMlog.max_age_days.value();
mlogConfig.maxDirMb = parsedMlog.max_dir_mb.value();
mlogConfig.cleanupIntervalSecs =
parsedMlog.cleanup_interval_secs.value().value_or(600u);
if (!mlogConfig.dir.empty()) {
std::error_code ec;
std::filesystem::create_directories(mlogConfig.dir, ec);
if (ec) {
return folly::makeUnexpected(
"Failed to create mlog directory '" + mlogConfig.dir + "': " + ec.message()
);
}
}
resolved.mlog = std::move(mlogConfig);
}
loggingConfig = std::move(resolved);
}

return ResolvedConfig{
.config =
Config{
Expand All @@ -732,6 +783,7 @@ folly::Expected<ResolvedConfig, std::string> resolveConfig(const ParsedConfig& c
.admin = std::move(adminConfig),
.relayID = std::move(relayID),
.threads = threads,
.logging = std::move(loggingConfig),
},
.warnings = std::move(warnings),
};
Expand Down
23 changes: 23 additions & 0 deletions src/config/loader/ParsedConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,28 @@ struct ParsedServiceDefaultsConfig {
rfl::Description<"Default cache settings for services", std::optional<ParsedCacheConfig>> cache;
};

struct ParsedMLogConfig {
rfl::Description<"Directory for per-session MoQ log files (empty = disabled)", std::string> dir;
rfl::Description<"Fraction of sessions to log (0.0-1.0, default 1.0)", std::optional<float>>
sample_rate;
rfl::Description<
"Delete log files older than this many days (omit = no age limit)",
std::optional<uint32_t>>
max_age_days;
rfl::Description<
"Trim mlog directory to this size in MB by deleting oldest files first (omit = no size limit)",
std::optional<uint64_t>>
max_dir_mb;
rfl::Description<
"How often to run mlog cleanup, in seconds (default 600 = 10 minutes)",
std::optional<uint32_t>>
cleanup_interval_secs;
};

struct ParsedLoggingConfig {
rfl::Description<"MoQ-level (mlog) per-session logging", std::optional<ParsedMLogConfig>> mlog;
};

struct ParsedConfig {
rfl::Description<
"Listener definitions (currently exactly one supported)",
Expand All @@ -235,6 +257,7 @@ struct ParsedConfig {
std::optional<ParsedListenerDefaultsConfig>>
listener_defaults;
rfl::Description<"Number of IO worker threads (default: 1)", std::optional<uint32_t>> threads;
rfl::Description<"Logging configuration", std::optional<ParsedLoggingConfig>> logging;
};

} // namespace openmoq::moqx::config
Loading