Skip to content
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

Add ENVOY_LOG_PERIODIC #12961

Merged
merged 7 commits into from
Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions source/common/common/logger.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <bitset>
#include <chrono>
#include <cstdint>
#include <memory>
#include <string>
Expand Down Expand Up @@ -403,9 +404,11 @@ template <Id id> class Loggable {

#define ENVOY_LOG_FIRST_N(LEVEL, N, ...) \
do { \
static auto* countdown = new std::atomic<uint64_t>(); \
if (countdown->fetch_add(1) < N) { \
ENVOY_LOG(LEVEL, ##__VA_ARGS__); \
if (ENVOY_LOG_COMP_LEVEL(ENVOY_LOGGER(), LEVEL)) { \
static auto* countdown = new std::atomic<uint64_t>(); \
oschaaf marked this conversation as resolved.
Show resolved Hide resolved
if (countdown->fetch_add(1) < N) { \

Choose a reason for hiding this comment

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

The counter may eventually overflow. ;)

ENVOY_LOG(LEVEL, ##__VA_ARGS__); \
} \
} \
} while (0)

Expand All @@ -416,17 +419,40 @@ template <Id id> class Loggable {

#define ENVOY_LOG_EVERY_NTH(LEVEL, N, ...) \
do { \
static auto* count = new std::atomic<uint64_t>(); \
if ((count->fetch_add(1) % N) == 0) { \
ENVOY_LOG(LEVEL, ##__VA_ARGS__); \
if (ENVOY_LOG_COMP_LEVEL(ENVOY_LOGGER(), LEVEL)) { \
static auto* count = new std::atomic<uint64_t>(); \
if ((count->fetch_add(1) % N) == 0) { \
ENVOY_LOG(LEVEL, ##__VA_ARGS__); \
} \
} \
} while (0)

#define ENVOY_LOG_EVERY_POW_2(LEVEL, ...) \
do { \
static auto* count = new std::atomic<uint64_t>(); \
if (std::bitset<64>(1 + count->fetch_add(1)).count() == 1) { \
ENVOY_LOG(LEVEL, ##__VA_ARGS__); \
if (ENVOY_LOG_COMP_LEVEL(ENVOY_LOGGER(), LEVEL)) { \
static auto* count = new std::atomic<uint64_t>(); \
if (std::bitset<64>(1 + count->fetch_add(1)).count() == 1) { \

Choose a reason for hiding this comment

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

How about this kind of thing:

auto bits = count->fetch_add(1);
if ( bits && !(bits & (bits-1)) )

The unsigned type here is fine.
Any bitwise && of any power of 2 with a number less by 1 will give 0. No need to involve std::bitset and we get rid of that magic number too.

Copy link
Member Author

@oschaaf oschaaf Sep 4, 2020

Choose a reason for hiding this comment

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

I went with std::bitset because I think I remember it being compiled to something fast under the hood (__builtin_popcount?); but also - and this is probably subjective - I find it easier to grok. The +1 deserves a comment, e.g. // +1 because the first hit needs to be logged, but this imho this also wouldn't hurt with the -1 (or maybe the whole expression) in the variant you propose, depending on who the audience is?

Choose a reason for hiding this comment

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

Ok. Let's leave it be for now, no problemo. bitset.count() is readable, true.
Yes, __builtin_popcount for GCC and for LLVM it's a similar thing; which translates, depending on the platform, into about 2-3 cpu intructions, or into Kernighan’s algorithm (latter is slower than this little trick).

But you're right. To follow readibility guidelines i should've suggested a lambda at least here, sth like auto is_exactly_one_bit_set = [](const uint64_t bits) { return bits && !(bits & (bits-1)); };.

ENVOY_LOG(LEVEL, ##__VA_ARGS__); \
} \
} \
} while (0)

// This is to get us to pass the format check. We reference a real-world time source here.
// We'd have to introduce a singleton for a time source here, and consensus was that avoiding
// that is preferable.
using t_logclock = std::chrono::steady_clock; // NOLINT

#define ENVOY_LOG_PERIODIC(LEVEL, CHRONO_DURATION, ...) \
do { \
if (ENVOY_LOG_COMP_LEVEL(ENVOY_LOGGER(), LEVEL)) { \
static auto* last_hit = new std::atomic<uint64_t>(); \
auto last = last_hit->load(); \
const auto now = t_logclock::now().time_since_epoch().count(); \
if ((now - last) > \
oschaaf marked this conversation as resolved.
Show resolved Hide resolved
std::chrono::duration_cast<std::chrono::nanoseconds>(CHRONO_DURATION).count() && \
last_hit->compare_exchange_strong(last, now)) { \
ENVOY_LOG(LEVEL, ##__VA_ARGS__); \
} \
} \
} while (0)

Expand Down
22 changes: 18 additions & 4 deletions test/common/common/log_macros_test.cc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <chrono>
#include <functional>
#include <iostream>
#include <string>
Expand All @@ -15,6 +16,8 @@

namespace Envoy {

using namespace std::chrono_literals;

class TestFilterLog : public Logger::Loggable<Logger::Id::filter> {
public:
void logMessage() {
Expand Down Expand Up @@ -148,6 +151,7 @@ TEST(Logger, SparseLogMacros) {
void logSomethingThrice() { ENVOY_LOG_FIRST_N(error, 3, "foo4 '{}'", evaluations()++); }
void logEverySeventh() { ENVOY_LOG_EVERY_NTH(error, 7, "foo5 '{}'", evaluations()++); }
void logEveryPow2() { ENVOY_LOG_EVERY_POW_2(error, "foo6 '{}'", evaluations()++); }
void logEverySecond() { ENVOY_LOG_PERIODIC(error, 1s, "foo7 '{}'", evaluations()++); }
std::atomic<int32_t>& evaluations() { MUTABLE_CONSTRUCT_ON_FIRST_USE(std::atomic<int32_t>); };
};
constexpr uint32_t kNumThreads = 100;
Expand Down Expand Up @@ -185,11 +189,21 @@ TEST(Logger, SparseLogMacros) {
// We should log on 2, 4, 8, 16, 32, 64, which means we can expect to add 6 more evaluations.
EXPECT_EQ(27, helper.evaluations());

spamCall([&helper]() { helper.logEverySecond(); }, kNumThreads);
// First call ought to evaluate.
EXPECT_EQ(28, helper.evaluations());

// We expect one log entry / second. Therefore each spamCall ought to result in one
// more evaluation. This depends on real time and not sim time, hopefully 1 second
// is enough to not introduce flakes in practice.
sleep(1);
spamCall([&helper]() { helper.logEverySecond(); }, kNumThreads);
EXPECT_EQ(29, helper.evaluations());

spamCall([&helper]() { helper.logSomethingBelowLogLevelOnce(); }, kNumThreads);
// Without fine-grained logging, we shouldn't observe additional argument evaluations
// for log lines below the configured log level.
// TODO(#12885): fancy logger shouldn't always evaluate variadic macro arguments.
EXPECT_EQ(::Envoy::Logger::Context::useFancyLogger() ? 28 : 27, helper.evaluations());
// We shouldn't observe additional argument evaluations for log lines below the configured
// log level.
EXPECT_EQ(29, helper.evaluations());
}

TEST(RegistryTest, LoggerWithName) {
Expand Down