-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initiating some documentation using Doxygen & Sphinx #25
Initiating some documentation using Doxygen & Sphinx #25
Conversation
Thank you! Until now we didn't use Doxgen (or any documentation generator at all). But if it helps you I am happy to merge it. Just please don't write documentation comments like this: /// \param msg the message
void print(char const* msg); I don't think that telling the reader that the I would rather like to document the non-obvious stuff and leave the obvious stuff undocumented. Having examples, usage instructions and documenting edge cases and non-obvious things is of course very welcome. |
Maybe as an acronym, MOTIS should be spelled uppercase ( |
Yes, I think that having documentation on core utilities like logging is important for the dev contributors 🙂
I totally agree 💯 I like those articles on this topic:
Would you agree to merge this PR first, when it's ready, |
For this PR we can focus on merging docs generation. |
7f2b564
to
a4fff0b
Compare
2f0ba76
to
e164c0c
Compare
In the end I implemented both in this PR. Is that OK with you? The benefit for me is that I did not waste time documenting the old logging function 😅 |
e164c0c
to
e363b76
Compare
29255a7
to
a5afa73
Compare
Ok, we can merge it also together. For logging we can merge it for now but in the end we should settle for a structured logging interface that makes use of additional fields. At least this would be in line with what OTEL offers. Example: utl::err(
"nigiri.loader.gtfs.stops",
{{"dataset", dataset},
{"stop_id", stop_id}},
"{}: parent stop ID {} not found",
dataset, stop_id); This way, you would be able to search in aggregation tools for messages regarding a specific stop or a specific dataset in this case (or whatever other fields if you add them in a structured way). I would also try if |
a5afa73
to
6acf5d1
Compare
Regarding the interface, I would add shorthands:
So we still have |
Regarding the structured part (indexable fields), it could maybe be something like using field_value_t = std::variant<std::string_view, std::uint64_t, std::int64_t, double>;
using fields_t = std::initializer_list<std::pair<std::string_view, field_value_t>>; But I haven't looked deeply into OTEL if this is a good fit. |
abdb361
to
91101c8
Compare
The CI pipeline is being a bit difficult to beat... If that's OK with you, we could maybe merge this PR as it is, Also, once this is merged, I'd like to volunteer to make I'm out for today. |
91101c8
to
cfe6c9d
Compare
The goal would be to eliminate all macro usage by using |
cfe6c9d
to
b6ea79b
Compare
IMO macros have only one use case left where it's hard to replace them. That's when you want to avoid that arguments are created at all if the function should not be called. Like here: #define log_if_macro(condition, ...) \
if (condition) { \
// ... \
}
template <typename... Args>
void log_if(bool const condition, Args... args) {
if (condition) {
// ...
}
}
auto log = false;
log_if_macro(log, expensive_function_call());
log_if(log, expensive_function_call()); Here, the templated version doesn't prevent calling Maybe a solution would be to hand a lambda and the lambda only gets called if the condition is met. Condition could be the log level filter. But my guess is that for >99% of the code where we want to log, the arguments are cheap to compute. |
There's also the But I agree that we should limit the use cases for macros to the minimum. I think we maybe should adopt the builder pattern to add metadata to a log: // Simple usage:
log(info, "Hello world!").end()
// Complex usage:
log(error, "String={} Int={}", "Hello", 42).ctx("http.get.resource").dataSet(data_set).stopId(stop_id).end() What do you think of this syntax/usage? |
Thank you for accepting my contribution |
If you configure GithHub Pages, the documentation should appear at https://motis-project.github.io/utl/: GitHub Actions pipeline: https://github.com/motis-project/utl/actions/runs/12795986486 ✅ |
The documentation is currently generated in the Maybe that's what missing? 🙂 |
I think another way would be to not use the |
Regarding logging API this would work without macros. https://gcc.godbolt.org/z/hfKhMTq8x #include <source_location>
#include <iostream>
#include <iomanip>
#include <initializer_list>
#include <cinttypes>
#include "fmt/core.h"
#include "fmt/ostream.h"
#include "fmt/ranges.h"
#include "fmt/std.h"
namespace utl {
enum class log_level { debug, info, error };
log_level s_verbosity;
constexpr char const* to_str(log_level const level) {
switch (level) {
case log_level::debug: return "debug";
case log_level::info: return "info";
case log_level::error: return "error";
}
return "";
}
inline std::string now() {
using clock = std::chrono::system_clock;
auto const now = clock::to_time_t(clock::now());
struct tm tmp {};
#if _MSC_VER >= 1400
gmtime_s(&tmp, &now);
#else
gmtime_r(&now, &tmp);
#endif
std::stringstream ss;
ss << std::put_time(&tmp, "%FT%TZ");
return ss.str();
}
template <typename... Args>
struct log {
log(log_level const level, char const* ctx, fmt::format_string<Args...> fmt_str, Args&&... args,
std::source_location const& loc = std::source_location::current())
: level_{level},
ctx_{ctx},
args_{fmt_str, std::forward<Args>(args)...},
loc_{loc}
{}
~log() {
if (level_ >= utl::s_verbosity) {
fmt::print(std::clog, "{time} [{level}] [{file}:{line} {fn}] [{ctx}] {msg} {params}\n",
fmt::arg("file", loc_.file_name()),
fmt::arg("line", loc_.line()),
fmt::arg("fn", loc_.function_name()),
fmt::arg("time", now()),
fmt::arg("level", to_str(level_)),
fmt::arg("ctx", ctx_),
fmt::arg("msg", std::apply([](fmt::format_string<Args...> fmt, Args... args) { return fmt::format(fmt, args...); }, args_)),
fmt::arg("params", params_));
}
}
void params(std::initializer_list<std::pair<std::string_view, std::string_view>>&& params) {
params_ = std::move(params);
}
log_level level_;
char const* ctx_;
std::tuple<fmt::format_string<Args...> , Args...> args_;
std::source_location loc_;
std::initializer_list<std::pair<std::string_view, std::string_view>> params_;
};
template <typename... Args>
log(log_level, char const*, fmt::format_string<Args...>, Args&&... args) -> log<Args...>;
} // namespace utl
int main() {
auto dataset = "test";
utl::log(utl::log_level::info, "nigiri.loader", "starting import: dataset={}", dataset).params({{"dataset", dataset}});
} Having a global macro called log and logF is essentially asking for trouble (collisions with logarithm function, other logging functions, etc.). So for macros, they should be prefixed with something like The log level severity probably has to be |
I just had a look and the documentation looks promising. Just a few questions, as I haven't used GitHub Pages much, yet.
|
There is indeed something missing, the docs are hosted there for now: I'm drafting a PR to fix this right now.
You mean the emojis at the end of the step names? From my humble experience, yes they are common in several GitHub projects 🙂 |
PR submitted: #26 |
Agreed. If that's OK with you, I'll suggest an improved API in another PR tomorrow, |
Except for the convenience functions, my implementation from #25 (comment) should be fine (refinements can of course be made if we hit problems in nigiri, osr, etc.).
|
Thank you for the reply. Great to see it working already. |
Yes, this seems promising! I have a few questions (some may be obvious to you, but I do not often code in C++ 😅) :
|
Don't worry. C++ is a bit tricky but you'll get used to it by reading and writing more and more of it (like with any other programming language). |
Thank you for your answers 👍
Yes, I agree that the final log line needs to be assembled in the destructor. fmt::arg("msg", std::apply([](fmt::format_string<Args...> fmt, Args... args) { return fmt::format(fmt, args...); }, args_)), Couldn't be done in the constructor? I opened PR #27, but I have trouble pleasing the C++ compiler... error: class template argument deduction failed:
9 | log(llvl::info, "[{}] starting", name);
| ^
src/timer.cc:9:40: error: no matching function for call to ‘log(utl::llvl, const char [14], std::string&)’
include/utl/logging.h:51:5: note: candidate: ‘log(utl::llvl, fmt::v10::format_string<T ...>, const T&& ...)-> utl::log<T> [with T = {std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >}; fmt::v10::format_string<T ...> = fmt::v10::basic_format_string<char, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >]’ (near match)
51 | log(llvl const level, fmt::format_string<T...> fmt_str, const T&&... args)
| ^~~
include/utl/logging.h:51:5: note: conversion of argument 3 would be ill-formed:
src/timer.cc:9:36: error: cannot bind rvalue reference of type ‘const std::__cxx11::basic_string<char>&&’ to lvalue of type ‘std::string’ {aka ‘std::__cxx11::basic_string<char>’}
9 | log(llvl::info, "[{}] starting", name);
| ^~~~ |
As it's done now, probably yes. But the best solution would be to do only one formatting call with all parameters to not have any allocations in our code at all. Allocations (especially many small allocations) make programs slow and lead to memory fragmentation. We try to prevent them where ever possible. So the end goal would be to have only one formatting call and probably reuse the buffer used for formatting (thread local). But let's keep it simple for now. Still it's probably better to keep the formatting calls together, so at one point we can merge them into one (and add buffer reuse + OTEL). |
Initiating some HTML documentation generation.
Based on this tutorial: Clear, Functional C++ Documentation with Sphinx + Breathe + Doxygen + CMake
Remains to be done:
.github/workflows/unix.yml
CMakeLists.txt
Preview