Skip to content

Commit 8a01b77

Browse files
committed
Add PrefixLogger which appends some predefined context to logs
Refactor more of BucketLogger into the core. PrefixLogger instances can be used to create a logger scoped to a component, like we already do with EP-Engine and DCP ConnHandler logging. Note PrefixLogger, like other loggers, needs to be registered if the logger needs to support dynamic change of verbosity. In the future, we could have a (one) PrefixLogger instance in the core which reads conn_id and other thread_local state and automatically prefixes logs with that. Now BucketLogger is left only with the functionality of prepending the engine and conn_id. Change-Id: I4531991e047b41bfda54fcaec2e432fb900f1613 Reviewed-on: https://review.couchbase.org/c/kv_engine/+/226362 Tested-by: Build Bot <build@couchbase.com> Reviewed-by: Trond Norbye <trond.norbye@couchbase.com>
1 parent 9df74e5 commit 8a01b77

File tree

10 files changed

+182
-52
lines changed

10 files changed

+182
-52
lines changed

engines/ep/src/bucket_logger.cc

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
// Construct the base logger with a nullptr for the sinks as they will never be
2121
// used. Requires a unique name for registry
2222
BucketLogger::BucketLogger(const std::string& name)
23-
: Logger(name, cb::logger::get()) {
23+
: PrefixLogger(name, cb::logger::get()) {
2424
}
2525

2626
void BucketLogger::logWithContext(spdlog::level::level_enum lvl,
@@ -40,40 +40,31 @@ void BucketLogger::logWithContext(spdlog::level::level_enum lvl,
4040
// handler - see MB-61032.
4141
NonBucketAllocationGuard guard;
4242
try {
43-
if (!ctx.is_object()) {
44-
#if CB_DEVELOPMENT_ASSERTS
45-
throw std::invalid_argument(fmt::format(
46-
"JSON context must be an object, not `{}`", ctx.dump()));
47-
#else
48-
// In production, handle this case gracefully.
49-
ctx = Json{{"context", std::move(ctx)}};
50-
#endif
51-
}
52-
53-
auto& object = ctx.get_ref<cb::logger::Json::object_t&>();
54-
object.reserve(object.size() + prefixContext.size() + bool(engine) +
55-
bool(connectionId));
43+
using namespace cb::logger;
44+
// Get the prefix early so we can reserve space in the final context.
45+
Json basePrefix = static_cast<BasicJsonType>(getContextPrefix());
46+
// Create a new context object into which we will merge all keys.
47+
// Final context is the conn_id + engine + static prefix + context.
48+
Json finalContext = Json::object();
49+
finalContext.get_ref<Json::object_t&>().reserve(
50+
ctx.size() + basePrefix.size() + bool(engine) +
51+
bool(connectionId));
5652

57-
if (!prefixContext.empty()) {
58-
const auto& prefixObject =
59-
prefixContext.get_ref<const nlohmann::json::object_t&>();
60-
for (auto it = prefixObject.crbegin(); it != prefixObject.crend();
61-
++it) {
62-
object.insert(object.begin(),
63-
cb::logger::Json{it->first, it->second});
64-
}
53+
// Write the ID.
54+
if (connectionId != 0) {
55+
finalContext["conn_id"] = connectionId;
6556
}
6657

6758
// Write the bucket
6859
if (engine) {
69-
object.insert(object.begin(), {"bucket", engine->getName()});
70-
}
71-
// Write the ID.
72-
if (connectionId != 0) {
73-
object.insert(object.begin(), {"conn_id", connectionId});
60+
finalContext["bucket"] = engine->getName();
7461
}
7562

76-
Logger::logWithContext(lvl, msg, std::move(ctx));
63+
mergeContext(finalContext, std::move(basePrefix));
64+
mergeContext(finalContext, std::move(ctx));
65+
// Call the Logger, not the PrefixLogger, since we don't want to add
66+
// the prefix again.
67+
Logger::logWithContext(lvl, msg, std::move(finalContext));
7768
} catch (const std::exception& e) {
7869
// Log a fixed message about this failing - we can't really be sure
7970
// what arguments failed above.

engines/ep/src/bucket_logger.h

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@
1111

1212
#pragma once
1313

14-
#include "logger/logger.h"
15-
#include "spdlog/logger.h"
14+
#include "logger/prefix_logger.h"
1615
#include <fmt/core.h>
1716
#include <platform/json_log.h>
1817
#include <spdlog/fmt/ostr.h>
@@ -80,7 +79,7 @@ const std::string globalBucketLoggerName = "globalBucketLogger";
8079
* is registered correctly. BucketLoggers must be registered to ensure that
8180
* their verbosity can be changed at runtime.
8281
*/
83-
class BucketLogger : public cb::logger::Logger {
82+
class BucketLogger : public cb::logger::PrefixLogger {
8483
public:
8584
/**
8685
* Record a log message for the bucket currently associated with the calling
@@ -111,12 +110,6 @@ class BucketLogger : public cb::logger::Logger {
111110
this->connectionId = id;
112111
}
113112

114-
/// The prefix printed before the log message contents
115-
std::string prefix;
116-
117-
/// Context to merge into logs.
118-
nlohmann::json prefixContext;
119-
120113
protected:
121114
/**
122115
* Connection ID prefix that is printed if set (printed before any other

engines/ep/src/connhandler.cc

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -380,12 +380,11 @@ void ConnHandler::pause(ConnHandler::PausedReason r) {
380380

381381
void ConnHandler::setLogContext(const std::string& header,
382382
const nlohmann::json& ctx) {
383-
logger->prefix = header;
384-
logger->prefixContext = ctx;
383+
logger->setPrefix(ctx, header);
385384
}
386385

387386
const char* ConnHandler::logHeader() const {
388-
return logger->prefix.c_str();
387+
return logger->getFmtPrefix().c_str();
389388
}
390389

391390
void ConnHandler::unPause() {

engines/ep/tests/module_tests/bucket_logger_test.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ TEST_F(BucketLoggerTest, InfoContextMacroWithCurlyBraces) {
239239
* characters which could be interpreted as fmtlib format strings.
240240
*/
241241
TEST_F(BucketLoggerTest, FmtlibPrefix) {
242-
getGlobalBucketLogger()->prefix = "abc:{def}";
242+
getGlobalBucketLogger()->setPrefix({}, "abc:{def}");
243243
EP_LOG_INFO("Test {}", "abc");
244244
}
245245

@@ -248,7 +248,7 @@ TEST_F(BucketLoggerTest, FmtlibPrefix) {
248248
* characters which is an invalid fmtlib format string.
249249
*/
250250
TEST_F(BucketLoggerTest, IllegalFmtlibPrefix) {
251-
getGlobalBucketLogger()->prefix = "abc:{def";
251+
getGlobalBucketLogger()->setPrefix({}, "abc:{def");
252252
EP_LOG_INFO("Test {}", "abc");
253253
}
254254

logger/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ add_library(memcached_logger STATIC
66
custom_rotating_file_sink.cc
77
custom_rotating_file_sink.h
88
logger_test_fixture.h
9-
logger_iface.h)
9+
logger_iface.h
10+
prefix_logger.h
11+
prefix_logger.cc)
1012
set_property(TARGET memcached_logger PROPERTY POSITION_INDEPENDENT_CODE 1)
1113
kv_enable_pch(memcached_logger)
1214
cb_enable_unity_build(memcached_logger)

logger/logger.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@ class Logger : protected spdlog::logger,
160160
/// Overriden flush_ method to flush via the inner spdlog::logger.
161161
void flush_() override;
162162

163+
/**
164+
* Fix up the context object to ensure it is a JSON object.
165+
*
166+
* @param ctx The context object to fix up.
167+
*/
168+
void fixupContext(Json& ctx) const;
169+
163170
private:
164171
/**
165172
* Pointer to the inner spdlog::logger.

logger/logger_test_fixture.cc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111

1212
#include "logger_test_fixture.h"
13+
#include "logger/prefix_logger.h"
1314

1415
#include <gsl/gsl-lite.hpp>
1516
#include <spdlog/spdlog.h>
@@ -101,3 +102,28 @@ std::string SpdloggerTest::getLogContents() {
101102

102103
return ret;
103104
}
105+
106+
TEST_F(SpdloggerTest, PrefixLogger) {
107+
auto prefixLogger = std::make_shared<cb::logger::PrefixLogger>(
108+
"prefix_logger", cb::logger::get());
109+
prefixLogger->setPrefix(cb::logger::BasicJsonType({{"prefix", "test"}}),
110+
"prefix=test");
111+
112+
prefixLogger->log(spdlog::level::info, "my message");
113+
prefixLogger->logWithContext(
114+
spdlog::level::info, "my message", {{"key", "value"}});
115+
116+
cb::logger::shutdown();
117+
files = cb::io::findFilesWithPrefix(config.filename);
118+
ASSERT_EQ(1, files.size()) << "We should only have a single logfile";
119+
EXPECT_EQ(
120+
1,
121+
countInFile(files.front(), "INFO my message {\"prefix\":\"test\"}"))
122+
<< getLogContents();
123+
EXPECT_EQ(
124+
1,
125+
countInFile(
126+
files.front(),
127+
"INFO my message {\"prefix\":\"test\",\"key\":\"value\"}"))
128+
<< getLogContents();
129+
}

logger/prefix_logger.cc

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2025-Present Couchbase, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License included
5+
* in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
6+
* in that file, in accordance with the Business Source License, use of this
7+
* software will be governed by the Apache License, Version 2.0, included in
8+
* the file licenses/APL2.txt.
9+
*/
10+
11+
#include "prefix_logger.h"
12+
13+
cb::logger::PrefixLogger::PrefixLogger(const std::string& name,
14+
std::shared_ptr<Logger> baseLogger)
15+
: Logger(name, baseLogger->getSpdLogger()),
16+
contextPrefix(nlohmann::ordered_json::object()) {
17+
}
18+
19+
void cb::logger::PrefixLogger::setPrefix(nlohmann::ordered_json contextPrefix,
20+
std::string_view fmtPrefix) {
21+
this->contextPrefix = contextPrefix;
22+
this->fmtPrefix = fmtPrefix;
23+
}
24+
25+
void cb::logger::PrefixLogger::logWithContext(spdlog::level::level_enum lvl,
26+
std::string_view msg,
27+
Json ctx) {
28+
auto finalCtx = Json(BasicJsonType(contextPrefix));
29+
mergeContext(finalCtx, std::move(ctx));
30+
Logger::logWithContext(lvl, msg, std::move(finalCtx));
31+
}
32+
33+
void cb::logger::PrefixLogger::mergeContext(Json& dest, Json src) const {
34+
fixupContext(dest);
35+
fixupContext(src);
36+
auto& destObj = dest.get_ref<Json::object_t&>();
37+
auto& srcObj = src.get_ref<Json::object_t&>();
38+
std::move(srcObj.begin(), srcObj.end(), std::back_inserter(destObj));
39+
}

logger/prefix_logger.h

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2025-Present Couchbase, Inc.
3+
*
4+
* Use of this software is governed by the Business Source License included
5+
* in the file licenses/BSL-Couchbase.txt. As of the Change Date specified
6+
* in that file, in accordance with the Business Source License, use of this
7+
* software will be governed by the Apache License, Version 2.0, included in
8+
* the file licenses/APL2.txt.
9+
*/
10+
11+
#pragma once
12+
13+
#include "logger/logger.h"
14+
15+
namespace cb::logger {
16+
17+
/**
18+
* A logger that prefixes all log messages with a context.
19+
*/
20+
class PrefixLogger : public Logger {
21+
public:
22+
PrefixLogger(const std::string& name, std::shared_ptr<Logger> baseLogger);
23+
24+
void logWithContext(spdlog::level::level_enum lvl,
25+
std::string_view msg,
26+
Json ctx) override;
27+
28+
/**
29+
* Set the prefix for the logger.
30+
*
31+
* @param contextPrefix The prefix to be added to a JSON context message.
32+
* @param fmtPrefix The prefix to be added to a fmtlib formatted message.
33+
*/
34+
void setPrefix(nlohmann::ordered_json contextPrefix,
35+
std::string_view fmtPrefix);
36+
37+
const nlohmann::ordered_json& getContextPrefix() const {
38+
return contextPrefix;
39+
}
40+
41+
const std::string& getFmtPrefix() const {
42+
return fmtPrefix;
43+
}
44+
45+
protected:
46+
/**
47+
* Append the source object to the destination context.
48+
*/
49+
void mergeContext(Json& dest, Json src) const;
50+
51+
private:
52+
/**
53+
* The prefix to be added to a JSON context message.
54+
*/
55+
nlohmann::ordered_json contextPrefix;
56+
57+
/**
58+
* The prefix to be added to a fmtlib formatted message.
59+
* Note: this is currently not used for formatting, but can be read back
60+
* from the instance, which is currently used.
61+
*/
62+
std::string fmtPrefix;
63+
};
64+
65+
} // namespace cb::logger

logger/spdlogger.cc

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,22 @@ void cb::logger::Logger::flush_() {
8181
baseLogger->flush();
8282
}
8383

84+
void cb::logger::Logger::fixupContext(Json& ctx) const {
85+
if (ctx.is_null()) {
86+
ctx = Json::object();
87+
return;
88+
}
89+
if (!ctx.is_object()) {
90+
#if CB_DEVELOPMENT_ASSERTS
91+
throw std::invalid_argument(fmt::format(
92+
"JSON context must be an object, not `{}`", ctx.dump()));
93+
#else
94+
// In production, handle this case gracefully.
95+
ctx = Json{{"context", std::move(ctx)}};
96+
#endif
97+
}
98+
}
99+
84100
void cb::logger::Logger::logFormatted(spdlog::level::level_enum lvl,
85101
fmt::string_view fmt,
86102
fmt::format_args args) {
@@ -366,15 +382,7 @@ void cb::logger::setLogLevels(spdlog::level::level_enum level) {
366382
void cb::logger::Logger::logWithContext(spdlog::level::level_enum lvl,
367383
std::string_view msg,
368384
Json ctx) {
369-
if (!ctx.is_object()) {
370-
#if CB_DEVELOPMENT_ASSERTS
371-
throw std::invalid_argument(fmt::format(
372-
"JSON context must be an object, not `{}`", ctx.dump()));
373-
#else
374-
// In production, handle this case gracefully.
375-
ctx = Json{{"context", std::move(ctx)}};
376-
#endif
377-
}
385+
fixupContext(ctx);
378386

379387
// TODO: Consider checking that the message conforms to the conventions and
380388
// doesn't contain ".:()", starts with a capital, etc.

0 commit comments

Comments
 (0)