342 lines
11 KiB
C++
342 lines
11 KiB
C++
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#include <folly/logging/CustomLogFormatter.h>
|
|
|
|
#include <algorithm>
|
|
|
|
#include <folly/Format.h>
|
|
#include <folly/logging/LogLevel.h>
|
|
#include <folly/logging/LogMessage.h>
|
|
#include <folly/portability/Time.h>
|
|
|
|
namespace {
|
|
using folly::LogLevel;
|
|
using folly::StringPiece;
|
|
|
|
StringPiece getGlogLevelName(LogLevel level) {
|
|
if (level < LogLevel::INFO) {
|
|
return "VERBOSE";
|
|
} else if (level < LogLevel::WARN) {
|
|
return "INFO";
|
|
} else if (level < LogLevel::ERR) {
|
|
return "WARNING";
|
|
} else if (level < LogLevel::CRITICAL) {
|
|
return "ERROR";
|
|
} else if (level < LogLevel::DFATAL) {
|
|
return "CRITICAL";
|
|
}
|
|
return "FATAL";
|
|
}
|
|
|
|
StringPiece getResetSequence(LogLevel level) {
|
|
if (level < LogLevel::INFO || level >= LogLevel::WARN) {
|
|
return "\033[0m";
|
|
} else {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
StringPiece getColorSequence(LogLevel level) {
|
|
if (level < LogLevel::INFO) {
|
|
return "\033[1;30m"; // BOLD/BRIGHT BLACK ~ GREY
|
|
} else if (level < LogLevel::WARN) {
|
|
return ""; // NO COLOR
|
|
} else if (level < LogLevel::ERR) {
|
|
return "\033[33m"; // YELLOW
|
|
} else if (level < LogLevel::CRITICAL) {
|
|
return "\033[31m"; // RED
|
|
}
|
|
return "\033[1;41m"; // BOLD ON RED BACKGROUND
|
|
}
|
|
|
|
struct FormatKeys {
|
|
const StringPiece key;
|
|
const std::size_t argIndex;
|
|
const std::size_t width;
|
|
|
|
constexpr FormatKeys(
|
|
StringPiece key_, std::size_t argIndex_, std::size_t width_ = 0)
|
|
: key(key_), argIndex(argIndex_), width(width_) {}
|
|
};
|
|
|
|
/**
|
|
* The first part of pairs in this array are the key names and the second part
|
|
* of the pairs are the argument index for folly::format().
|
|
*
|
|
* NOTE: This array must be sorted by key name, since we use std::lower_bound
|
|
* to search in it.
|
|
*
|
|
* TODO: Support including thread names and thread context info.
|
|
*/
|
|
constexpr std::array<FormatKeys, 12> formatKeys{{
|
|
FormatKeys(/* key */ "CTX", /* argIndex */ 11),
|
|
FormatKeys(/* key */ "D", /* argIndex */ 2, /* width */ 2),
|
|
FormatKeys(/* key */ "FILE", /* argIndex */ 8),
|
|
FormatKeys(/* key */ "FUN", /* argIndex */ 9),
|
|
FormatKeys(/* key */ "H", /* argIndex */ 3, /* width */ 2),
|
|
FormatKeys(/* key */ "L", /* argIndex */ 0, /* width */ 1),
|
|
FormatKeys(/* key */ "LINE", /* argIndex */ 10, /* width */ 4),
|
|
FormatKeys(/* key */ "M", /* argIndex */ 4, /* width */ 2),
|
|
FormatKeys(/* key */ "S", /* argIndex */ 5, /* width */ 2),
|
|
FormatKeys(/* key */ "THREAD", /* argIndex */ 7, /* width */ 5),
|
|
FormatKeys(/* key */ "USECS", /* argIndex */ 6, /* width */ 6),
|
|
FormatKeys(/* key */ "m", /* argIndex */ 1, /* width */ 2),
|
|
}};
|
|
constexpr size_t messageIndex = formatKeys.size();
|
|
|
|
} // namespace
|
|
|
|
namespace folly {
|
|
|
|
CustomLogFormatter::CustomLogFormatter(StringPiece format, bool colored)
|
|
: colored_(colored) {
|
|
parseFormatString(format);
|
|
}
|
|
|
|
void CustomLogFormatter::parseFormatString(StringPiece input) {
|
|
std::size_t estimatedWidth = 0;
|
|
functionNameCount_ = 0;
|
|
fileNameCount_ = 0;
|
|
// Replace all format keys to numbers to improve performance and to use
|
|
// varying value types (which is not possible using folly::vformat()).
|
|
std::string output;
|
|
output.reserve(input.size());
|
|
const char* varNameStart = nullptr;
|
|
|
|
enum StateEnum {
|
|
LITERAL,
|
|
FMT_NAME,
|
|
FMT_MODIFIERS,
|
|
} state = LITERAL;
|
|
|
|
for (const char* p = input.begin(); p < input.end(); ++p) {
|
|
switch (state) {
|
|
case LITERAL:
|
|
output.append(p, 1);
|
|
// In case of `{{` or `}}`, copy it as it is and only increment the
|
|
// estimatedWidth once as it will result to a single character in
|
|
// output.
|
|
if ((p + 1) != input.end() /* ensure not last character */ &&
|
|
(0 == memcmp(p, "}}", 2) || 0 == memcmp(p, "{{", 2))) {
|
|
output.append(p + 1, 1);
|
|
estimatedWidth++;
|
|
p++;
|
|
}
|
|
// If we see a single open curly brace, it denotes a start of a format
|
|
// name and so we change the state to FMT_NAME and do not increment
|
|
// estimatedWidth as it won't be in the output.
|
|
else if (*p == '{') {
|
|
varNameStart = p + 1;
|
|
state = FMT_NAME;
|
|
}
|
|
// In case it is just a regular literal, just increment estimatedWidth
|
|
// by one and move on to the next character.
|
|
else {
|
|
estimatedWidth++;
|
|
}
|
|
break;
|
|
// In case we have started processing a format name/key
|
|
case FMT_NAME:
|
|
// Unless it is the end of the format name/key, do nothing and scan over
|
|
// the name/key. When it is the end of the format name/key, look up
|
|
// the argIndex for it and replace the name/key with that index.
|
|
if (*p == ':' || *p == '}') {
|
|
StringPiece varName(varNameStart, p);
|
|
auto item = std::lower_bound(
|
|
formatKeys.begin(),
|
|
formatKeys.end(),
|
|
varName,
|
|
[](const auto& a, const auto& b) { return a.key < b; });
|
|
|
|
if (UNLIKELY(item == formatKeys.end() || item->key != varName)) {
|
|
throw std::runtime_error(folly::to<std::string>(
|
|
"unknown format argument \"", varName, "\""));
|
|
}
|
|
output.append(folly::to<std::string>(item->argIndex));
|
|
output.append(p, 1);
|
|
|
|
// Based on the format key, increment estimatedWidth with the
|
|
// estimate of how many characters long the value of the format key
|
|
// will be. If it is a FILE or a FUN, the width will be variable
|
|
// depending on the values of those fields.
|
|
estimatedWidth += item->width;
|
|
if (item->key == "FILE") {
|
|
fileNameCount_++;
|
|
} else if (item->key == "FUN") {
|
|
functionNameCount_++;
|
|
}
|
|
|
|
// Figure out if there are modifiers that follow the key or if we
|
|
// continue processing literals.
|
|
if (*p == ':') {
|
|
state = FMT_MODIFIERS;
|
|
} else {
|
|
state = LITERAL;
|
|
}
|
|
}
|
|
break;
|
|
// In case we have started processing a format modifier (after :)
|
|
case FMT_MODIFIERS:
|
|
// Modifiers are just copied as is and are not considered to determine
|
|
// the estimatedWidth.
|
|
output.append(p, 1);
|
|
if (*p == '}') {
|
|
state = LITERAL;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (state != LITERAL) {
|
|
throw std::runtime_error("unterminated format string");
|
|
}
|
|
// Append a single space after the header format if header is not empty.
|
|
if (!output.empty()) {
|
|
output.append(" ");
|
|
estimatedWidth++;
|
|
}
|
|
logFormat_ = output;
|
|
staticEstimatedWidth_ = estimatedWidth;
|
|
|
|
// populate singleLineLogFormat_ with the padded line format.
|
|
if (colored_) {
|
|
singleLineLogFormat_ = folly::to<std::string>(
|
|
"{",
|
|
messageIndex + 1,
|
|
"}",
|
|
logFormat_,
|
|
"{",
|
|
messageIndex,
|
|
"}{",
|
|
messageIndex + 2,
|
|
"}\n");
|
|
} else {
|
|
singleLineLogFormat_ =
|
|
folly::to<std::string>(logFormat_, "{", messageIndex, "}\n");
|
|
}
|
|
}
|
|
|
|
std::string CustomLogFormatter::formatMessage(
|
|
const LogMessage& message, const LogCategory* /* handlerCategory */) {
|
|
// Get the local time info
|
|
struct tm ltime;
|
|
auto timeSinceEpoch = message.getTimestamp().time_since_epoch();
|
|
auto epochSeconds =
|
|
std::chrono::duration_cast<std::chrono::seconds>(timeSinceEpoch);
|
|
std::chrono::microseconds usecs =
|
|
std::chrono::duration_cast<std::chrono::microseconds>(timeSinceEpoch) -
|
|
epochSeconds;
|
|
time_t unixTimestamp = epochSeconds.count();
|
|
if (!localtime_r(&unixTimestamp, <ime)) {
|
|
memset(<ime, 0, sizeof(ltime));
|
|
}
|
|
|
|
auto basename = message.getFileBaseName();
|
|
|
|
// Most common logs will be single line logs and so we can format the entire
|
|
// log string including the message at once.
|
|
if (!message.containsNewlines()) {
|
|
return folly::sformat(
|
|
singleLineLogFormat_,
|
|
getGlogLevelName(message.getLevel())[0],
|
|
ltime.tm_mon + 1,
|
|
ltime.tm_mday,
|
|
ltime.tm_hour,
|
|
ltime.tm_min,
|
|
ltime.tm_sec,
|
|
usecs.count(),
|
|
message.getThreadID(),
|
|
basename,
|
|
message.getFunctionName(),
|
|
message.getLineNumber(),
|
|
message.getContextString(),
|
|
// NOTE: THE FOLLOWING ARGUMENTS ALWAYS NEED TO BE THE LAST 3:
|
|
message.getMessage(),
|
|
// If colored logs are enabled, the singleLineLogFormat_ will contain
|
|
// placeholders for the color and the reset sequences. If not, then
|
|
// the following params will just be ignored by the folly::sformat().
|
|
getColorSequence(message.getLevel()),
|
|
getResetSequence(message.getLevel()));
|
|
}
|
|
// If the message contains multiple lines, ensure that the log header is
|
|
// prepended before each message line.
|
|
else {
|
|
const auto header = folly::sformat(
|
|
logFormat_,
|
|
getGlogLevelName(message.getLevel())[0],
|
|
ltime.tm_mon + 1,
|
|
ltime.tm_mday,
|
|
ltime.tm_hour,
|
|
ltime.tm_min,
|
|
ltime.tm_sec,
|
|
usecs.count(),
|
|
message.getThreadID(),
|
|
basename,
|
|
message.getFunctionName(),
|
|
message.getLineNumber(),
|
|
message.getContextString());
|
|
|
|
// Estimate header length. If this still isn't long enough the string will
|
|
// grow as necessary, so the code will still be correct, but just slightly
|
|
// less efficient than if we had allocated a large enough buffer the first
|
|
// time around.
|
|
size_t headerLengthGuess = staticEstimatedWidth_ +
|
|
(fileNameCount_ * basename.size()) +
|
|
(functionNameCount_ * message.getFunctionName().size());
|
|
|
|
// Format the data into a buffer.
|
|
std::string buffer;
|
|
// If colored logging is supported, then process the color based on
|
|
// the level of the message.
|
|
if (colored_) {
|
|
buffer.append(getColorSequence(message.getLevel()).toString());
|
|
}
|
|
StringPiece msgData{message.getMessage()};
|
|
|
|
// Make a guess at how many lines will be in the message, just to make an
|
|
// initial buffer allocation. If the guess is too small then the string
|
|
// will reallocate and grow as necessary, it will just be slightly less
|
|
// efficient than if we had guessed enough space.
|
|
size_t numLinesGuess = 4;
|
|
buffer.reserve((headerLengthGuess * numLinesGuess) + msgData.size());
|
|
|
|
size_t idx = 0;
|
|
while (true) {
|
|
auto end = msgData.find('\n', idx);
|
|
if (end == StringPiece::npos) {
|
|
end = msgData.size();
|
|
}
|
|
|
|
auto line = msgData.subpiece(idx, end - idx);
|
|
buffer += header;
|
|
buffer.append(line.data(), line.size());
|
|
buffer.push_back('\n');
|
|
|
|
if (end == msgData.size()) {
|
|
break;
|
|
}
|
|
idx = end + 1;
|
|
}
|
|
// If colored logging is supported and the current message is a color other
|
|
// than the default, then RESET colors after printing message.
|
|
if (colored_) {
|
|
buffer.append(getResetSequence(message.getLevel()).toString());
|
|
}
|
|
return buffer;
|
|
}
|
|
}
|
|
} // namespace folly
|