diff --git a/include/blackhole/formatter/json.hpp b/include/blackhole/formatter/json.hpp index c52b31ef..157df40b 100644 --- a/include/blackhole/formatter/json.hpp +++ b/include/blackhole/formatter/json.hpp @@ -136,6 +136,18 @@ class builder { auto timestamp(const std::string& pattern) & -> builder&; auto timestamp(const std::string& pattern) && -> builder&&; + /// Sets the given formatting specification to an attribute with the specified name. + /// + /// Custom formatting is useful when it's required to build a JSON tree with preformatted + /// values using a spec, for example, to write a number using HEX representation. Note, that + /// after formatting a value will be written as a string. + /// + /// \param name attribute name. + /// \param spec string representation of a spec. The spec should match with libfmt formatting + /// specification. + auto format(std::string name, std::string spec) & -> builder&; + auto format(std::string name, std::string spec) && -> builder&&; + auto build() const && -> std::unique_ptr; }; diff --git a/src/formatter/json.cpp b/src/formatter/json.cpp index e9a2d9a4..8359eb61 100644 --- a/src/formatter/json.cpp +++ b/src/formatter/json.cpp @@ -61,7 +61,7 @@ struct visitor_t { rapidjson::StringRef(value.data(), value.size()), allocator); } - // For non-owning buffers. + // For temporary buffers, rapidjson will copy the data. auto operator()(const char* data, std::size_t size) -> void { node.AddMember(rapidjson::StringRef(name.data(), name.size()), rapidjson::Value(data, static_cast(size), allocator), allocator); @@ -76,6 +76,35 @@ struct visitor_t { } }; +// TODO: Decompose. Almost the same class is in `string.cpp`. +class view_visitor : public boost::static_visitor<> { + writer_t& writer; + const std::string& spec; + +public: + view_visitor(writer_t& writer, const std::string& spec) noexcept : + writer(writer), + spec(spec) + {} + + auto operator()(std::nullptr_t) const -> void { + writer.write(spec, "none"); + } + + template + auto operator()(T value) const -> void { + writer.write(spec, value); + } + + auto operator()(const string_view& value) const -> void { + writer.write(spec, value.data()); + } + + auto operator()(const attribute::view_t::function_type& value) const -> void { + value(writer); + } +}; + /// A RapidJSON Stream concept implementation required to avoid intermediate buffer allocation. struct stream_t { typedef char Ch; @@ -109,6 +138,7 @@ class json_t::properties_t { } routing; std::unordered_map mapping; + std::unordered_map formatting; properties_t() : unique(false), @@ -126,6 +156,7 @@ class json_t::inner_t { std::map routing; std::unordered_map mapping; + std::unordered_map formatting; bool unique; bool newline; @@ -136,6 +167,7 @@ class json_t::inner_t { inner_t(json_t::properties_t properties) : rest(properties.routing.unspecified), mapping(std::move(properties.mapping)), + formatting(std::move(properties.formatting)), unique(properties.unique), newline(properties.newline), timestamp(properties.timestamp), @@ -264,19 +296,43 @@ class json_t::inner_t::builder { auto apply(const string_view& name, const T& value) -> void { const auto renamed = inner.renamed(name); visitor_t visitor{inner.get(name, root), root.GetAllocator(), renamed}; - visitor(value); + + const auto it = inner.formatting.find(std::string(name.data(), name.size())); + if (it != std::end(inner.formatting)) { + writer_t wr; + wr.write(it->second, value); + visitor(wr.inner.data(), wr.inner.size()); + } else { + visitor(value); + } } auto apply(const string_view& name, const char* data, std::size_t size) -> void { const auto renamed = inner.renamed(name); visitor_t visitor{inner.get(name, root), root.GetAllocator(), renamed}; - visitor(data, size); + + const auto it = inner.formatting.find(std::string(name.data(), name.size())); + if (it != std::end(inner.formatting)) { + writer_t wr; + wr.write(it->second, fmt::StringRef(data, size)); + visitor(wr.inner.data(), wr.inner.size()); + } else { + visitor(data, size); + } } auto apply(const string_view& name, const attribute::view_t& value) -> void { const auto renamed = inner.renamed(name); visitor_t visitor{inner.get(name, root), root.GetAllocator(), renamed}; - boost::apply_visitor(visitor, value.inner().value); + + const auto it = inner.formatting.find(std::string(name.data(), name.size())); + if (it != std::end(inner.formatting)) { + writer_t wr; + boost::apply_visitor(view_visitor(wr, it->second), value.inner().value); + visitor(wr.inner.data(), wr.inner.size()); + } else { + boost::apply_visitor(visitor, value.inner().value); + } } }; @@ -426,6 +482,15 @@ auto builder::timestamp(const std::string& pattern) && -> builder&& { return std::move(timestamp(pattern)); } +auto builder::format(std::string name, std::string spec) & -> builder& { + d->formatting[name] = spec; + return *this; +} + +auto builder::format(std::string name, std::string spec) && -> builder&& { + return std::move(format(std::move(name), std::move(spec))); +} + auto builder::build() const && -> std::unique_ptr { return blackhole::make_unique(std::move(*d)); } diff --git a/tests/src/unit/formatter/json.cpp b/tests/src/unit/formatter/json.cpp index ab878ce2..6d480170 100644 --- a/tests/src/unit/formatter/json.cpp +++ b/tests/src/unit/formatter/json.cpp @@ -535,6 +535,63 @@ TEST(json_t, NoNewlineByDefault) { EXPECT_FALSE(json_t().newline()); } +TEST(json_t, MutateSeverityFormat) { + auto formatter = builder() + .format("severity", "{:>4}") + .build(); + + const string_view message(""); + const attribute_pack pack; + record_t record(2, message, pack); + writer_t writer; + formatter->format(record, writer); + + rapidjson::Document doc; + doc.Parse<0>(writer.result().to_string().c_str()); + + ASSERT_TRUE(doc.HasMember("severity")); + ASSERT_TRUE(doc["severity"].IsString()); + EXPECT_EQ(" 2", std::string(doc["severity"].GetString())); +} + +TEST(json_t, MutateThreadFormat) { + auto formatter = builder() + .format("thread", "0x123456") + .build(); + + const string_view message("value"); + const attribute_list attributes{{"source", "storage"}}; + const attribute_pack pack{attributes}; + record_t record(0, message, pack); + writer_t writer; + formatter->format(record, writer); + + rapidjson::Document doc; + doc.Parse<0>(writer.result().to_string().c_str()); + ASSERT_TRUE(doc.HasMember("thread")); + ASSERT_TRUE(doc["thread"].IsString()); + EXPECT_STREQ("0x123456", doc["thread"].GetString()); +} + +TEST(json_t, MutateAttributeFormat) { + auto formatter = builder() + .format("source", "[{:.>6.4}]") + .build(); + + const string_view message("value"); + const attribute_list attributes{{"source", "storage"}}; + const attribute_pack pack{attributes}; + record_t record(0, message, pack); + writer_t writer; + formatter->format(record, writer); + + rapidjson::Document doc; + doc.Parse<0>(writer.result().to_string().c_str()); + ASSERT_TRUE(doc.HasMember("source")); + ASSERT_TRUE(doc["source"].IsString()); + EXPECT_STREQ("[..stor]", doc["source"].GetString()); +} + TEST(builder_t, Newline) { const auto layout = builder() .newline()