Skip to content

Commit 8de2f99

Browse files
committed
Swift: add support to output JSON diagnostics
New `DIAGNOSE_ERROR` and `DIAGNOSE_CRITICAL` macros are added. These accept an ID which should indicate a diagnostic source via a function definition in `codeql::diagnostics`, together with the usual format + arguments accepted by other `LOG_*` macros. When the log is flushed, these special logs will result in an error JSON diagnostic entry in the database.
1 parent 0ad529d commit 8de2f99

6 files changed

Lines changed: 245 additions & 27 deletions

File tree

swift/extractor/main.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
using namespace std::string_literals;
2424

25-
const std::string_view codeql::logRootName = "extractor";
25+
const std::string_view codeql::programName = "extractor";
2626

2727
// must be called before processFrontendOptions modifies output paths
2828
static void lockOutputSwiftModuleTraps(codeql::SwiftExtractorState& state,
@@ -220,6 +220,7 @@ int main(int argc, char** argv, char** envp) {
220220
codeql::Logger logger{"main"};
221221
LOG_INFO("calling extractor with arguments \"{}\"", argDump(argc, argv));
222222
LOG_DEBUG("environment:\n{}\n", envDump(envp));
223+
DIAGNOSE_ERROR(internal_error, "prout {}", 42);
223224
}
224225

225226
auto openInterception = codeql::setupFileInterception(configuration);

swift/log/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ cc_library(
55
visibility = ["//visibility:public"],
66
deps = [
77
"@binlog",
8+
"@date",
89
"@json",
910
],
1011
)

swift/log/SwiftDiagnostics.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#include "swift/log/SwiftDiagnostics.h"
2+
3+
#include <date/date.h>
4+
#include <binlog/Entries.hpp>
5+
#include <nlohmann/json.hpp>
6+
7+
namespace codeql {
8+
SwiftDiagnosticsSource::SwiftDiagnosticsSource(std::string_view internalId,
9+
std::string&& name,
10+
std::vector<std::string>&& helpLinks,
11+
std::string&& action)
12+
: name{std::move(name)}, helpLinks{std::move(helpLinks)}, action{std::move(action)} {
13+
id = extractorName;
14+
id += '/';
15+
id += programName;
16+
id += '/';
17+
std::transform(internalId.begin(), internalId.end(), std::back_inserter(id),
18+
[](char c) { return c == '_' ? '-' : c; });
19+
}
20+
21+
void SwiftDiagnosticsSource::create(std::string_view id,
22+
std::string name,
23+
std::vector<std::string> helpLinks,
24+
std::string action) {
25+
auto [it, inserted] = map().emplace(
26+
id, SwiftDiagnosticsSource{id, std::move(name), std::move(helpLinks), std::move(action)});
27+
assert(inserted);
28+
}
29+
30+
void SwiftDiagnosticsSource::emit(std::ostream& out,
31+
std::string_view timestamp,
32+
std::string_view message) const {
33+
nlohmann::json entry;
34+
auto& source = entry["source"];
35+
source["id"] = id;
36+
source["name"] = name;
37+
source["extractorName"] = extractorName;
38+
39+
auto& visibility = entry["visibility"];
40+
visibility["statusPage"] = true;
41+
visibility["cliSummaryTable"] = true;
42+
visibility["telemetry"] = true;
43+
44+
entry["severity"] = "error";
45+
entry["helpLinks"] = helpLinks;
46+
std::string plaintextMessage{message};
47+
plaintextMessage += ".\n\n";
48+
plaintextMessage += action;
49+
plaintextMessage += '.';
50+
entry["plaintextMessage"] = plaintextMessage;
51+
52+
entry["timestamp"] = timestamp;
53+
54+
out << entry << '\n';
55+
}
56+
57+
void SwiftDiagnosticsDumper::write(const char* buffer, std::size_t bufferSize) {
58+
binlog::Range range{buffer, bufferSize};
59+
binlog::RangeEntryStream input{range};
60+
while (auto event = events.nextEvent(input)) {
61+
const auto& source = SwiftDiagnosticsSource::get(event->source->category);
62+
std::ostringstream oss;
63+
timestampedMessagePrinter.printEvent(oss, *event, events.writerProp(), events.clockSync());
64+
auto data = oss.str();
65+
std::string_view view = data;
66+
auto sep = view.find(' ');
67+
assert(sep != std::string::npos);
68+
auto timestamp = view.substr(0, sep);
69+
auto message = view.substr(sep + 1);
70+
source.emit(output, timestamp, message);
71+
}
72+
}
73+
} // namespace codeql

swift/log/SwiftDiagnostics.h

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#pragma once
2+
3+
#include <binlog/EventStream.hpp>
4+
#include <binlog/PrettyPrinter.hpp>
5+
#include <string>
6+
#include <vector>
7+
#include <unordered_map>
8+
#include <cassert>
9+
#include <fstream>
10+
#include <filesystem>
11+
12+
namespace codeql {
13+
14+
extern const std::string_view programName;
15+
16+
// Models a diagnostic source for Swift, holding static information that goes out into a diagnostic
17+
// These are internally stored into a map on id's. A specific error log can use binlog's category
18+
// as id, which will then be used to recover the diagnostic source while dumping.
19+
class SwiftDiagnosticsSource {
20+
public:
21+
// creates a SwiftDiagnosticsSource with the given data
22+
static void create(std::string_view id,
23+
std::string name,
24+
std::vector<std::string> helpLinks,
25+
std::string action);
26+
27+
// gets a previously created SwiftDiagnosticsSource for the given id. Will abort if none exists
28+
static const SwiftDiagnosticsSource& get(const std::string& id) { return map().at(id); }
29+
30+
// emit a JSON diagnostics for this source with the given timestamp and message to out
31+
// A plaintextMessage is used that includes both the message and the action to take. Dots are
32+
// appended to both. The id is used to construct the source id in the form
33+
// `swift/<prog name>/<id with '-' replacing '_'>`
34+
void emit(std::ostream& out, std::string_view timestamp, std::string_view message) const;
35+
36+
private:
37+
using Map = std::unordered_map<std::string, SwiftDiagnosticsSource>;
38+
39+
std::string id;
40+
std::string name;
41+
static constexpr std::string_view extractorName = "swift";
42+
43+
// for the moment, we only output errors, so no need to store the severity
44+
45+
std::vector<std::string> helpLinks;
46+
std::string action;
47+
48+
static Map& map() {
49+
static Map ret;
50+
return ret;
51+
}
52+
53+
SwiftDiagnosticsSource(std::string_view internalId,
54+
std::string&& name,
55+
std::vector<std::string>&& helpLinks,
56+
std::string&& action);
57+
};
58+
59+
// An output modeling binlog's output stream concept that intercepts binlog entries and translates
60+
// them to appropriate diagnostics JSON entries
61+
class SwiftDiagnosticsDumper {
62+
public:
63+
// opens path for writing out JSON entries. Returns whether the operation was successful.
64+
bool open(const std::filesystem::path& path) {
65+
output.open(path);
66+
return output.good();
67+
}
68+
69+
// write out binlog entries as corresponding JSON diagnostics entries. Expects all entries to have
70+
// a category equal to an id of a previously created SwiftDiagnosticSource.
71+
void write(const char* buffer, std::size_t bufferSize);
72+
73+
private:
74+
binlog::EventStream events;
75+
std::ofstream output;
76+
binlog::PrettyPrinter timestampedMessagePrinter{"%u %m", "%Y-%m-%dT%H:%M:%S.%NZ"};
77+
};
78+
79+
namespace diagnostics {
80+
inline void internal_error() {
81+
SwiftDiagnosticsSource::create("internal_error", "Internal error", {},
82+
"Contact us about this issue");
83+
}
84+
} // namespace diagnostics
85+
86+
} // namespace codeql

swift/log/SwiftLogging.cpp

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Log::Level matchToLevel(std::csub_match m) {
5050

5151
} // namespace
5252

53-
std::vector<std::string> Log::collectSeverityRulesAndReturnProblems(const char* envVar) {
53+
std::vector<std::string> Log::collectLevelRulesAndReturnProblems(const char* envVar) {
5454
std::vector<std::string> problems;
5555
if (auto levels = getEnvOr(envVar, nullptr)) {
5656
// expect comma-separated <glob pattern>:<log severity>
@@ -92,12 +92,13 @@ std::vector<std::string> Log::collectSeverityRulesAndReturnProblems(const char*
9292

9393
void Log::configure() {
9494
// as we are configuring logging right now, we collect problems and log them at the end
95-
auto problems = collectSeverityRulesAndReturnProblems("CODEQL_EXTRACTOR_SWIFT_LOG_LEVELS");
95+
auto problems = collectLevelRulesAndReturnProblems("CODEQL_EXTRACTOR_SWIFT_LOG_LEVELS");
96+
auto now = std::to_string(std::chrono::system_clock::now().time_since_epoch().count());
9697
if (text || binary) {
9798
std::filesystem::path logFile = getEnvOr("CODEQL_EXTRACTOR_SWIFT_LOG_DIR", "extractor-out/log");
9899
logFile /= "swift";
99-
logFile /= logRootName;
100-
logFile /= std::to_string(std::chrono::system_clock::now().time_since_epoch().count());
100+
logFile /= programName;
101+
logFile /= now;
101102
std::error_code ec;
102103
std::filesystem::create_directories(logFile.parent_path(), ec);
103104
if (!ec) {
@@ -123,6 +124,25 @@ void Log::configure() {
123124
binary.level = Level::no_logs;
124125
text.level = Level::no_logs;
125126
}
127+
if (diagnostics) {
128+
std::filesystem::path diagFile =
129+
getEnvOr("CODEQL_EXTRACTOR_SWIFT_DIAGNOSTIC_DIR", "extractor-out/diagnostics");
130+
diagFile /= programName;
131+
diagFile /= now;
132+
diagFile.replace_extension(".jsonl");
133+
std::error_code ec;
134+
std::filesystem::create_directories(diagFile.parent_path(), ec);
135+
if (!ec) {
136+
if (!diagnostics.output.open(diagFile)) {
137+
problems.emplace_back("Unable to open diagnostics json file " + diagFile.string());
138+
diagnostics.level = Level::no_logs;
139+
}
140+
} else {
141+
problems.emplace_back("Unable to create diagnostics directory " +
142+
diagFile.parent_path().string() + ": " + ec.message());
143+
diagnostics.level = Level::no_logs;
144+
}
145+
}
126146
}
127147
for (const auto& problem : problems) {
128148
LOG_ERROR("{}", problem);
@@ -137,7 +157,7 @@ void Log::flushImpl() {
137157
}
138158

139159
Log::LoggerConfiguration Log::getLoggerConfigurationImpl(std::string_view name) {
140-
LoggerConfiguration ret{session, std::string{logRootName}};
160+
LoggerConfiguration ret{session, std::string{programName}};
141161
ret.fullyQualifiedName += '/';
142162
ret.fullyQualifiedName += name;
143163
ret.level = std::min({binary.level, text.level, console.level});
@@ -153,6 +173,7 @@ Log& Log::write(const char* buffer, std::streamsize size) {
153173
if (text) text.write(buffer, size);
154174
if (binary) binary.write(buffer, size);
155175
if (console) console.write(buffer, size);
176+
if (diagnostics) diagnostics.write(buffer, size);
156177
return *this;
157178
}
158179

swift/log/SwiftLogging.h

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#include <binlog/adapt_stdoptional.hpp>
1313
#include <binlog/adapt_stdvariant.hpp>
1414

15+
#include "swift/log/SwiftDiagnostics.h"
16+
1517
// Logging macros. These will call `logger()` to get a Logger instance, picking up any `logger`
1618
// defined in the current scope. Domain-specific loggers can be added or used by either:
1719
// * providing a class field called `logger` (as `Logger::operator()()` returns itself)
@@ -20,23 +22,39 @@
2022
// * passing a logger around using a `Logger& logger` function parameter
2123
// They are created with a name that appears in the logs and can be used to filter debug levels (see
2224
// `Logger`).
23-
#define LOG_CRITICAL(...) LOG_WITH_LEVEL(codeql::Log::Level::critical, __VA_ARGS__)
24-
#define LOG_ERROR(...) LOG_WITH_LEVEL(codeql::Log::Level::error, __VA_ARGS__)
25-
#define LOG_WARNING(...) LOG_WITH_LEVEL(codeql::Log::Level::warning, __VA_ARGS__)
26-
#define LOG_INFO(...) LOG_WITH_LEVEL(codeql::Log::Level::info, __VA_ARGS__)
27-
#define LOG_DEBUG(...) LOG_WITH_LEVEL(codeql::Log::Level::debug, __VA_ARGS__)
28-
#define LOG_TRACE(...) LOG_WITH_LEVEL(codeql::Log::Level::trace, __VA_ARGS__)
25+
#define LOG_CRITICAL(...) LOG_WITH_LEVEL(critical, __VA_ARGS__)
26+
#define LOG_ERROR(...) LOG_WITH_LEVEL(error, __VA_ARGS__)
27+
#define LOG_WARNING(...) LOG_WITH_LEVEL(warning, __VA_ARGS__)
28+
#define LOG_INFO(...) LOG_WITH_LEVEL(info, __VA_ARGS__)
29+
#define LOG_DEBUG(...) LOG_WITH_LEVEL(debug, __VA_ARGS__)
30+
#define LOG_TRACE(...) LOG_WITH_LEVEL(trace, __VA_ARGS__)
2931

3032
// only do the actual logging if the picked up `Logger` instance is configured to handle the
3133
// provided log level. `LEVEL` must be a compile-time constant. `logger()` is evaluated once
32-
#define LOG_WITH_LEVEL(LEVEL, ...) \
33-
do { \
34-
constexpr codeql::Log::Level _level = LEVEL; \
35-
codeql::Logger& _logger = logger(); \
36-
if (_level >= _logger.level()) { \
37-
BINLOG_CREATE_SOURCE_AND_EVENT(_logger.writer(), _level, /* category */, binlog::clockNow(), \
38-
__VA_ARGS__); \
39-
} \
34+
#define LOG_WITH_LEVEL_AND_CATEGORY(LEVEL, CATEGORY, ...) \
35+
do { \
36+
constexpr codeql::Log::Level _level = codeql::Log::Level::LEVEL; \
37+
codeql::Logger& _logger = logger(); \
38+
if (_level >= _logger.level()) { \
39+
BINLOG_CREATE_SOURCE_AND_EVENT(_logger.writer(), _level, CATEGORY, binlog::clockNow(), \
40+
__VA_ARGS__); \
41+
} \
42+
} while (false)
43+
44+
#define LOG_WITH_LEVEL(LEVEL, ...) LOG_WITH_LEVEL_AND_CATEGORY(LEVEL, , __VA_ARGS__)
45+
46+
// Emit errors with a specified diagnostics ID. This must be the name of a function in the
47+
// codeql::diagnostics namespace, which must call SwiftDiagnosticSource::create with ID as first
48+
// argument. This function will be called at most once during the program execution.
49+
// See codeql::diagnostics::internal_error below as an example.
50+
#define DIAGNOSE_CRITICAL(ID, ...) DIAGNOSE_WITH_LEVEL(critical, ID, __VA_ARGS__)
51+
#define DIAGNOSE_ERROR(ID, ...) DIAGNOSE_WITH_LEVEL(error, ID, __VA_ARGS__)
52+
53+
#define DIAGNOSE_WITH_LEVEL(LEVEL, ID, ...) \
54+
do { \
55+
static int _ignore = (codeql::diagnostics::ID(), 0); \
56+
std::ignore = _ignore; \
57+
LOG_WITH_LEVEL_AND_CATEGORY(LEVEL, ID, __VA_ARGS__); \
4058
} while (false)
4159

4260
// avoid calling into binlog's original macros
@@ -68,15 +86,15 @@
6886
namespace codeql {
6987

7088
// tools should define this to tweak the root name of all loggers
71-
extern const std::string_view logRootName;
89+
extern const std::string_view programName;
7290

7391
// This class is responsible for the global log state (outputs, log level rules, flushing)
7492
// State is stored in the singleton `Log::instance()`.
7593
// Before using logging, `Log::configure("<name>")` should be used (e.g.
7694
// `Log::configure("extractor")`). Then, `Log::flush()` should be regularly called.
7795
// Logging is configured upon first usage. This consists in
7896
// * using environment variable `CODEQL_EXTRACTOR_SWIFT_LOG_DIR` to choose where to dump the log
79-
// file(s). Log files will go to a subdirectory thereof named after `logRootName`
97+
// file(s). Log files will go to a subdirectory thereof named after `programName`
8098
// * using environment variable `CODEQL_EXTRACTOR_SWIFT_LOG_LEVELS` to configure levels for
8199
// loggers and outputs. This must have the form of a comma separated `spec:level` list, where
82100
// `spec` is either a glob pattern (made up of alphanumeric, `/`, `*` and `.` characters) for
@@ -122,23 +140,40 @@ class Log {
122140
friend binlog::Session;
123141
Log& write(const char* buffer, std::streamsize size);
124142

143+
struct OnlyWithCategory {};
144+
125145
// Output filtered according to a configured log level
126146
template <typename Output>
127147
struct FilteredOutput {
128148
binlog::Severity level;
129149
Output output;
130-
binlog::EventFilter filter{
131-
[this](const binlog::EventSource& src) { return src.severity >= level; }};
150+
binlog::EventFilter filter;
132151

133152
template <typename... Args>
134153
FilteredOutput(Level level, Args&&... args)
135-
: level{level}, output{std::forward<Args>(args)...} {}
154+
: level{level}, output{std::forward<Args>(args)...}, filter{filterOnLevel()} {}
155+
156+
template <typename... Args>
157+
FilteredOutput(OnlyWithCategory, Level level, Args&&... args)
158+
: level{level},
159+
output{std::forward<Args>(args)...},
160+
filter{filterOnLevelAndNonEmptyCategory()} {}
136161

137162
FilteredOutput& write(const char* buffer, std::streamsize size) {
138163
filter.writeAllowed(buffer, size, output);
139164
return *this;
140165
}
141166

167+
binlog::EventFilter::Predicate filterOnLevel() const {
168+
return [this](const binlog::EventSource& src) { return src.severity >= level; };
169+
}
170+
171+
binlog::EventFilter::Predicate filterOnLevelAndNonEmptyCategory() const {
172+
return [this](const binlog::EventSource& src) {
173+
return !src.category.empty() && src.severity >= level;
174+
};
175+
}
176+
142177
// if configured as `no_logs`, the output is effectively disabled
143178
explicit operator bool() const { return level < Level::no_logs; }
144179
};
@@ -151,14 +186,15 @@ class Log {
151186
FilteredOutput<std::ofstream> binary{Level::no_logs};
152187
FilteredOutput<binlog::TextOutputStream> text{Level::info, textFile, format};
153188
FilteredOutput<binlog::TextOutputStream> console{Level::warning, std::cerr, format};
189+
FilteredOutput<SwiftDiagnosticsDumper> diagnostics{OnlyWithCategory{}, Level::error};
154190
LevelRules sourceRules;
155-
std::vector<std::string> collectSeverityRulesAndReturnProblems(const char* envVar);
191+
std::vector<std::string> collectLevelRulesAndReturnProblems(const char* envVar);
156192
};
157193

158194
// This class represent a named domain-specific logger, responsible for pushing logs using the
159195
// underlying `binlog::SessionWriter` class. This has a configured log level, so that logs on this
160196
// `Logger` with a level lower than the configured one are no-ops. The level is configured based
161-
// on rules matching `<logRootName>/<name>` in `CODEQL_EXTRACTOR_SWIFT_LOG_LEVELS` (see above).
197+
// on rules matching `<programName>/<name>` in `CODEQL_EXTRACTOR_SWIFT_LOG_LEVELS` (see above).
162198
// `<name>` is provided in the constructor. If no rule matches the name, the log level defaults to
163199
// the minimum level of all outputs.
164200
class Logger {

0 commit comments

Comments
 (0)