Skip to content

std::format instead of snprintf in logger.cpp#4767

Merged
AlanGriffiths merged 13 commits intocanonical:mainfrom
yotam-medini:stdfmt-4660-logger
Mar 23, 2026
Merged

std::format instead of snprintf in logger.cpp#4767
AlanGriffiths merged 13 commits intocanonical:mainfrom
yotam-medini:stdfmt-4660-logger

Conversation

@yotam-medini
Copy link
Copy Markdown
Contributor

Closes nothing

Related: #4660

Related to #4736

What's new?

std::format instead of snprintf in logger.cpp

How to test

Checklist

  • Tests added and pass
  • Adequate documentation added
  • (optional) Added Screenshots or videos

@yotam-medini yotam-medini requested a review from a team as a code owner March 11, 2026 14:39
@Saviq Saviq requested a review from Copilot March 11, 2026 16:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Mir’s core logger timestamp formatting to use C++ std::format/std::chrono instead of snprintf-based char buffers, aligning with the direction in issue #4660.

Changes:

  • Replace manual timespec/strftime/snprintf timestamp construction with std::chrono + std::format.
  • Add a fallback timestamp path when time zone formatting via zoned_time/current_zone() fails.
  • Build a preformatted "[timestamp] <severity>" prefix using std::format_to_n.

Comment thread src/core/logging/logger.cpp Outdated
Comment thread src/core/logging/logger.cpp Outdated
Comment thread src/core/logging/logger.cpp Outdated
Comment thread src/core/logging/logger.cpp Outdated
Comment thread src/core/logging/logger.cpp
Copy link
Copy Markdown
Contributor

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this idea originates from one of my comments, but I don't think that should be applied without regard to context.

Specifically, I'm not sure the change is well motivated here. The proposed code is harder to follow and adds complexity to widely used functionality. Is there anything it makes better?

Comment thread src/core/logging/logger.cpp Outdated
Comment thread src/core/logging/logger.cpp Outdated
Revert to UTC if current_zone() throws exception.
Comment thread src/core/logging/logger.cpp Outdated
Copy link
Copy Markdown
Contributor

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the change is well motivated here:

  • A significant part of the motivation to move away from snprintf() is the fragile nature of passing char*, size_t pairs. But with this proposal we still end up doing the same thing for std::format_to_n().

  • The proposed code is harder to follow and adds complexity to widely used functionality.

Is there anything it makes better?

@yotam-medini
Copy link
Copy Markdown
Contributor Author

yotam-medini commented Mar 18, 2026

Is there anything it makes better?

In my recent commit I tried to avoid explicit passing the size-limit
in "our" mir functions (via TimeStampBuffer).
As I understood from previous review comments, in ml::format_message
we want to avoid dynamic allocation, so exceptions will not be thrown.
Thus we cannot use std::format(...) and use automatic buffer on the stack.

If we want to move to modern C++ API as reasonably possible,
Can you suggest an alternative to C++ format-API?

Or, would you suggest to give up on eliminating snprintf
from ml::format_message(...) in logger.cpp?

@AlanGriffiths
Copy link
Copy Markdown
Contributor

Is there anything it makes better?

In my recent commit I tried to avoid explicit passing the size-limit in "our" mir functions (via TimeStampBuffer). As I understood from previous review comments, in ml::format_message we want to avoid dynamic allocation, so exceptions will not be thrown. Thus we cannot use std::format(...) and use automatic buffer on the stack.

Yes, we've explored alternatives: None of these appear to be a clear improvement to the existing code.

If we want to move to modern C++ API as reasonably possible, Can you suggest an alternative to C++ format-API?

We seem to have different objectives: I want to improve the code, you want to "move to modern C++ API as reasonably possible". While new APIs and features are, in general, good options they are not always an improvement.

Or, would you suggest to give up on eliminating snprintf from ml::format_message(...) in logger.cpp?

I'm open to being convinced some variant of this is an improvement. But:

  1. one call to snprintf() seems simpler than two calls to format_to_n() (and some logic);
  2. snprintf() and format_to_n() have the same memory safety issue; and,
  3. format(...) leads to potential allocations with possible performance or exceptions being raised.

@yotam-medini
Copy link
Copy Markdown
Contributor Author

yotam-medini commented Mar 18, 2026

Before coding.
How about avoiding char buffers and use something like:

   // no (char*,size) pairs, direct write to the stream
   auto iter = std::ostreambuf_iterator<char>(out);
   iter = std::format_to(iter, "{:%F %T}", ...);
   [iter =] std::format_to(iter, " {}  ...", severity, ...);
   ...
   out << std::endl; // for flush

While at it. When I tested the try/catch section in "my" format_timestamp(...)
I wanted to fail the current_zone() and have the exception caught.
By playing with env-vars ("TZ", "TZINFO"} or [re]moving system files
/etc/localtime, or under /usr/share/zoneinfo,
but current_zone() overcame all such "landmines".
Eventually, I just added temporary throw for testing.

How about breaking backward compatibility, and just use:
... , "{:%F %T} UTC", now_us)

Copy link
Copy Markdown
Contributor

@robert-ancell robert-ancell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've no further issues with this PR but I'll leave it to @AlanGriffiths for the final approval as this is using C++ APIs I'm not familiar with.

@AlanGriffiths
Copy link
Copy Markdown
Contributor

Before coding. How about avoiding char buffers and use something like:
...
iter = std::format_to(iter, "{:%F %T}", ...);

That would avoid both allocating a string and passing ..., buf, len, ...; and also looks like it will actually compile with g++-13. So that could actually be an improvement.

While at it. When I tested the try/catch section in "my" format_timestamp(...) I wanted to fail the current_zone() and have the exception caught.

Maybe that points to a design issue with format_timestamp(...)? The try-catch logic could, in principle, be separated out. But this logic is too elaborate: if the runtime cannot find any tzdb, the environment is likely toxic anyway. And throwing and catching an exception for every log message is expensive. Maybe:

void format_timestamp(...)
try
{
    ...
}
catch (std::runtime_error const& e)
{
    mir::fatal_error_abort("Cannot format timestamp: %s", e.what());
}

Copy link
Copy Markdown
Contributor

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks promising. Just needs some tidy-up. Something like:

    try
    {
        std::ostreambuf_iterator<char> iter{out};
        auto const now = std::chrono::system_clock::now();
        auto const now_us = std::chrono::time_point_cast<std::chrono::microseconds>(now);
        iter = std::format_to(iter, "[{:%F %T}]{}{}: {}\n",
            std::chrono::zoned_time{std::chrono::current_zone(), now_us},
            lut[static_cast<int>(severity)], component, message);
        out.flush();
    }
    catch (std::runtime_error const& e)
    {
        mir::fatal_error_abort("Cannot format log message: %s", e.what());
    }

Comment thread src/core/logging/logger.cpp Outdated
Comment on lines 26 to 27
#include <cstdio>
#include <ctime>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need cstdio or ctime anymore

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need , since logger.cpp still uses vsnprintf(...)
that is not addressed in this task.

Comment thread src/core/logging/logger.cpp Outdated
Comment on lines +78 to +95
namespace {

using OutIter = std::ostreambuf_iterator<char>;

OutIter format_timestamp(OutIter iter)
{
auto now = std::chrono::system_clock::now();
auto now_us = std::chrono::time_point_cast<std::chrono::microseconds>(now);
try {
auto local =
std::chrono::zoned_time{std::chrono::current_zone(), now_us};
iter = std::format_to(iter, "[{:%F %T}]", local);
} catch (std::runtime_error const& e) {
mir::fatal_error_abort("Cannot format timestamp: %s", e.what());
}
return iter;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can all be inlined - it is simple enough to be part of ml::format_message()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inlined

Comment thread src/core/logging/logger.cpp Outdated
Comment on lines +115 to +117
iter = format_timestamp(iter);
format_to(iter, "{}{}: {}\n",
lut[static_cast<int>(severity)], component, message);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After inlining format_timestamp(), the format_to() statements can be combined into a single call.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

single call now.

Comment thread src/core/logging/logger.cpp Outdated
Comment on lines +86 to +92
try {
auto local =
std::chrono::zoned_time{std::chrono::current_zone(), now_us};
iter = std::format_to(iter, "[{:%F %T}]", local);
} catch (std::runtime_error const& e) {
mir::fatal_error_abort("Cannot format timestamp: %s", e.what());
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the style guide the braces should be on their own lines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In recent commit:
c9461d2 yotam.m inline format_timestamp, single std::format_to call(...)
I tried to follow your suggection.
Two remarks.

  1. My earlier separation of format_timestamp(...) was following
    Robert's suggesion from March 11.
  2. In general, don't we want to localize try/catch block
    to the smallest possible code, for which exception may be thrown?
    In this case: std::chrono::current_zone().

Copy link
Copy Markdown
Contributor

@AlanGriffiths AlanGriffiths Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. It is simpler now than when Robert suggested separating it
  2. That depends on how it affects the complexity of the code. We could write:
auto const local = []
        {
            try
            {
                return std::chrono::zoned_time{std::chrono::current_zone(), now_us};
            }
            catch (std::runtime_error const& e)
            {
                mir::fatal_error_abort("Cannot format log timestamp: %s", e.what());
            }
        }();

But that's an absurd extreme.

@yotam-medini
Copy link
Copy Markdown
Contributor Author

BTW, how can I run a single failed CI test. For example: the current single failed:
Spread / BuildAndTest (lxd:...:spread/ubuntu:asan) (pull_request)Failing after 4m
?

Copy link
Copy Markdown
Contributor

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A little tweaking, but looks good.

Maybe a strategic using namespace std::chrono; would allow us to reduce unnecessary verbosity?

Comment thread src/core/logging/logger.cpp Outdated
Comment on lines +96 to +104
std::ostreambuf_iterator<char> iter{out};
auto now = std::chrono::system_clock::now();
auto now_us =
std::chrono::time_point_cast<std::chrono::microseconds>(now);
auto local =
std::chrono::zoned_time{std::chrono::current_zone(), now_us};
std::format_to(iter, "[{:%F %T}] {}{}: {}\n",
local, lut[static_cast<int>(severity)], component, message);
out.flush();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't really need a name for iter:

Suggested change
std::ostreambuf_iterator<char> iter{out};
auto now = std::chrono::system_clock::now();
auto now_us =
std::chrono::time_point_cast<std::chrono::microseconds>(now);
auto local =
std::chrono::zoned_time{std::chrono::current_zone(), now_us};
std::format_to(iter, "[{:%F %T}] {}{}: {}\n",
local, lut[static_cast<int>(severity)], component, message);
out.flush();
auto now = std::chrono::system_clock::now();
auto now_us =
std::chrono::time_point_cast<std::chrono::microseconds>(now);
auto local =
std::chrono::zoned_time{std::chrono::current_zone(), now_us};
std::format_to(std::ostreambuf_iterator{out}, "[{:%F %T}] {}{}: {}\n",
local, lut[static_cast<int>(severity)], component, message);
out.flush();

Comment thread src/core/logging/logger.cpp Outdated
}
catch (std::runtime_error const& e)
{
mir::fatal_error_abort("Cannot format message: %s", e.what());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message is a little too generic: we ought to mention logging so that the reader has some idea where it comes from:

Suggested change
mir::fatal_error_abort("Cannot format message: %s", e.what());
mir::fatal_error_abort("Cannot format log message: %s", e.what());

Comment thread src/core/logging/logger.cpp Outdated
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include <mir/fatal.h>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Project standard is to begin a .cpp file with the corresponding .h (to ensure the .h has no hidden dependencies).

@AlanGriffiths
Copy link
Copy Markdown
Contributor

BTW, how can I run a single failed CI test. For example: the current single failed: Spread / BuildAndTest (lxd:...:spread/ubuntu:asan) (pull_request)Failing after 4m ?

I doubt you have the permissions to re-run CI tasks. In this case it looks like a network timeout, nothing to do with your changes.

Team members (like me) could re-run it, but as I've requested changes there's no point.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread src/core/logging/logger.cpp
Copy link
Copy Markdown
Contributor

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think we can inline iter, but ok

Comment thread src/core/logging/logger.cpp Outdated
auto now = system_clock::now();
auto now_us = time_point_cast<microseconds>(now);
auto local = zoned_time{current_zone(), now_us};
std::format_to(std::ostreambuf_iterator<char>(out),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's only six characters, but the compiler can deduce the template parameter.

Suggested change
std::format_to(std::ostreambuf_iterator<char>(out),
std::format_to(std::ostreambuf_iterator(out),

@AlanGriffiths AlanGriffiths enabled auto-merge March 23, 2026 09:55
Copy link
Copy Markdown
Contributor

@AlanGriffiths AlanGriffiths left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc check is failing because www.akkadia.org has dropped off the internet. (So non-blocking on this PR)

@yotam-medini
Copy link
Copy Markdown
Contributor Author

yotam-medini commented Mar 23, 2026

documentation-checks ... failed with:
matplotlib is not installed, social cards will not be generated
<rst_epilog>:2: CRITICAL: Problems with "include" directive path:
InputError: [Errno 2] No such file or directory: 'reuse/links.txt'.
<rst_epilog>:3: CRITICAL: Problems with "include" directive path:
InputError: [Errno 2] No such file or directory: 'reuse/substitutions.txt'.

Do I need to do anything to have this PR merged ?

@AlanGriffiths
Copy link
Copy Markdown
Contributor

Do I need to do anything to have this PR merged

No, I'm dealing with it

auto-merge was automatically disabled March 23, 2026 10:43

Head branch was pushed to by a user without write access

@AlanGriffiths AlanGriffiths merged commit f984ec7 into canonical:main Mar 23, 2026
50 of 52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants