Skip to content

Commit ed48065

Browse files
committed
Swift: add logging infrastructure
1 parent 6331c37 commit ed48065

File tree

8 files changed

+437
-12
lines changed

8 files changed

+437
-12
lines changed

swift/extractor/infra/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ swift_cc_library(
88
deps = [
99
"//swift/extractor/config",
1010
"//swift/extractor/infra/file",
11+
"//swift/extractor/infra/log",
1112
"//swift/extractor/trap",
1213
"//swift/third_party/swift-llvm-support",
1314
],

swift/extractor/infra/log/BUILD.bazel

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
cc_library(
2+
name = "log",
3+
srcs = glob(["*.cpp"]),
4+
hdrs = glob(["*.h"]),
5+
visibility = ["//visibility:public"],
6+
deps = ["@binlog"],
7+
)
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#include "swift/extractor/infra/log/SwiftLogging.h"
2+
3+
#include <filesystem>
4+
#include <stdlib.h>
5+
#include <optional>
6+
7+
#define LEVEL_REGEX_PATTERN "trace|debug|info|warning|error|critical|no_logs"
8+
9+
BINLOG_ADAPT_ENUM(codeql::Log::Level, trace, debug, info, warning, error, critical, no_logs)
10+
11+
namespace codeql {
12+
13+
namespace {
14+
using LevelRule = std::pair<std::regex, Log::Level>;
15+
using LevelRules = std::vector<LevelRule>;
16+
17+
Log::Level getLevelFor(std::string_view name, const LevelRules& rules, Log::Level dflt) {
18+
for (auto it = rules.rbegin(); it != rules.rend(); ++it) {
19+
if (std::regex_match(std::begin(name), std::end(name), it->first)) {
20+
return it->second;
21+
}
22+
}
23+
return dflt;
24+
}
25+
26+
const char* getEnvOr(const char* var, const char* deflt) {
27+
if (const char* ret = getenv(var)) {
28+
return ret;
29+
}
30+
return deflt;
31+
}
32+
33+
std::string_view matchToView(std::csub_match m) {
34+
return {m.first, static_cast<size_t>(m.length())};
35+
}
36+
37+
Log::Level stringToLevel(std::string_view v) {
38+
if (v == "trace") return Log::Level::trace;
39+
if (v == "debug") return Log::Level::debug;
40+
if (v == "info") return Log::Level::info;
41+
if (v == "warning") return Log::Level::warning;
42+
if (v == "error") return Log::Level::error;
43+
if (v == "critical") return Log::Level::critical;
44+
return Log::Level::no_logs;
45+
}
46+
47+
Log::Level matchToLevel(std::csub_match m) {
48+
return stringToLevel(matchToView(m));
49+
}
50+
51+
struct LevelConfiguration {
52+
LevelRules& sourceRules;
53+
Log::Level& binSeverity;
54+
Log::Level& textSeverity;
55+
Log::Level& consoleSeverity;
56+
std::vector<std::string>& problems;
57+
};
58+
59+
void collectSeverityRules(const char* var, LevelConfiguration&& configuration) {
60+
if (auto levels = getEnvOr(var, nullptr)) {
61+
// expect comma-separated <glob pattern>:<log severity>
62+
std::regex comma{","};
63+
std::regex levelAssignment{R"((?:([*./\w]+)|(?:out:(bin|text|console))):()" LEVEL_REGEX_PATTERN
64+
")"};
65+
std::cregex_token_iterator begin{levels, levels + strlen(levels), comma, -1};
66+
std::cregex_token_iterator end{};
67+
for (auto it = begin; it != end; ++it) {
68+
std::cmatch match;
69+
if (std::regex_match(it->first, it->second, match, levelAssignment)) {
70+
auto level = matchToLevel(match[3]);
71+
if (match[1].matched) {
72+
auto pattern = match[1].str();
73+
// replace all "*" with ".*" and all "." with "\.", turning the glob pattern into a regex
74+
std::string::size_type pos = 0;
75+
while ((pos = pattern.find_first_of("*.", pos)) != std::string::npos) {
76+
pattern.insert(pos, (pattern[pos] == '*') ? "." : "\\");
77+
pos += 2;
78+
}
79+
configuration.sourceRules.emplace_back(pattern, level);
80+
} else {
81+
auto out = matchToView(match[2]);
82+
if (out == "bin") {
83+
configuration.binSeverity = level;
84+
} else if (out == "text") {
85+
configuration.textSeverity = level;
86+
} else if (out == "console") {
87+
configuration.consoleSeverity = level;
88+
}
89+
}
90+
} else {
91+
configuration.problems.emplace_back("Malformed log level rule: " + it->str());
92+
}
93+
}
94+
}
95+
}
96+
97+
} // namespace
98+
99+
void Log::configure(std::string_view root) {
100+
auto& i = instance();
101+
102+
i.rootName = root;
103+
// as we are configuring logging right now, we collect problems and log them at the end
104+
std::vector<std::string> problems;
105+
collectSeverityRules("CODEQL_EXTRACTOR_SWIFT_LOG_LEVELS",
106+
{i.sourceRules, i.binary.level, i.text.level, i.console.level, problems});
107+
if (i.text || i.binary) {
108+
std::filesystem::path logFile = getEnvOr("CODEQL_EXTRACTOR_SWIFT_LOG_DIR", ".");
109+
logFile /= root;
110+
logFile /= std::to_string(std::chrono::system_clock::now().time_since_epoch().count());
111+
std::error_code ec;
112+
std::filesystem::create_directories(logFile.parent_path(), ec);
113+
if (!ec) {
114+
if (i.text) {
115+
logFile.replace_extension(".log");
116+
i.textFile.open(logFile);
117+
if (!i.textFile) {
118+
problems.emplace_back("Unable to open text log file " + logFile.string());
119+
i.text.level = Level::no_logs;
120+
}
121+
}
122+
if (i.binary) {
123+
logFile.replace_extension(".blog");
124+
i.binary.output.open(logFile, std::fstream::out | std::fstream::binary);
125+
if (!i.binary.output) {
126+
problems.emplace_back("Unable to open binary log file " + logFile.string());
127+
i.binary.level = Level::no_logs;
128+
}
129+
}
130+
} else {
131+
problems.emplace_back("Unable to create log directory " + logFile.parent_path().string() +
132+
": " + ec.message());
133+
i.binary.level = Level::no_logs;
134+
i.text.level = Level::no_logs;
135+
}
136+
}
137+
for (const auto& problem : problems) {
138+
LOG_ERROR("{}", problem);
139+
}
140+
LOG_INFO("Logging configured (binary: {}, text: {}, console: {})", i.binary.level, i.text.level,
141+
i.console.level);
142+
flush();
143+
}
144+
145+
void Log::flush() {
146+
auto& i = instance();
147+
i.session.consume(i);
148+
}
149+
150+
Log& Log::write(const char* buffer, std::streamsize size) {
151+
if (text) text.write(buffer, size);
152+
if (binary) binary.write(buffer, size);
153+
if (console) console.write(buffer, size);
154+
return *this;
155+
}
156+
157+
Log::Level Log::getLevelForSource(std::string_view name) const {
158+
auto dflt = std::min({binary.level, text.level, console.level});
159+
auto level = getLevelFor(name, sourceRules, dflt);
160+
// avoid Log::logger() constructor loop
161+
if (name.size() > rootName.size() && name.substr(rootName.size() + 1) != "logging") {
162+
LOG_DEBUG("setting up logger \"{}\" with level {}", name, level);
163+
}
164+
return level;
165+
}
166+
167+
Logger& Log::logger() {
168+
static Logger ret{"logging"};
169+
return ret;
170+
}
171+
172+
void Logger::setName(std::string name) {
173+
level_ = Log::instance().getLevelForSource(name);
174+
w.setName(std::move(name));
175+
}
176+
177+
Logger& logger() {
178+
static Logger ret{};
179+
return ret;
180+
}
181+
} // namespace codeql
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#pragma once
2+
3+
#include <fstream>
4+
#include <iostream>
5+
#include <regex>
6+
#include <vector>
7+
8+
#include <binlog/binlog.hpp>
9+
#include <binlog/TextOutputStream.hpp>
10+
#include <binlog/EventFilter.hpp>
11+
12+
// Logging macros. These will call `logger()` to get a Logger instance, picking up any `logger`
13+
// defined in the current scope. A default `codeql::logger()` is provided, otherwise domain-specific
14+
// loggers can be added as class fields called `logger` (as `Logger::operator()()` returns itself).
15+
// Domain specific loggers are set up with a name that appears in the log and can be used to filter
16+
// debug levels (see `Logger`). If working in the global namespace, the default logger can be used
17+
// by defining `auto& logger = codeql::logger();`
18+
#define LOG_CRITICAL(...) LOG_IMPL(codeql::Log::Level::critical, __VA_ARGS__)
19+
#define LOG_ERROR(...) LOG_IMPL(codeql::Log::Level::error, __VA_ARGS__)
20+
#define LOG_WARNING(...) LOG_IMPL(codeql::Log::Level::warning, __VA_ARGS__)
21+
#define LOG_INFO(...) LOG_IMPL(codeql::Log::Level::info, __VA_ARGS__)
22+
#define LOG_DEBUG(...) LOG_IMPL(codeql::Log::Level::debug, __VA_ARGS__)
23+
#define LOG_TRACE(...) LOG_IMPL(codeql::Log::Level::trace, __VA_ARGS__)
24+
25+
// avoid calling into binlog's original macros
26+
#undef BINLOG_CRITICAL
27+
#undef BINLOG_CRITICAL_W
28+
#undef BINLOG_CRITICAL_C
29+
#undef BINLOG_CRITICAL_WC
30+
#undef BINLOG_ERROR
31+
#undef BINLOG_ERROR_W
32+
#undef BINLOG_ERROR_C
33+
#undef BINLOG_ERROR_WC
34+
#undef BINLOG_WARNING
35+
#undef BINLOG_WARNING_W
36+
#undef BINLOG_WARNING_C
37+
#undef BINLOG_WARNING_WC
38+
#undef BINLOG_INFO
39+
#undef BINLOG_INFO_W
40+
#undef BINLOG_INFO_C
41+
#undef BINLOG_INFO_WC
42+
#undef BINLOG_DEBUG
43+
#undef BINLOG_DEBUG_W
44+
#undef BINLOG_DEBUG_C
45+
#undef BINLOG_DEBUG_WC
46+
#undef BINLOG_TRACE
47+
#undef BINLOG_TRACE_W
48+
#undef BINLOG_TRACE_C
49+
#undef BINLOG_TRACE_WC
50+
51+
// only do the actual logging if the picked up `Logger` instance is configured to handle the
52+
// provided log level
53+
#define LOG_IMPL(severity, ...) \
54+
do { \
55+
if (auto& _logger = logger(); severity >= _logger.level()) { \
56+
BINLOG_CREATE_SOURCE_AND_EVENT(_logger.writer(), severity, , binlog::clockNow(), \
57+
__VA_ARGS__); \
58+
} \
59+
} while (false)
60+
61+
namespace codeql {
62+
63+
// This class is responsible for the global log state (outputs, log level rules, flushing)
64+
// State is stored in the singleton `Log::instance()`.
65+
// Before using logging, `Log::configure("<name>")` should be used (e.g.
66+
// `Log::configure("extractor")`). Then, `Log::flush()` should be regularly called.
67+
class Log {
68+
public:
69+
using Level = binlog::Severity;
70+
71+
private:
72+
// Output filtered according to a configured log level
73+
template <typename Output>
74+
struct FilteredOutput {
75+
binlog::Severity level;
76+
Output output;
77+
binlog::EventFilter filter{
78+
[this](const binlog::EventSource& src) { return src.severity >= level; }};
79+
80+
template <typename... Args>
81+
FilteredOutput(Level level, Args&&... args)
82+
: level{level}, output{std::forward<Args>(args)...} {}
83+
84+
FilteredOutput& write(const char* buffer, std::streamsize size) {
85+
filter.writeAllowed(buffer, size, output);
86+
return *this;
87+
}
88+
89+
// if configured as `no_logs`, the output is effectively disabled
90+
explicit operator bool() const { return level < Level::no_logs; }
91+
};
92+
93+
using LevelRule = std::pair<std::regex, Level>;
94+
using LevelRules = std::vector<LevelRule>;
95+
96+
static constexpr const char* format = "%u %S [%n] %m (%G:%L)\n";
97+
98+
binlog::Session session;
99+
std::ofstream textFile;
100+
FilteredOutput<std::ofstream> binary{Level::no_logs};
101+
FilteredOutput<binlog::TextOutputStream> text{Level::info, textFile, format};
102+
FilteredOutput<binlog::TextOutputStream> console{Level::warning, std::cerr, format};
103+
LevelRules sourceRules;
104+
std::string rootName;
105+
106+
Log() = default;
107+
108+
static Log& instance() {
109+
static Log ret;
110+
return ret;
111+
}
112+
113+
friend class Logger;
114+
friend binlog::Session;
115+
116+
Level getLevelForSource(std::string_view name) const;
117+
Log& write(const char* buffer, std::streamsize size);
118+
static class Logger& logger();
119+
120+
public:
121+
// Configure logging. This consists in
122+
// * setting up a default logger with `root` as name
123+
// * using environment variable `CODEQL_EXTRACTOR_SWIFT_LOG_DIR` to choose where to dump the log
124+
// file(s). Log files will go to a subdirectory thereof named after `root`
125+
// * using environment variable `CODEQL_EXTRACTOR_SWIFT_LOG_LEVELS` to configure levels for
126+
// loggers and outputs. This must have the form of a comma separated `spec:level` list, where
127+
// `spec` is either a glob pattern (made up of alphanumeric, `/`, `*` and `.` characters) for
128+
// matching logger names or one of `out:bin`, `out:text` or `out:console`.
129+
// Output default levels can be seen in the corresponding initializers above. By default, all
130+
// loggers are configured with the lowest output level
131+
static void configure(std::string_view root);
132+
133+
// Flush logs to the designated outputs
134+
static void flush();
135+
};
136+
137+
// This class represent a named domain-specific logger, responsible for pushing logs using the
138+
// underlying `binlog::SessionWriter` class. This has a configured log level, so that logs on this
139+
// `Logger` with a level lower than the configured one are no-ops.
140+
class Logger {
141+
binlog::SessionWriter w{Log::instance().session};
142+
Log::Level level_{Log::Level::no_logs};
143+
144+
void setName(std::string name);
145+
146+
friend Logger& logger();
147+
// constructor for the default `Logger`
148+
explicit Logger() { setName(Log::instance().rootName); }
149+
150+
public:
151+
explicit Logger(const std::string& name) { setName(Log::instance().rootName + '/' + name); }
152+
153+
binlog::SessionWriter& writer() { return w; }
154+
Log::Level level() const { return level_; }
155+
156+
// make defining a `Logger logger` field be equivalent to providing a `Logger& logger()` function
157+
// in order to be picked up by logging macros
158+
Logger& operator()() { return *this; }
159+
};
160+
161+
// default logger
162+
Logger& logger();
163+
164+
} // namespace codeql

swift/third_party/binlog/BUILD.bazel

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
cc_library(
2+
name = "binlog",
3+
hdrs = glob(["include/**/*.hpp"]),
4+
srcs = glob(["include/**/*.cpp"]),
5+
includes = ["include"],
6+
visibility = ["//visibility:public"],
7+
)
8+
9+
cc_binary(
10+
name = "bread",
11+
srcs = ["bin/bread.cpp", "bin/printers.hpp", "bin/printers.cpp", "bin/getopt.hpp"],
12+
deps = [":binlog"],
13+
)
14+
15+
cc_binary(
16+
name = "brecovery",
17+
srcs = ["bin/brecovery.cpp", "bin/getopt.hpp"],
18+
deps = [":binlog"],
19+
)

0 commit comments

Comments
 (0)