Skip to content

Commit a19b3b1

Browse files
committed
RCBC-375: implement log forwarding
1 parent 66e1c28 commit a19b3b1

File tree

6 files changed

+345
-12
lines changed

6 files changed

+345
-12
lines changed

bin/console

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ def cluster
5353
end
5454
end
5555

56+
def shutdown
57+
return unless @cluster
58+
59+
@cluster.disconnect
60+
@cluster = nil
61+
end
62+
63+
at_exit { shutdown }
64+
5665
def default_collection(bucket_name = "default")
5766
@collections ||= {}
5867
@collections[bucket_name] ||= cluster.bucket(bucket_name).default_collection

ext/couchbase

ext/couchbase.cxx

Lines changed: 189 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#include <asio.hpp>
2323
#include <spdlog/cfg/env.h>
24+
#include <spdlog/sinks/base_sink.h>
2425
#include <spdlog/spdlog.h>
2526

2627
#include <snappy.h>
@@ -32,6 +33,7 @@
3233

3334
#include <core/cluster.hxx>
3435
#include <core/design_document_namespace_fmt.hxx>
36+
#include <core/logger/configuration.hxx>
3537
#include <core/operations.hxx>
3638
#include <core/operations/management/analytics.hxx>
3739
#include <core/operations/management/bucket.hxx>
@@ -182,10 +184,11 @@ cb_binary_new(VALUE str)
182184
return couchbase::core::utils::to_binary(static_cast<const char*>(RSTRING_PTR(str)), static_cast<std::size_t>(RSTRING_LEN(str)));
183185
}
184186

187+
template<typename StringLike>
185188
static inline VALUE
186-
cb_str_new(const std::string& str)
189+
cb_str_new(const StringLike str)
187190
{
188-
return rb_external_str_new(str.data(), static_cast<long>(str.size()));
191+
return rb_external_str_new(std::data(str), static_cast<long>(std::size(str)));
189192
}
190193

191194
static inline VALUE
@@ -200,6 +203,12 @@ cb_str_new(const std::byte* data, std::size_t size)
200203
return rb_external_str_new(reinterpret_cast<const char*>(data), static_cast<long>(size));
201204
}
202205

206+
static inline VALUE
207+
cb_str_new(const char* data)
208+
{
209+
return rb_external_str_new_cstr(data);
210+
}
211+
203212
static inline VALUE
204213
cb_str_new(const std::optional<std::string>& str)
205214
{
@@ -266,6 +275,132 @@ init_versions(VALUE mCouchbase)
266275
std::string_view(RSTRING_PTR(build_info), static_cast<std::size_t>(RSTRING_LEN(build_info))));
267276
}
268277

278+
template<typename Mutex>
279+
class ruby_logger_sink : public spdlog::sinks::base_sink<Mutex>
280+
{
281+
public:
282+
explicit ruby_logger_sink(VALUE ruby_logger)
283+
: ruby_logger_{ ruby_logger }
284+
{
285+
}
286+
287+
void flush_deferred_messages()
288+
{
289+
std::lock_guard<Mutex> lock(spdlog::sinks::base_sink<Mutex>::mutex_);
290+
auto messages_ = std::move(deferred_messages_);
291+
while (!messages_.empty()) {
292+
write_message(messages_.front());
293+
messages_.pop();
294+
}
295+
}
296+
297+
static VALUE map_log_level(spdlog::level::level_enum level)
298+
{
299+
switch (level) {
300+
case spdlog::level::trace:
301+
return rb_id2sym(rb_intern("trace"));
302+
case spdlog::level::debug:
303+
return rb_id2sym(rb_intern("debug"));
304+
case spdlog::level::info:
305+
return rb_id2sym(rb_intern("info"));
306+
case spdlog::level::warn:
307+
return rb_id2sym(rb_intern("warn"));
308+
case spdlog::level::err:
309+
return rb_id2sym(rb_intern("error"));
310+
case spdlog::level::critical:
311+
return rb_id2sym(rb_intern("critical"));
312+
case spdlog::level::off:
313+
return rb_id2sym(rb_intern("off"));
314+
default:
315+
break;
316+
}
317+
return Qnil;
318+
}
319+
320+
protected:
321+
struct log_message_for_ruby {
322+
spdlog::level::level_enum level;
323+
spdlog::log_clock::time_point time;
324+
size_t thread_id;
325+
std::string payload;
326+
const char* filename;
327+
int line;
328+
const char* funcname;
329+
};
330+
331+
void sink_it_(const spdlog::details::log_msg& msg) override
332+
{
333+
deferred_messages_.emplace(log_message_for_ruby{
334+
msg.level,
335+
msg.time,
336+
msg.thread_id,
337+
{ msg.payload.begin(), msg.payload.end() },
338+
msg.source.filename,
339+
msg.source.line,
340+
msg.source.funcname,
341+
});
342+
}
343+
344+
void flush_() override
345+
{
346+
/* do nothing here, the flush will be initiated by the SDK */
347+
}
348+
349+
private:
350+
struct argument_pack {
351+
VALUE logger;
352+
const log_message_for_ruby& msg;
353+
};
354+
355+
static VALUE invoke_log(VALUE arg)
356+
{
357+
auto* args = reinterpret_cast<argument_pack*>(arg);
358+
const auto& msg = args->msg;
359+
360+
VALUE filename = Qnil;
361+
if (msg.filename != nullptr) {
362+
filename = cb_str_new(msg.filename);
363+
}
364+
VALUE line = Qnil;
365+
if (msg.line > 0) {
366+
line = ULL2NUM(msg.line);
367+
}
368+
VALUE function_name = Qnil;
369+
if (msg.funcname != nullptr) {
370+
function_name = cb_str_new(msg.funcname);
371+
}
372+
auto seconds = std::chrono::duration_cast<std::chrono::seconds>(msg.time.time_since_epoch());
373+
auto nanoseconds = msg.time.time_since_epoch() - seconds;
374+
return rb_funcall(args->logger,
375+
rb_intern("log"),
376+
8,
377+
map_log_level(msg.level),
378+
ULL2NUM(msg.thread_id),
379+
ULL2NUM(seconds.count()),
380+
ULL2NUM(nanoseconds.count()),
381+
cb_str_new(msg.payload),
382+
filename,
383+
line,
384+
function_name);
385+
}
386+
387+
void write_message(const log_message_for_ruby& msg)
388+
{
389+
if (NIL_P(ruby_logger_)) {
390+
return;
391+
}
392+
argument_pack args{ ruby_logger_, msg };
393+
rb_rescue(invoke_log, reinterpret_cast<VALUE>(&args), nullptr, Qnil);
394+
}
395+
396+
VALUE ruby_logger_{ Qnil };
397+
std::queue<log_message_for_ruby> deferred_messages_{};
398+
};
399+
400+
using ruby_logger_sink_ptr = std::shared_ptr<ruby_logger_sink<std::mutex>>;
401+
402+
static ruby_logger_sink_ptr cb_global_sink{ nullptr };
403+
269404
struct cb_backend_data {
270405
std::unique_ptr<asio::io_context> ctx;
271406
std::shared_ptr<couchbase::core::cluster> cluster;
@@ -1064,6 +1199,9 @@ cb_wait_for_future(Future&& f) -> decltype(f.get())
10641199
&arg,
10651200
nullptr,
10661201
nullptr);
1202+
if (cb_global_sink) {
1203+
cb_global_sink->flush_deferred_messages();
1204+
}
10671205
return std::move(arg.res);
10681206
}
10691207

@@ -1373,6 +1511,9 @@ cb_Backend_close(VALUE self)
13731511
cb_backend_data* backend = nullptr;
13741512
TypedData_Get_Struct(self, cb_backend_data, &cb_backend_type, backend);
13751513
cb_backend_close(backend);
1514+
if (cb_global_sink) {
1515+
cb_global_sink->flush_deferred_messages();
1516+
}
13761517
return Qnil;
13771518
}
13781519

@@ -7265,9 +7406,8 @@ cb_Backend_document_view(VALUE self, VALUE bucket_name, VALUE design_document_na
72657406
}
72667407

72677408
static VALUE
7268-
cb_Backend_set_log_level(VALUE self, VALUE log_level)
7409+
cb_Backend_set_log_level(VALUE /* self */, VALUE log_level)
72697410
{
7270-
(void)self;
72717411
Check_Type(log_level, T_SYMBOL);
72727412
if (ID type = rb_sym2id(log_level); type == rb_intern("trace")) {
72737413
spdlog::set_level(spdlog::level::trace);
@@ -7291,9 +7431,8 @@ cb_Backend_set_log_level(VALUE self, VALUE log_level)
72917431
}
72927432

72937433
static VALUE
7294-
cb_Backend_get_log_level(VALUE self)
7434+
cb_Backend_get_log_level(VALUE /* self */)
72957435
{
7296-
(void)self;
72977436
switch (spdlog::get_level()) {
72987437
case spdlog::level::trace:
72997438
return rb_id2sym(rb_intern("trace"));
@@ -7315,6 +7454,43 @@ cb_Backend_get_log_level(VALUE self)
73157454
return Qnil;
73167455
}
73177456

7457+
static VALUE
7458+
cb_Backend_install_logger_shim(VALUE self, VALUE logger, VALUE log_level)
7459+
{
7460+
couchbase::core::logger::reset();
7461+
rb_iv_set(self, "@__logger_shim", logger);
7462+
if (NIL_P(logger)) {
7463+
return Qnil;
7464+
}
7465+
Check_Type(log_level, T_SYMBOL);
7466+
couchbase::core::logger::level level{ couchbase::core::logger::level::off };
7467+
if (ID type = rb_sym2id(log_level); type == rb_intern("trace")) {
7468+
level = couchbase::core::logger::level::trace;
7469+
} else if (type == rb_intern("debug")) {
7470+
level = couchbase::core::logger::level::debug;
7471+
} else if (type == rb_intern("info")) {
7472+
level = couchbase::core::logger::level::info;
7473+
} else if (type == rb_intern("warn")) {
7474+
level = couchbase::core::logger::level::warn;
7475+
} else if (type == rb_intern("error")) {
7476+
level = couchbase::core::logger::level::err;
7477+
} else if (type == rb_intern("critical")) {
7478+
level = couchbase::core::logger::level::critical;
7479+
} else {
7480+
rb_iv_set(self, "__logger_shim", Qnil);
7481+
return Qnil;
7482+
}
7483+
7484+
auto sink = std::make_shared<ruby_logger_sink<std::mutex>>(logger);
7485+
couchbase::core::logger::configuration configuration;
7486+
configuration.console = false;
7487+
configuration.log_level = level;
7488+
configuration.sink = sink;
7489+
couchbase::core::logger::create_file_logger(configuration);
7490+
cb_global_sink = sink;
7491+
return Qnil;
7492+
}
7493+
73187494
static VALUE
73197495
cb_Backend_snappy_compress(VALUE self, VALUE data)
73207496
{
@@ -7524,6 +7700,7 @@ init_backend(VALUE mCouchbase)
75247700
rb_define_singleton_method(cBackend, "parse_connection_string", VALUE_FUNC(cb_Backend_parse_connection_string), 1);
75257701
rb_define_singleton_method(cBackend, "set_log_level", VALUE_FUNC(cb_Backend_set_log_level), 1);
75267702
rb_define_singleton_method(cBackend, "get_log_level", VALUE_FUNC(cb_Backend_get_log_level), 0);
7703+
rb_define_singleton_method(cBackend, "install_logger_shim", VALUE_FUNC(cb_Backend_install_logger_shim), 2);
75277704
rb_define_singleton_method(cBackend, "snappy_compress", VALUE_FUNC(cb_Backend_snappy_compress), 1);
75287705
rb_define_singleton_method(cBackend, "snappy_uncompress", VALUE_FUNC(cb_Backend_snappy_uncompress), 1);
75297706
rb_define_singleton_method(cBackend, "leb128_encode", VALUE_FUNC(cb_Backend_leb128_encode), 1);
@@ -7536,14 +7713,15 @@ init_backend(VALUE mCouchbase)
75367713
void
75377714
init_logger()
75387715
{
7539-
couchbase::core::logger::create_console_logger();
7540-
if (auto env_val = spdlog::details::os::getenv("COUCHBASE_BACKEND_LOG_LEVEL"); !env_val.empty()) {
7541-
couchbase::core::logger::set_log_levels(couchbase::core::logger::level_from_str(env_val));
7542-
}
7543-
75447716
if (auto env_val = spdlog::details::os::getenv("COUCHBASE_BACKEND_DONT_INSTALL_TERMINATE_HANDLER"); env_val.empty()) {
75457717
couchbase::core::platform::install_backtrace_terminate_handler();
75467718
}
7719+
if (auto env_val = spdlog::details::os::getenv("COUCHBASE_BACKEND_DONT_USE_BUILTIN_LOGGER"); env_val.empty()) {
7720+
couchbase::core::logger::create_console_logger();
7721+
if (env_val = spdlog::details::os::getenv("COUCHBASE_BACKEND_LOG_LEVEL"); !env_val.empty()) {
7722+
couchbase::core::logger::set_log_levels(couchbase::core::logger::level_from_str(env_val));
7723+
}
7724+
}
75477725
}
75487726

75497727
extern "C" {

lib/couchbase/logger.rb

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
require "logger"
16+
1517
module Couchbase
1618
# Set log level
1719
#
@@ -39,4 +41,45 @@ def self.log_level=(level)
3941
def self.log_level
4042
Backend.get_log_level
4143
end
44+
45+
# Return logger associated with the library
46+
def self.logger
47+
@logger # rubocop:disable ThreadSafety/InstanceVariableInClassMethod
48+
end
49+
50+
# Associate logger with the library
51+
#
52+
# The log messages, that are generated by extension might come with out of order timestamps, in order to reduce number
53+
# of switches between Ruby and Native code.
54+
#
55+
# @param [Logger] logger an object implementing logging interface, e.g. stdlib Logger, or something that responds to
56+
# "level"-methods like +#debug+, +#error+, etc.
57+
# @param [Class] adapter_class custom implementation of the logger adapter interface between extension and ruby code.
58+
# See {Utils::StdlibLoggerAdapter} and {Utils::GenericLoggerAdapter}
59+
# @param [Boolean] verbose if true, the message will also include source code location, where the message was
60+
# generated (if available)
61+
# @param [Symbol] log level, see {#log_level=} for allowed values
62+
#
63+
# @example Specify custom logger and limit core messages to debug level
64+
# Couchbase.set_logger(Logger.new(STDERR), level: :debug)
65+
#
66+
# @since 3.3.1
67+
def self.set_logger(logger, adapter_class: nil, verbose: false, level: :info)
68+
@logger = logger # rubocop:disable ThreadSafety/InstanceVariableInClassMethod
69+
if @logger.nil? # rubocop:disable ThreadSafety/InstanceVariableInClassMethod
70+
Backend.install_logger_shim(nil)
71+
return
72+
end
73+
shim =
74+
if adapter_class
75+
adapter_class
76+
elsif logger.is_a?(::Logger)
77+
require "couchbase/utils/stdlib_logger_adapter"
78+
Utils::StdlibLoggerAdapter
79+
else
80+
require "couchbase/utils/generic_logger_adapter"
81+
Utils::GenericLoggerAdapter
82+
end
83+
Backend.install_logger_shim(shim.new(logger, verbose: verbose), level)
84+
end
4285
end

0 commit comments

Comments
 (0)