Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ bazel_dep(name = "gazelle", version = "0.45.0")
bazel_dep(name = "abseil-cpp", version = "20250814.0")
bazel_dep(name = "bazel_skylib", version = "1.8.1")
bazel_dep(name = "crc32c", version = "1.1.0")
bazel_dep(name = "fmt", version = "9.1.0")
bazel_dep(name = "fmt", version = "12.1.0")
bazel_dep(name = "googletest", version = "1.17.0")
bazel_dep(name = "lexy", version = "2025.05.0")
bazel_dep(name = "liburing", version = "2.14")
Expand Down
34 changes: 28 additions & 6 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 0 additions & 6 deletions bazel/internal.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,6 @@ def redpanda_copts():
copts.append("-Wno-missing-field-initializers")
copts.append("-Wimplicit-fallthrough")

# for fmt v9 so that we do not need to write fmt::formatter wrappers for
# every output stream operator. note that we can't move to fmt >=v10 because
# this workaround macro was removed. so we'll need to rewrite the format
# handling for >1000 types.
copts.append("-DFMT_DEPRECATED_OSTREAM")

return copts

def antithesis_deps():
Expand Down
5 changes: 4 additions & 1 deletion bazel/repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ def data_dependency():
sha256 = "791d9f163f458d0ba4c94251f58ef5af9157952a9569ce0968d89aeb585af34f",
strip_prefix = "avro-46fe1e36f680d75219cba46368de38321f1810ed",
url = "https://github.com/redpanda-data/avro/archive/46fe1e36f680d75219cba46368de38321f1810ed.tar.gz",
patches = ["//bazel/thirdparty:avro-snappy-includes.patch"],
patches = [
"//bazel/thirdparty:avro-snappy-includes.patch",
"//bazel/thirdparty:avro-fmt-const.patch",
],
patch_args = ["-p1"],
)

Expand Down
26 changes: 26 additions & 0 deletions bazel/thirdparty/avro-fmt-const.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
diff --git a/lang/c++/include/avro/Types.hh b/lang/c++/include/avro/Types.hh
index 0000000..0000001 100644
--- a/lang/c++/include/avro/Types.hh
+++ b/lang/c++/include/avro/Types.hh
@@ -113,7 +113,7 @@
template<>
struct fmt::formatter<avro::Type> : fmt::formatter<std::string> {
template<typename FormatContext>
- auto format(avro::Type t, FormatContext &ctx) {
+ auto format(avro::Type t, FormatContext &ctx) const {
return fmt::formatter<std::string>::format(avro::toString(t), ctx);
}
};
diff --git a/lang/c++/include/avro/Node.hh b/lang/c++/include/avro/Node.hh
index 0000000..0000001 100644
--- a/lang/c++/include/avro/Node.hh
+++ b/lang/c++/include/avro/Node.hh
@@ -232,7 +232,7 @@
template<>
struct fmt::formatter<avro::Name> : fmt::formatter<std::string> {
template<typename FormatContext>
- auto format(const avro::Name &n, FormatContext &ctx) {
+ auto format(const avro::Name &n, FormatContext &ctx) const {
return fmt::formatter<std::string>::format(n.fullname(), ctx);
}
};
4 changes: 4 additions & 0 deletions src/v/base/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ redpanda_cc_library(
],
hdrs = [
"compiler_utils.h",
"external_fmt.h",
"format_to.h",
"likely.h",
"oncore.h",
Expand All @@ -23,7 +24,10 @@ redpanda_cc_library(
],
visibility = ["//visibility:public"],
deps = [
"@boost//:beast",
"@boost//:filesystem",
"@boost//:outcome",
"@boost//:system",
"@fmt",
"@seastar",
],
Expand Down
34 changes: 34 additions & 0 deletions src/v/base/external_fmt.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2026 Redpanda Data, Inc.
*
* Use of this software is governed by the Business Source License
* included in the file licenses/BSL.md
*
* As of the Change Date specified in that file, in accordance with
* the Business Source License, use of this software will be governed
* by the Apache License, Version 2.0
*/

#pragma once

/// Formatters for commonly-used external / third-party types that have
/// operator<< but no fmt::formatter specialization. Include this header
/// whenever you need to format one of these types with {fmt}.

#include <boost/beast/http/status.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/system/error_code.hpp>
#include <fmt/ostream.h>

template<>
struct fmt::formatter<boost::beast::http::status> : fmt::ostream_formatter {};

template<>
struct fmt::formatter<boost::beast::http::verb> : fmt::ostream_formatter {};

template<>
struct fmt::formatter<boost::system::error_code> : fmt::ostream_formatter {};

template<>
struct fmt::formatter<boost::filesystem::path> : fmt::ostream_formatter {};
83 changes: 81 additions & 2 deletions src/v/base/format_to.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,44 @@

#include <fmt/format.h>
#include <fmt/ostream.h>
#include <fmt/std.h>

namespace fmt {

/// Formatter for std::unique_ptr<T> — formats the pointee or "null".
template<typename T, typename D>
requires is_formattable<T>::value
struct formatter<std::unique_ptr<T, D>> {
constexpr auto parse(format_parse_context& ctx) const {
return ctx.begin();
}
template<typename Ctx>
auto format(const std::unique_ptr<T, D>& p, Ctx& ctx) const {
if (p) {
return fmt::format_to(ctx.out(), "{}", *p);
}
return fmt::format_to(ctx.out(), "null");
}
};

using iterator = format_context::iterator;

template<typename T>
concept HasFormatToMethod = requires(const T& obj, iterator out) {
{ obj.format_to(out) } -> std::same_as<iterator>;
};

/// Concept for types with a free function `format_to(const T&, iterator)`.
/// Used for enums and other types that cannot have member functions.
/// The free function must be ADL-findable (i.e. in the same namespace as T).
template<typename T>
concept HasFormatToFreeFunction = !HasFormatToMethod<T>
&& requires(const T& obj, iterator out) {
{
format_to(obj, out)
} -> std::same_as<iterator>;
};

/**
* A formatter that supports any type that implements a `format_to` method.
*
Expand Down Expand Up @@ -66,16 +94,67 @@ struct formatter<T> {
}
};

/**
* A formatter for types that provide a free function
* `format_to(const T&, fmt::iterator) -> fmt::iterator`, found via ADL.
*
* This is the counterpart of HasFormatToMethod for types that cannot have
* member functions (e.g. enums).
*/
template<HasFormatToFreeFunction T>
struct formatter<T> {
constexpr fmt::format_parse_context::iterator
parse(fmt::format_parse_context& ctx) const {
auto it = ctx.begin();
if (it != ctx.end() && *it != '}') {
throw fmt::format_error("invalid format specifier for this type");
}
return it;
}

iterator format(const T& obj, format_context& ctx) const {
return format_to(obj, ctx.out());
}
};

} // namespace fmt

/// Checked wrapper around fmt::streamed().
///
/// fmt::streamed(x) formats x via operator<<. But types that satisfy
/// HasFormatToMethod / HasFormatToFreeFunction have a blanket operator<<
/// (below) that delegates back to fmt, creating an infinite recursion:
///
/// format_to() -> streamed(x) -> operator<< -> fmt::print -> format_to()
///
/// If a type is formattable through format_to, just use fmt::format("{}", x)
/// directly — there is no need for streamed(). This wrapper enforces that
/// invariant at compile time.
template<typename T>
auto fmt_streamed(const T& val) {
using raw = std::remove_cvref_t<T>;
static_assert(
!fmt::HasFormatToMethod<raw> && !fmt::HasFormatToFreeFunction<raw>,
"Do not use fmt::streamed() / fmt_streamed() on types with format_to — "
"use fmt::format(\"{}\", val) instead. streamed() triggers operator<< "
"which calls back into fmt, causing infinite recursion.");
return fmt::streamed(val);
}

namespace std {
// For both googletest and for other external libraries that may use
// `operator<<` to print stuff, give a blanket implementation that delegates to
// the `format_to` method.
// the `format_to` method or free function.
//
// We have to put this in the std namespace for overload resolution rules to be
// able to find it for arbitrary T.
template<fmt::HasFormatToMethod T>
//
// WARNING: this creates a potential infinite-recursion footgun with
// fmt::streamed(). Never pass a type that has format_to through
// fmt::streamed() — use fmt_streamed() (above) which static_asserts against
// it, or just use fmt::format("{}", val) directly.
template<typename T>
requires fmt::HasFormatToMethod<T> || fmt::HasFormatToFreeFunction<T>
// NOLINTNEXTLINE(*-dcl58-*)
ostream& operator<<(ostream& os, const T& obj) {
fmt::print(os, "{}", obj);
Comment thread
dotnwat marked this conversation as resolved.
Expand Down
16 changes: 16 additions & 0 deletions src/v/base/outcome.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <boost/outcome/iostream_support.hpp>
#include <boost/outcome/try.hpp>
#include <boost/outcome/utils.hpp>
#include <fmt/format.h>

namespace outcome = boost::outcome_v2;

Expand All @@ -43,3 +44,18 @@ using checked

template<class T>
constexpr bool is_result_v = outcome::is_basic_result_v<T>;

template<typename T, typename E, typename P>
struct fmt::formatter<boost::outcome_v2::basic_result<T, E, P>> {
constexpr auto parse(fmt::format_parse_context& ctx) const {
return ctx.begin();
}
template<typename Ctx>
auto
format(const boost::outcome_v2::basic_result<T, E, P>& r, Ctx& ctx) const {
if (r.has_value()) {
return fmt::format_to(ctx.out(), "{}", r.value());
}
return fmt::format_to(ctx.out(), "{}", r.error());
}
};
12 changes: 4 additions & 8 deletions src/v/base/source_location.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
* by the Apache License, Version 2.0
*/
#pragma once
#include "base/format_to.h"

#include <cstdint>
#include <ostream>
#include <source_location>

namespace vlog {
Expand All @@ -32,11 +33,6 @@ consteval const char* file_basename(
}
} // namespace detail

// file_line represents a source file (without the full path) and the line
// number in a file.
//
// Usage is similar to std::source_location, but the full path is dropped so
// that we don't include local paths from CI machines, etc.
struct file_line {
const char* filename;
unsigned line;
Expand All @@ -48,8 +44,8 @@ struct file_line {
.line = src.line()};
}

friend std::ostream& operator<<(std::ostream& o, const file_line& fl) {
return o << fl.filename << ":" << fl.line;
fmt::iterator format_to(fmt::iterator it) const {
return fmt::format_to(it, "{}:{}", filename, line);
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/v/base/tests/format_to_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ TEST(Formatter, InsideNamespacedFormatTo) {
EXPECT_EQ("hello: {a: bar, b: 3}", fmt::format("hello: {}", f)) << f;
ns::bar b{.f = f};
EXPECT_EQ(
"world: {f: {a: bar, b: 3}}", fmt::format("world: {}", fmt::streamed(b)))
"world: {f: {a: bar, b: 3}}", fmt::format("world: {}", fmt_streamed(b)))
<< b;
}

Expand Down
Loading
Loading