Skip to content

Commit

Permalink
Implemented C4LogObserver [API]
Browse files Browse the repository at this point in the history
Old API is still present but "semi-deprecated".
  • Loading branch information
snej committed Dec 16, 2024
1 parent 14d8096 commit cf107a9
Show file tree
Hide file tree
Showing 17 changed files with 635 additions and 486 deletions.
5 changes: 5 additions & 0 deletions C/c4.exp
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,11 @@ _c4index_getQueryLanguage
_c4index_getExpression
_c4index_getOptions

_c4log_defaultCallback
_c4log_newObserver
_c4log_removeObserver
_c4log_replaceObserver

_FLDoc_FromJSON
_FLDoc_Retain
_FLDoc_GetAllocedData
Expand Down
209 changes: 185 additions & 24 deletions C/c4Log.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,49 +30,120 @@ CBL_CORE_API const C4LogDomain kC4WebSocketLog = (C4LogDomain)&websocket::WSLogD

// NOLINTEND(cppcoreguidelines-interfaces-global-init)

#pragma mark - CALLBACK LOGGING:

static Retained<LogCallback> sDefaultLogCallback;
static C4LogCallback sDefaultLogCallbackFn;
static C4LogLevel sDefaultLogCallbackLevel = kC4LogNone;

// LCOV_EXCL_START
void c4log_writeToCallback(C4LogLevel level, C4LogCallback callback, bool preformatted) noexcept {
LogCallback::setCallback((LogCallback::Callback_t)callback, preformatted);
LogCallback::setCallbackLogLevel((LogLevel)level);
if ( !callback ) level = kC4LogNone;
if ( sDefaultLogCallback ) {
LogObserver::remove(sDefaultLogCallback);
sDefaultLogCallback = nullptr;
}
if ( level != kC4LogNone ) {
if ( preformatted ) {
LogCallback::Callback_t thunk = [](void* ctx, LogEntry const& e) {
va_list noArgs{};
((C4LogCallback)ctx)(C4LogDomain(&e.domain), C4LogLevel(e.level), e.message.data(), noArgs);
};
sDefaultLogCallback = make_retained<LogCallback>(thunk, (void*)callback);
} else {
LogCallback::RawCallback_t thunk = [](void* ctx, const LogDomain& domain, LogLevel level,
const char* format, va_list args) {
((C4LogCallback)ctx)(C4LogDomain(&domain), C4LogLevel(level), format, args);
};
sDefaultLogCallback = make_retained<LogCallback>(thunk, (void*)callback);
}
LogObserver::add(sDefaultLogCallback, LogLevel(level));
}
sDefaultLogCallbackLevel = level;
}

C4LogCallback c4log_getCallback() noexcept { return (C4LogCallback)LogCallback::currentCallback(); }
C4LogCallback c4log_getCallback() noexcept { return sDefaultLogCallbackFn; }

void c4log_defaultCallback(C4LogDomain domain, C4LogLevel level, const char* fmt, va_list args) {
LogCallback::defaultCallback(*(const LogDomain*)domain, LogLevel(level), fmt, args);
char* cstr = nullptr;
if ( vasprintf(&cstr, fmt, args) < 0 ) return;
LogCallback::consoleCallback(nullptr, LogEntry{.timestamp = uint64_t(c4_now()),
.domain = *(LogDomain*)domain,
.level = LogLevel(level),
.message = cstr});
free(cstr);
}

void c4log_setCallbackLevel(C4LogLevel level) noexcept {
if ( level != sDefaultLogCallbackLevel && sDefaultLogCallback ) {
LogObserver::remove(sDefaultLogCallback);
LogObserver::add(sDefaultLogCallback, LogLevel(level));
}
sDefaultLogCallbackLevel = level;
}

C4LogLevel c4log_callbackLevel() noexcept { return sDefaultLogCallbackLevel; } // LCOV_EXCL_LINE

// LCOV_EXCL_STOP

#pragma mark - FILE LOGGING:

static Retained<LogFiles> sDefaultLogFiles;
static C4LogLevel sDefaultLogFilesLevel = kC4LogNone;

bool c4log_writeToBinaryFile(C4LogFileOptions options, C4Error* outError) noexcept {
return tryCatch(outError, [=] {
LogFiles::Options lfOptions{slice(options.base_path).asString(), (LogLevel)options.log_level,
options.max_size_bytes, options.max_rotate_count, options.use_plaintext};

string buildInfo(c4_getBuildInfo());
const string header = options.header.buf != nullptr ? slice(options.header).asString()
: string("Generated by LiteCore ") + buildInfo;
LogFiles::writeEncodedLogsTo(lfOptions, header);
if ( options.base_path.empty() || options.log_level == kC4LogNone ) {
// Disabling file logging:
if ( sDefaultLogFiles ) {
LogObserver::remove(sDefaultLogFiles);
sDefaultLogFiles = nullptr;
}
sDefaultLogFilesLevel = kC4LogNone;
} else {
LogFiles::Options lfOptions{.directory = slice(options.base_path).asString(),
.maxSize = options.max_size_bytes,
.maxCount = options.max_rotate_count,
.isPlaintext = options.use_plaintext};
if ( options.header ) lfOptions.initialMessage = slice(options.header).asString();
else
lfOptions.initialMessage = "Generated by LiteCore "s + string(c4_getBuildInfo());
if ( sDefaultLogFiles ) {
LogObserver::remove(sDefaultLogFiles);
sDefaultLogFiles->setOptions(lfOptions);
} else {
sDefaultLogFiles = make_retained<LogFiles>(lfOptions);
}
LogObserver::add(sDefaultLogFiles, LogLevel(options.log_level));
sDefaultLogFilesLevel = options.log_level;
}
});
}

C4LogLevel c4log_callbackLevel() noexcept { return (C4LogLevel)LogCallback::callbackLogLevel(); } // LCOV_EXCL_LINE

C4LogLevel c4log_binaryFileLevel() noexcept { return (C4LogLevel)LogFiles::logLevel(); }
C4LogLevel c4log_binaryFileLevel() noexcept { return sDefaultLogFilesLevel; }

void c4log_setCallbackLevel(C4LogLevel level) noexcept {
LogCallback::setCallbackLogLevel((LogLevel)level);
} //LCOV_EXCL_LINE
void c4log_setBinaryFileLevel(C4LogLevel level) noexcept {
if ( sDefaultLogFiles && level != sDefaultLogFilesLevel ) {
LogObserver::remove(sDefaultLogFiles);
LogObserver::add(sDefaultLogFiles.get(), LogLevel(level));
sDefaultLogFilesLevel = level;
}
}

void c4log_setBinaryFileLevel(C4LogLevel level) noexcept { LogFiles::setLogLevel((LogLevel)level); }
C4StringResult c4log_binaryFilePath() C4API {
if ( sDefaultLogFiles ) {
auto options = sDefaultLogFiles->options();
if ( !options.isPlaintext ) return C4StringResult(alloc_slice(options.directory));
}
return {};
}

C4StringResult c4log_binaryFilePath(void) C4API {
auto options = LogFiles::currentOptions();
if ( !options.path.empty() && !options.isPlaintext ) return C4StringResult(alloc_slice(options.path));
else
return {};
void c4log_flushLogFiles() C4API {
if ( sDefaultLogFiles ) sDefaultLogFiles->flush();
}

#pragma mark - LOG DOMAINS AND LEVELS:

C4LogDomain c4log_getDomain(const char* name, bool create) noexcept {
if ( !name ) return kC4DefaultLog;
auto domain = LogDomain::named(name);
Expand Down Expand Up @@ -115,7 +186,7 @@ void c4log_enableFatalExceptionBacktrace() C4API {
});
}

void c4log_flushLogFiles() C4API { LogFiles::flush(); }
#pragma mark - WRITING LOG MESSAGES:

void c4log(C4LogDomain c4Domain, C4LogLevel level, const char* fmt, ...) noexcept {
va_list args;
Expand All @@ -140,3 +211,93 @@ void c4slog(C4LogDomain c4Domain, C4LogLevel level, C4Slice msg) noexcept {
}

// LCOV_EXCL_STOP

#pragma mark - LOG OBSERVER:

// There is no real `C4LogObserver` struct. `C4LogObserver*` is an alias for C++ `LogObserver*`.

static inline LogObserver* toInternal(C4LogObserver* obs) { return reinterpret_cast<LogObserver*>(obs); }

static inline C4LogObserver* toExternal(Retained<LogObserver> obs) {
return reinterpret_cast<C4LogObserver*>(std::move(obs).detach());
}

static vector<pair<LogDomain&, LogLevel>> convertDomains(C4LogObserverConfig const& config) {
vector<pair<LogDomain&, LogLevel>> domains;
domains.reserve(config.domainsCount);
for ( size_t i = 0; i < config.domainsCount; ++i ) {
auto [domain, level] = config.domains[i];
if ( domain == nullptr || level < kC4LogDebug || level > kC4LogError )
error::_throw(error::InvalidParameter, "invalid log domain or level");
for ( size_t j = 0; j < i; j++ ) {
if ( config.domains[j].domain == domain ) error::_throw(error::InvalidParameter, "duplicate log domain");
}
domains.emplace_back(*(LogDomain*)domain, LogLevel(level));
}
return domains;
}

static LogFiles::Options convertFileOptions(C4LogObserverConfig const& config) {
auto& fopts = *config.fileOptions;
return {.directory = slice(fopts.base_path).asString(),
.initialMessage = slice(fopts.header).asString(),
.maxSize = fopts.max_size_bytes,
.maxCount = fopts.max_rotate_count,
.isPlaintext = fopts.use_plaintext};
}

C4LogObserver* c4log_newObserver(C4LogObserverConfig config, C4Error* outError) noexcept {
try {
if ( (config.callback != nullptr) + (config.fileOptions != nullptr) != 1 ) {
c4error_return(LiteCoreDomain, kC4ErrorInvalidParameter,
"log observer needs either a callback or a file but not both"_sl, outError);
}
auto domains = convertDomains(config);
Retained<LogObserver> obs;
if ( config.callback ) {
// Create LogCallback observer:
auto thunk = [cb = config.callback, ctx = config.callbackContext](LogEntry const& e) {
C4LogEntry c4entry{.timestamp = C4Timestamp(e.timestamp),
.level = C4LogLevel(e.level),
.domain = (C4LogDomain)&e.domain,
.message = slice(e.message)};
cb(&c4entry, ctx);
};
obs = make_retained<LogFunction>(thunk);
} else {
// Create LogFiles observer:
obs = make_retained<LogFiles>(convertFileOptions(config));
}
LogObserver::add(obs, LogLevel(config.defaultLevel), domains);
return toExternal(std::move(obs));
}
catchError(outError);
return nullptr;
}

void c4log_removeObserver(C4LogObserver* observer) noexcept {
LogObserver::remove(reinterpret_cast<LogObserver*>(observer));
}

C4LogObserver* c4log_replaceObserver(C4LogObserver* oldObs, C4LogObserverConfig config, C4Error* outError) noexcept {
Retained<LogFiles> fileObs = dynamic_cast<LogFiles*>(toInternal(oldObs));
if ( fileObs && config.fileOptions ) {
// If the old and new config both log to files, keep the same LogFiles and tell it to
// change its file settings. That allows it to keep log files open as long as the directory
// doesn't change.
try {
auto domains = convertDomains(config);
fileObs->setOptions(convertFileOptions(config));
LogObserver::remove(fileObs);
LogObserver::add(fileObs, LogLevel(config.defaultLevel), domains);
return toExternal(std::move(fileObs));
}
catchError(outError);
return nullptr;
} else {
// By default just create a new observer and remove the old one:
C4LogObserver* newObs = c4log_newObserver(config, outError);
if ( newObs && oldObs ) c4log_removeObserver(oldObs);
return newObs;
}
}
5 changes: 5 additions & 0 deletions C/c4_ee.exp
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,11 @@ _c4index_getQueryLanguage
_c4index_getExpression
_c4index_getOptions

_c4log_defaultCallback
_c4log_newObserver
_c4log_removeObserver
_c4log_replaceObserver

_FLDoc_FromJSON
_FLDoc_Retain
_FLDoc_GetAllocedData
Expand Down
10 changes: 10 additions & 0 deletions C/include/c4Base.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ typedef struct C4KeyPair C4KeyPair;
/** A LiteCore network listener -- supports the REST API, replication, or both. */
typedef struct C4Listener C4Listener;

/** Opaque handle to a registered logging observer. */
typedef struct C4LogObserver C4LogObserver;

/** Opaque handle to a compiled query. */
typedef struct C4Query C4Query;

Expand Down Expand Up @@ -207,6 +210,10 @@ inline C4IndexUpdater* C4NULLABLE c4indexupdater_retain(C4IndexUpdater* C4NULLAB

inline C4KeyPair* C4NULLABLE c4keypair_retain(C4KeyPair* C4NULLABLE r) C4API { return (C4KeyPair*)c4base_retain(r); }

inline C4LogObserver* C4NULLABLE c4logobserver_retain(C4LogObserver* C4NULLABLE r) C4API {
return (C4LogObserver*)c4base_retain(r);
}

inline C4Query* C4NULLABLE c4query_retain(C4Query* C4NULLABLE r) C4API { return (C4Query*)c4base_retain(r); }

CBL_CORE_API C4Document* C4NULLABLE c4doc_retain(C4Document* C4NULLABLE) C4API;
Expand All @@ -227,6 +234,9 @@ inline void c4indexupdater_release(C4IndexUpdater* C4NULLABLE u) C4API { c4base_

inline void c4keypair_release(C4KeyPair* C4NULLABLE r) C4API { c4base_release(r); }

/** \note This function is thread-safe. */
inline void c4logobserver_release(C4LogObserver* C4NULLABLE r) C4API { c4base_release(r); }

/** \note This function is thread-safe. */
inline void c4query_release(C4Query* C4NULLABLE r) C4API { c4base_release(r); }

Expand Down
67 changes: 59 additions & 8 deletions C/include/c4Log.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@
#include "c4Base.h"
#include "c4Error.h"
#include "fleece/FLSlice.h"
#ifdef __cplusplus
# include <cstdarg>
#else
# include <stdarg.h>
#endif
#include <stdarg.h>

C4_ASSUME_NONNULL_BEGIN
C4API_BEGIN_DECLS
Expand Down Expand Up @@ -104,8 +100,10 @@ typedef void (*C4NULLABLE C4LogCallback)(C4LogDomain, C4LogLevel, const char* fm
will be NULL. */
CBL_CORE_API void c4log_writeToCallback(C4LogLevel level, C4LogCallback callback, bool preformatted) C4API;

/** A log callback that writes log messages to stderr, or on Android to `__android_log_write`. */
CBL_CORE_API void c4log_defaultCallback(C4LogDomain, C4LogLevel, const char* fmt, va_list args) __printflike(3, 0);
/** A log callback that writes log messages to stderr, or on Android to `__android_log_write`.
This callback is preformatted: it expects `message` to be the complete string, and ignores `args`.
If passing it to \ref c4log_writeToCallback, you MUST pass `true` for `preformatted`. */
CBL_CORE_API void c4log_defaultCallback(C4LogDomain, C4LogLevel, const char* message, va_list) __printflike(3, 0);

/** Returns the current logging callback, or the default one if none has been set. */
CBL_CORE_API C4LogCallback c4log_getCallback(void) C4API;
Expand All @@ -118,6 +116,59 @@ CBL_CORE_API C4LogLevel c4log_callbackLevel(void) C4API;
CBL_CORE_API void c4log_setCallbackLevel(C4LogLevel level) C4API;


#pragma mark - LOG OBSERVERS:

/** A log entry, as passed to a C4LogObserverCallback. */
typedef struct C4LogEntry {
C4Timestamp timestamp;
C4LogLevel level;
C4LogDomain domain;
FLString message;
} C4LogEntry;

/** A (domain, level) pair, used to customize a log observer's configuration. */
typedef struct C4DomainLevel {
C4LogDomain domain;
C4LogLevel level;
} C4DomainLevel;

/** The callback that will be called by a C4LogObserver.
Will be called on arbitrary threads. Should return as quickly as possible. */
typedef void (*C4LogObserverCallback)(const C4LogEntry*, void* C4NULLABLE context);

/** Configuration for creating a C4LogObserver,
which may either call a callback or write to a file (but not both.)
Exactly one of `callback` and `fileOptions` must be non-NULL. */
typedef struct C4LogObserverConfig {
C4LogLevel defaultLevel; ///< Log level for domains not listed
const C4DomainLevel* C4NULLABLE domains; ///< List of domains and levels (may be NULL if empty)
size_t domainsCount; ///< Length of `domains` array
C4LogObserverCallback C4NULLABLE callback; ///< C callback to invoke
void* C4NULLABLE callbackContext; ///< `context` value to pass the callback
const C4LogFileOptions* C4NULLABLE fileOptions; ///< Config for file logging (Note: `log_level` is ignored)
} C4LogObserverConfig;

/** Creates and registers a log observer, returning a reference.
Fails if the configuration is invalid.
@note Call \ref c4logobserver_release when done with the reference.
(You don't need to keep it unless you're going to call \ref c4log_removeObserver later.) */
NODISCARD CBL_CORE_API C4LogObserver* c4log_newObserver(C4LogObserverConfig config, C4Error* C4NULLABLE outError) C4API;

/** Unregisters a log observer. Does nothing if it's not registered.
@note This does not release your reference. You should call \ref c4logobserver_release afterward
if you don't need the object anymore. */
CBL_CORE_API void c4log_removeObserver(C4LogObserver*) C4API;

/** Atomically unregisters an observer and registers a new one.
If oldObs is NULL, nothing is unregistered.
In case of failure (invalid config) oldObs is left intact.
@note This does not release `oldObs`. You should call \ref c4logobserver_release afterward
if you don't need the object anymore. */
NODISCARD CBL_CORE_API C4LogObserver* c4log_replaceObserver(C4LogObserver* C4NULLABLE oldObs,
C4LogObserverConfig config,
C4Error* C4NULLABLE outError) C4API;


#pragma mark - LOG DOMAINS:


Expand Down Expand Up @@ -181,7 +232,7 @@ CBL_CORE_API void c4log(C4LogDomain domain, C4LogLevel level, const char* fmt, .
/** Same as c4log, for use in calling functions that already take variable args. */
CBL_CORE_API void c4vlog(C4LogDomain domain, C4LogLevel level, const char* fmt, va_list args) C4API __printflike(3, 0);

/** Same as c4log, except it accepts preformatted messages as FLSlices */
/** Writes a preformatted message to log files, but does not invoke log callbacks. */
CBL_CORE_API void c4slog(C4LogDomain domain, C4LogLevel level, FLString msg) C4API;

// Convenient aliases for c4log:
Expand Down
Loading

0 comments on commit cf107a9

Please sign in to comment.