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
49 changes: 46 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,54 @@ with the same library loaded elsewhere in the host process.

Code should be easy to read and understand. If a sacrifice is made for performance or readability, it should be documented.

Adhere to clang-format checks configured in `.clang-format`. Run `./scripts/clang-format.sh` if needed to confirm code styling, and `./scripts/clang-format.sh --fix` to auto-format generated code created.
#### Formatting (clang-format)

### Static Analysis
The repo enforces `.clang-format` in CI (see `.github/workflows/builds.yml` →
`clang-format` job). Any divergence fails the build.

Adhere to clang-tidy checks configured in `.clang-tidy`. Run `./scripts/clang-tidy.sh` if needed to confirm code quality.
**Required workflow when generating or editing C/C++ code:**

1. After completing a set of edits to any `.c`, `.cc`, `.cpp`, `.cxx`, `.h`,
`.hpp`, or `.hxx` file under `src/`, `include/`, or `benchmarks/`, run:

```bash
./scripts/clang-format.sh --fix <paths-you-edited>
```

Pass the explicit paths so the run is fast; omit them only when doing a
sweep over the entire tree.
2. If clang-format reports any rewrites, treat the rewritten contents as the
final state and re-read the file before doing any further edits to it.
3. Do not commit, hand off, or declare a task complete with un-formatted
code. CI will reject it and the round-trip wastes time.

`./scripts/clang-format.sh` (no `--fix`) runs in dry-run / `--Werror` mode and
is what CI uses; it exits non-zero on any divergence.

#### Static Analysis (clang-tidy)

The repo enforces `.clang-tidy` in CI (see `.github/workflows/builds.yml` →
`clang-tidy` job, run with `--fail-on-warning`). Any new warning fails the
build.

**Required workflow when generating or editing C/C++ code:**

1. After substantive C/C++ edits — especially anything that adds new
identifiers, new enums, new public API, new free functions, or that
touches the FFI / threading boundary — run:

```bash
./scripts/clang-tidy.sh
```

This requires a prior `./build.sh release` (or
`./build.sh release-tests` / `release-all`) so that
`build-release/compile_commands.json` and the generated protobuf headers
exist.
2. Address any new findings introduced by your edits. Do not introduce new
`// NOLINT` suppressions without a one-line comment explaining *why*.
3. Pre-existing findings in code you did not touch are out of scope unless
the user explicitly asks for a sweep.

## Dependencies

Expand Down
2 changes: 1 addition & 1 deletion client-sdk-rust
16 changes: 14 additions & 2 deletions include/livekit/livekit.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
namespace livekit {

/// The log sink to use for SDK messages.
enum class LogSink {
/// @deprecated Use livekit::setLogCallback instead to register a custom log callback
enum class [[deprecated("Use livekit::setLogCallback instead to register a custom log callback")]] LogSink {
/// Log messages to the console.
kConsole = 0,
/// Log messages to a callback function.
Expand All @@ -60,7 +61,18 @@ enum class LogSink {
/// @param log_sink The log sink to use for SDK messages (default: Console).
/// @returns true if initialization happened on this call, false if it was
/// already initialized.
LIVEKIT_API bool initialize(const LogLevel& level = LogLevel::Info, const LogSink& log_sink = LogSink::kConsole);
/// @deprecated Use livekit::setLogCallback instead to register a custom log callback
[[deprecated("Use livekit::setLogCallback instead to register a custom log callback")]] LIVEKIT_API bool initialize(
const LogLevel& level, const LogSink& log_sink);

/// Initialize the LiveKit SDK.
///
/// This **must be the first LiveKit API called** in the process.
/// It configures global SDK state.
///
/// @param level Minimum log level for SDK messages (default: Info).
/// Use setLogLevel() to change at runtime.
LIVEKIT_API bool initialize(const LogLevel& level = LogLevel::Info);

/// Shut down the LiveKit SDK.
///
Expand Down
38 changes: 37 additions & 1 deletion src/ffi_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "ffi_client.h"

#include <cassert>
#include <csignal>

#include "data_track.pb.h"
#include "e2ee.pb.h"
Expand Down Expand Up @@ -219,6 +220,23 @@ proto::FfiResponse FfiClient::sendRequest(const proto::FfiRequest& request) cons
return response;
}

LogLevel toSpdlogLevel(proto::LogLevel level) {
switch (level) {
case proto::LOG_ERROR:
return LogLevel::Error;
case proto::LOG_WARN:
return LogLevel::Warn;
case proto::LOG_INFO:
return LogLevel::Info;
case proto::LOG_DEBUG:
return LogLevel::Debug;
case proto::LOG_TRACE:
return LogLevel::Trace;
default:
return LogLevel::Info;
}
}

void FfiClient::PushEvent(const proto::FfiEvent& event) const {
std::unique_ptr<PendingBase> to_complete;
std::vector<Listener> listeners_copy;
Expand Down Expand Up @@ -251,11 +269,29 @@ void FfiClient::PushEvent(const proto::FfiEvent& event) const {
}
}

void LivekitFfiCallback(const uint8_t* buf, size_t len) {
extern "C" LIVEKIT_INTERNAL_API void LivekitFfiCallback(const uint8_t* buf, size_t len) {
proto::FfiEvent event;
event.ParseFromArray(buf,
static_cast<int>(len)); // TODO: this fixes for now, what if len exceeds int?

// Forward any FFI logs to the SDK logger
if (event.has_logs()) {
// Note: explicitly acquiring the logger here to avoid mutex acquires per-message
auto logger = detail::getLogger();
for (const auto& rec : event.logs().records()) {
detail::forwardFfiLog(logger, toSpdlogLevel(rec.level()), rec.target(), rec.message());
}
return; // No need to queue the log event from here
}

// We are in a unrecoverable state, terminate the process
// This is what Python does, may not make sense for C++
if (event.has_panic()) {
std::cerr << "FFI Panic: " << event.panic().message() << '\n';
std::raise(SIGTERM);
return;
}

FfiClient::instance().PushEvent(event);
}

Expand Down
2 changes: 1 addition & 1 deletion src/ffi_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ extern "C" void livekit_ffi_initialize(FfiCallbackFn cb, bool capture_logs, cons

extern "C" void livekit_ffi_dispose();

extern "C" void LivekitFfiCallback(const uint8_t* buf, size_t len);
extern "C" LIVEKIT_INTERNAL_API void LivekitFfiCallback(const uint8_t* buf, size_t len);

// The FfiClient is used to communicate with the FFI interface of the Rust SDK
// We use the generated protocol messages to facilitate the communication.
Expand Down
9 changes: 6 additions & 3 deletions src/livekit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@
namespace livekit {

bool initialize(const LogLevel& level, const LogSink& log_sink) {
(void)log_sink;
return initialize(level);
}

bool initialize(const LogLevel& level) {
// Initializes logger if singleton instance is not already initialized
setLogLevel(level);
auto& ffi_client = FfiClient::instance();
return ffi_client.initialize(log_sink == LogSink::kCallback);
Copy link
Copy Markdown
Collaborator Author

@alan-george-lk alan-george-lk May 19, 2026

Choose a reason for hiding this comment

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

The log_sink variable was not working as I suspect it was intended previously. The argument to FfiClient::initialize here was to enable FFI logging (forwards all Rust log macros over the FFI callback as events), but LogSink::kCallback as a sink type is basically irrelevant to that feature. And passing that value here didn't actually register any callbacks. We should figure out what to do here, options are:

  • Breaking change: simply remove the second arg to livekit::initialize()
  • Just leave this .initialize(true) as-is
  • Add a third enum value to LogSink called kOff, and then this arg becomes .initialize(log_sink != kOff), so users have the ability to turn off the FFI logs. But then that brings up the question, we probably should have a cleaner log control. Just turning off FFI logs while others remain doesn't really make sense

return ffi_client.initialize(true);
}

bool isInitialized() { return FfiClient::instance().isInitialized(); }
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This accidentally made it in from an older PR (no header signature equivalent)


void shutdown() {
FfiClient::instance().shutdown();
detail::shutdownLogger();
Expand Down
7 changes: 7 additions & 0 deletions src/lk_log.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
#include <spdlog/spdlog.h>

#include <memory>
#include <string>

#include "livekit/logging.h"
#include "livekit/visibility.h"

namespace livekit::detail {
Expand All @@ -32,6 +34,11 @@ LIVEKIT_INTERNAL_API std::shared_ptr<spdlog::logger> getLogger();
/// Tears down the spdlog logger. Called by livekit::shutdown().
LIVEKIT_INTERNAL_API void shutdownLogger();

/// Forward a single record received from the Rust FFI log bridge into the
/// supplied logger's spdlog sinks
LIVEKIT_INTERNAL_API void forwardFfiLog(const std::shared_ptr<spdlog::logger>& logger, LogLevel level,
const std::string& target, const std::string& message);

} // namespace livekit::detail

// Convenience macros — two-tier filtering:
Expand Down
19 changes: 19 additions & 0 deletions src/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,25 @@ void shutdownLogger() {
}
}

void forwardFfiLog(const std::shared_ptr<spdlog::logger>& logger, LogLevel level, const std::string& target,
const std::string& message) {
const auto spd_level = toSpdlogLevel(level);
if (!logger->should_log(spd_level)) {
return;
}

// Construct a log_msg directly so the Rust target survives as
// `logger_name`, instead of being collapsed into the single "livekit"
// logger name that spdlog would otherwise apply.
const spdlog::details::log_msg msg(spdlog::source_loc{}, spdlog::string_view_t{target.data(), target.size()},
spd_level, spdlog::string_view_t{message.data(), message.size()});
for (auto& sink : logger->sinks()) {
if (sink->should_log(spd_level)) {
sink->log(msg);
}
}
}

} // namespace detail

void setLogLevel(LogLevel level) { detail::getLogger()->set_level(toSpdlogLevel(level)); }
Expand Down
Loading
Loading