Skip to content

Commit 30e9c6b

Browse files
committed
adapter for otel logger
1 parent fb94132 commit 30e9c6b

File tree

6 files changed

+502
-7
lines changed

6 files changed

+502
-7
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,7 @@ endif()
588588

589589
option(COUCHBASE_CXX_CLIENT_BUILD_TOOLS "Build tools" ${COUCHBASE_CXX_CLIENT_MASTER_PROJECT})
590590
if(COUCHBASE_CXX_CLIENT_BUILD_TOOLS)
591+
add_subdirectory(observability)
591592
add_subdirectory(tools)
592593
endif()
593594

observability/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
add_library(couchbase_observability OBJECT fmt_log_record_exporter.cxx)
2+
set_target_properties(couchbase_observability PROPERTIES POSITION_INDEPENDENT_CODE ON)
3+
target_link_libraries(
4+
couchbase_observability
5+
PRIVATE project_options
6+
project_warnings
7+
opentelemetry_logs
8+
spdlog::spdlog)
9+
target_include_directories(couchbase_observability PRIVATE ${PROJECT_BINARY_DIR}/generated ${PROJECT_SOURCE_DIR})
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2+
/*
3+
* Copyright 2025-Present Couchbase, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
#include "fmt_log_record_exporter.hxx"
19+
20+
#if defined(__GNUC__)
21+
#pragma GCC diagnostic push
22+
#pragma GCC diagnostic ignored "-Wsign-conversion"
23+
#elif defined(__clang__)
24+
#pragma clang diagnostic push
25+
#pragma clang diagnostic ignored "-Wdeprecated-builtins"
26+
#endif
27+
#include <opentelemetry/sdk/common/attribute_utils.h>
28+
#include <opentelemetry/sdk/logs/read_write_log_record.h>
29+
#include <opentelemetry/sdk/logs/recordable.h>
30+
#if defined(__GNUC__)
31+
#pragma GCC diagnostic pop
32+
#elif defined(__clang__)
33+
#pragma clang diagnostic pop
34+
#endif
35+
36+
#include <spdlog/fmt/chrono.h>
37+
#include <spdlog/fmt/fmt.h>
38+
#include <spdlog/fmt/ranges.h>
39+
40+
#include <spdlog/fmt/bundled/args.h>
41+
42+
#include <cstdio>
43+
#include <memory>
44+
45+
template<>
46+
struct fmt::formatter<opentelemetry::common::SystemTimestamp>
47+
: fmt::formatter<std::chrono::system_clock::time_point> {
48+
49+
template<typename FormatContext>
50+
auto format(const opentelemetry::common::SystemTimestamp& ts, FormatContext& ctx) const
51+
{
52+
return fmt::formatter<std::chrono::system_clock::time_point>::format(
53+
static_cast<std::chrono::system_clock::time_point>(ts), ctx);
54+
}
55+
};
56+
57+
namespace
58+
{
59+
template<typename IdType>
60+
auto
61+
to_hex(const IdType& id) -> std::string
62+
{
63+
std::string buffer(2 * IdType::kSize, '0');
64+
id.ToLowerBase16(buffer);
65+
return buffer;
66+
}
67+
68+
auto
69+
trim_quotes(const std::string& s) -> std::string_view
70+
{
71+
size_t start = 0;
72+
size_t end = s.size();
73+
74+
if (!s.empty() && s.front() == '"') {
75+
++start;
76+
}
77+
if (end > start && s.back() == '"') {
78+
--end;
79+
}
80+
return { s.data() + start, end - start };
81+
}
82+
83+
struct log_ids {
84+
const opentelemetry::trace::TraceId& tid;
85+
const opentelemetry::trace::SpanId& sid;
86+
};
87+
88+
struct log_body {
89+
const opentelemetry::common::AttributeValue& fmt_string;
90+
const std::unordered_map<std::string, opentelemetry::common::AttributeValue>& params;
91+
};
92+
} // namespace
93+
94+
template<>
95+
struct fmt::formatter<log_ids> {
96+
constexpr auto parse(format_parse_context& ctx) const
97+
{
98+
return ctx.begin();
99+
}
100+
101+
template<typename FormatContext>
102+
auto format(const log_ids& ids, FormatContext& ctx) const
103+
{
104+
if (ids.tid.IsValid() && ids.sid.IsValid()) {
105+
return fmt::format_to(
106+
ctx.out(), "\t[tid=\"{}\", sid=\"{}\"]", to_hex(ids.tid), to_hex(ids.sid));
107+
}
108+
return ctx.out();
109+
}
110+
};
111+
112+
template<>
113+
struct fmt::formatter<opentelemetry::common::AttributeValue> {
114+
template<typename ParseContext>
115+
constexpr auto parse(ParseContext& ctx)
116+
{
117+
return ctx.begin();
118+
}
119+
120+
template<typename FormatContext>
121+
auto format(const opentelemetry::common::AttributeValue& value, FormatContext& ctx) const
122+
{
123+
return std::visit(
124+
[&ctx](const auto& v) {
125+
using T = std::decay_t<decltype(v)>;
126+
if constexpr (std::is_same_v<T, std::vector<bool>> ||
127+
std::is_same_v<T, std::vector<int32_t>> ||
128+
std::is_same_v<T, std::vector<int64_t>> ||
129+
std::is_same_v<T, std::vector<uint32_t>> ||
130+
std::is_same_v<T, std::vector<double>> ||
131+
std::is_same_v<T, std::vector<std::string_view>> ||
132+
std::is_same_v<T, std::vector<uint64_t>> ||
133+
std::is_same_v<T, std::vector<uint8_t>>) {
134+
return fmt::format_to(ctx.out(), "[{}]", fmt::join(v, ", "));
135+
}
136+
if constexpr (std::is_same_v<T, const char*>) {
137+
return fmt::format_to(ctx.out(), "\"{}\"", v);
138+
}
139+
if constexpr (std::is_same_v<T, std::string_view>) {
140+
return fmt::format_to(ctx.out(), "\"{}\"", v);
141+
}
142+
return fmt::format_to(ctx.out(), "{}", v);
143+
},
144+
value);
145+
}
146+
};
147+
148+
template<>
149+
struct fmt::formatter<std::unordered_map<std::string, opentelemetry::common::AttributeValue>> {
150+
template<typename ParseContext>
151+
constexpr auto parse(ParseContext& ctx)
152+
{
153+
return ctx.begin();
154+
}
155+
156+
template<typename FormatContext>
157+
auto format(
158+
const std::unordered_map<std::string, opentelemetry::common::AttributeValue>& attributes,
159+
FormatContext& ctx) const
160+
{
161+
if (attributes.empty()) {
162+
return ctx.out();
163+
}
164+
165+
auto out = ctx.out();
166+
out = fmt::format_to(out, "\t{{");
167+
for (auto it = attributes.begin(); it != attributes.end(); ++it) {
168+
if (it != attributes.begin()) {
169+
out = fmt::format_to(out, ", ");
170+
}
171+
out = fmt::format_to(out, "\"{}\": {}", it->first, it->second);
172+
}
173+
out = fmt::format_to(out, "}}");
174+
return out;
175+
}
176+
};
177+
178+
template<>
179+
struct fmt::formatter<log_body> {
180+
constexpr auto parse(format_parse_context& ctx) const
181+
{
182+
return ctx.begin();
183+
}
184+
185+
template<typename FormatContext>
186+
auto format(const log_body& body, FormatContext& ctx) const
187+
{
188+
std::string quoted_fmt_string = fmt::format("{}", body.fmt_string);
189+
auto fmt_string = trim_quotes(quoted_fmt_string);
190+
191+
if (fmt_string.empty()) {
192+
return ctx.out();
193+
}
194+
195+
fmt::dynamic_format_arg_store<fmt::format_context> store;
196+
197+
for (const auto& [name, value] : body.params) {
198+
std::visit(
199+
[&](const auto& v) {
200+
store.push_back(fmt::arg(name.c_str(), v));
201+
},
202+
value);
203+
}
204+
205+
auto result = fmt::vformat(fmt_string, store);
206+
return fmt::format_to(ctx.out(), "\t{}", result);
207+
}
208+
};
209+
210+
template<>
211+
struct fmt::formatter<opentelemetry::sdk::common::OwnedAttributeValue> {
212+
template<typename ParseContext>
213+
constexpr auto parse(ParseContext& ctx)
214+
{
215+
return ctx.begin();
216+
}
217+
218+
template<typename FormatContext>
219+
auto format(const opentelemetry::sdk::common::OwnedAttributeValue& value,
220+
FormatContext& ctx) const
221+
{
222+
return std::visit(
223+
[&ctx](const auto& v) {
224+
using T = std::decay_t<decltype(v)>;
225+
if constexpr (std::is_same_v<T, std::vector<bool>> ||
226+
std::is_same_v<T, std::vector<int32_t>> ||
227+
std::is_same_v<T, std::vector<int64_t>> ||
228+
std::is_same_v<T, std::vector<uint32_t>> ||
229+
std::is_same_v<T, std::vector<double>> ||
230+
std::is_same_v<T, std::vector<std::string_view>> ||
231+
std::is_same_v<T, std::vector<uint64_t>> ||
232+
std::is_same_v<T, std::vector<uint8_t>>) {
233+
return fmt::format_to(ctx.out(), "[{}]", fmt::join(v, ", "));
234+
}
235+
if constexpr (std::is_same_v<T, const char*>) {
236+
return fmt::format_to(ctx.out(), "\"{}\"", v);
237+
}
238+
if constexpr (std::is_same_v<T, std::string_view>) {
239+
return fmt::format_to(ctx.out(), "\"{}\"", v);
240+
}
241+
return fmt::format_to(ctx.out(), "{}", v);
242+
},
243+
value);
244+
}
245+
};
246+
247+
template<>
248+
struct fmt::formatter<opentelemetry::sdk::instrumentationscope::InstrumentationScope> {
249+
constexpr auto parse(format_parse_context& ctx) const
250+
{
251+
return ctx.begin();
252+
}
253+
254+
template<typename FormatContext>
255+
auto format(const opentelemetry::sdk::instrumentationscope::InstrumentationScope& scope,
256+
FormatContext& ctx) const
257+
{
258+
if (const auto& name = scope.GetName(); !name.empty()) {
259+
auto out = fmt::format_to(ctx.out(), " [");
260+
261+
const auto& attrs = scope.GetAttributes();
262+
if (auto attr = attrs.find("process_id"); attr != attrs.end()) {
263+
out = fmt::format_to(out, "{},", attr->second);
264+
}
265+
if (auto attr = attrs.find("thread_id"); attr != attrs.end()) {
266+
out = fmt::format_to(out, "{},", attr->second);
267+
}
268+
return fmt::format_to(out, "{}]", name);
269+
}
270+
return ctx.out();
271+
}
272+
};
273+
274+
namespace couchbase::observability
275+
{
276+
277+
fmt_log_exporter::fmt_log_exporter(FILE* file)
278+
: file_(file)
279+
{
280+
}
281+
282+
auto
283+
fmt_log_exporter::MakeRecordable() noexcept -> std::unique_ptr<opentelemetry::sdk::logs::Recordable>
284+
{
285+
return std::make_unique<opentelemetry::sdk::logs::ReadWriteLogRecord>();
286+
}
287+
288+
auto
289+
fmt_log_exporter::Export(
290+
const opentelemetry::nostd::span<std::unique_ptr<opentelemetry::sdk::logs::Recordable>>&
291+
records) noexcept -> opentelemetry::sdk::common::ExportResult
292+
{
293+
for (auto& record : records) {
294+
auto log_record = std::unique_ptr<opentelemetry::sdk::logs::ReadWriteLogRecord>(
295+
dynamic_cast<opentelemetry::sdk::logs::ReadWriteLogRecord*>(record.release()));
296+
297+
if (log_record == nullptr) {
298+
continue;
299+
}
300+
301+
fmt::println(file_,
302+
"{:%Y-%m-%dT%H:%M:%S%z}{:>7}{}{}{}{}",
303+
log_record->GetObservedTimestamp(),
304+
log_record->GetSeverityText(),
305+
log_record->GetInstrumentationScope(),
306+
log_body{ log_record->GetBody(), log_record->GetAttributes() },
307+
log_record->GetAttributes(),
308+
log_ids{ log_record->GetTraceId(), log_record->GetSpanId() });
309+
}
310+
return opentelemetry::sdk::common::ExportResult::kSuccess;
311+
}
312+
313+
auto
314+
fmt_log_exporter::Shutdown(std::chrono::microseconds /* timeout */) noexcept -> bool
315+
{
316+
return true;
317+
}
318+
319+
auto
320+
fmt_log_exporter::ForceFlush(std::chrono::microseconds /* timeout */) noexcept -> bool
321+
{
322+
fflush(file_);
323+
return true;
324+
}
325+
} // namespace couchbase::observability
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/* -*- Mode: C++; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2+
/*
3+
* Copyright 2025-Present Couchbase, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
#include <opentelemetry/logs/log_record.h>
19+
#include <opentelemetry/sdk/logs/exporter.h>
20+
21+
#include <chrono>
22+
23+
namespace couchbase::observability
24+
{
25+
class fmt_log_exporter : public opentelemetry::sdk::logs::LogRecordExporter
26+
{
27+
public:
28+
explicit fmt_log_exporter(FILE* file);
29+
30+
auto MakeRecordable() noexcept -> std::unique_ptr<opentelemetry::sdk::logs::Recordable> override;
31+
32+
auto Export(
33+
const opentelemetry::nostd::span<std::unique_ptr<opentelemetry::sdk::logs::Recordable>>&
34+
records) noexcept -> opentelemetry::sdk::common::ExportResult override;
35+
36+
auto Shutdown(std::chrono::microseconds timeout) noexcept -> bool override;
37+
38+
auto ForceFlush(std::chrono::microseconds timeout) noexcept -> bool override;
39+
40+
private:
41+
FILE* file_;
42+
};
43+
} // namespace couchbase::observability

tools/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ target_link_libraries(
3535
PRIVATE
3636
${CMAKE_THREAD_LIBS_INIT}
3737
${couchbase_cxx_client_DEFAULT_LIBRARY}
38+
couchbase_observability
3839
CLI11
3940
Microsoft.GSL::GSL
4041
taocpp::json

0 commit comments

Comments
 (0)