Skip to content

Commit 3f2a059

Browse files
committed
Swift: add location support to TSP diagnostics
This required a bit of an overhaul of the original integration of JSON diagnostics into binlog. The problem is that it is quite hard to add a kind of metadata to binlog entries without changing its code. Another problem is that when wanting to avoid double evaluation of logging macro arguments one cannot really add a separate "diagnose" step easily. The proposed solution consists in two things: * hook into a binlog plumbing function by providing a better overload resolution match, which happens after logging macro expansion, bypassing the problem of double evaluation * in that hook, produce the diagnostic directly, without waiting to reconstruct the diagnostics entry from the binlog serialized entry. This allows to forgo the weird category to diagnostic mapping, and now a diagnostics emission simply happens when a diagnostic source is given as the first argument after the log format string. A flavour of diganostics sources with locations is then added with the same mechanism, allowing to write something like ```cpp LOG_ERROR("[{}] ouch!", internalError.withLocation("foo.swift", 32)); ```
1 parent 82e780d commit 3f2a059

File tree

10 files changed

+244
-133
lines changed

10 files changed

+244
-133
lines changed

swift/logging/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ cc_library(
66
deps = [
77
"@absl//absl/strings",
88
"@binlog",
9+
"@fmt",
910
"@json",
1011
],
1112
)

swift/logging/SwiftDiagnostics.cpp

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,16 @@
11
#include "swift/logging/SwiftDiagnostics.h"
22

33
#include <binlog/Entries.hpp>
4-
#include <nlohmann/json.hpp>
54
#include "absl/strings/str_join.h"
65
#include "absl/strings/str_cat.h"
76
#include "absl/strings/str_split.h"
87
#include "swift/logging/SwiftAssert.h"
98

109
namespace codeql {
1110

12-
namespace {
13-
Logger& logger() {
14-
static Logger ret{"diagnostics"};
15-
return ret;
16-
}
17-
} // namespace
18-
19-
void SwiftDiagnosticsSource::emit(std::ostream& out,
20-
std::string_view timestamp,
21-
std::string_view message) const {
22-
nlohmann::json entry = {
11+
nlohmann::json SwiftDiagnosticsSource::json(const std::chrono::system_clock::time_point& timestamp,
12+
std::string_view message) const {
13+
return {
2314
{"source",
2415
{
2516
{"id", sourceId()},
@@ -35,34 +26,28 @@ void SwiftDiagnosticsSource::emit(std::ostream& out,
3526
{"severity", "error"},
3627
{"helpLinks", std::vector<std::string_view>(absl::StrSplit(helpLinks, ' '))},
3728
{"plaintextMessage", absl::StrCat(message, ".\n\n", action, ".")},
38-
{"timestamp", timestamp},
29+
{"timestamp", fmt::format("{:%FT%T%z}", timestamp)},
3930
};
40-
out << entry << '\n';
4131
}
4232

4333
std::string SwiftDiagnosticsSource::sourceId() const {
44-
auto ret = absl::StrJoin({extractorName, programName, id}, "/");
45-
std::replace(ret.begin(), ret.end(), '_', '-');
46-
return ret;
34+
return absl::StrJoin({extractorName, programName, id}, "/");
4735
}
48-
void SwiftDiagnosticsSource::registerImpl(const SwiftDiagnosticsSource* source) {
49-
auto [it, inserted] = map().emplace(source->id, source);
50-
CODEQL_ASSERT(inserted, "duplicate diagnostics source detected with id {}", source->id);
36+
37+
nlohmann::json SwiftDiagnosticsSourceWithLocation::json(
38+
const std::chrono::system_clock::time_point& timestamp,
39+
std::string_view message) const {
40+
auto ret = source.json(timestamp, message);
41+
auto& location = ret["location"] = {{"file", file}};
42+
if (startLine) location["startLine"] = startLine;
43+
if (startColumn) location["startColumn"] = startColumn;
44+
if (endLine) location["endLine"] = endLine;
45+
if (endColumn) location["endColumn"] = endColumn;
46+
return ret;
5147
}
5248

53-
void SwiftDiagnosticsDumper::write(const char* buffer, std::size_t bufferSize) {
54-
binlog::Range range{buffer, bufferSize};
55-
binlog::RangeEntryStream input{range};
56-
while (auto event = events.nextEvent(input)) {
57-
const auto& source = SwiftDiagnosticsSource::get(event->source->category);
58-
std::ostringstream oss;
59-
timestampedMessagePrinter.printEvent(oss, *event, events.writerProp(), events.clockSync());
60-
// TODO(C++20) use oss.view() directly
61-
auto data = oss.str();
62-
std::string_view view = data;
63-
using ViewPair = std::pair<std::string_view, std::string_view>;
64-
auto [timestamp, message] = ViewPair(absl::StrSplit(view, absl::MaxSplits(' ', 1)));
65-
source.emit(output, timestamp, message);
66-
}
49+
std::string SwiftDiagnosticsSourceWithLocation::str() const {
50+
return absl::StrCat(source.id, "@", file, ":", startLine, ":", startColumn, ":", endLine, ":",
51+
endColumn);
6752
}
6853
} // namespace codeql

swift/logging/SwiftDiagnostics.h

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
#pragma once
22

3-
#include <binlog/EventStream.hpp>
4-
#include <binlog/PrettyPrinter.hpp>
3+
#include <binlog/binlog.hpp>
54
#include <string>
65
#include <vector>
76
#include <unordered_map>
@@ -10,11 +9,31 @@
109
#include <filesystem>
1110
#include <sstream>
1211
#include <mutex>
12+
#include <fmt/format.h>
13+
#include <fmt/chrono.h>
14+
#include <nlohmann/json.hpp>
1315

1416
namespace codeql {
1517

1618
extern const std::string_view programName;
1719

20+
struct SwiftDiagnosticsSource;
21+
22+
struct SwiftDiagnosticsSourceWithLocation {
23+
const SwiftDiagnosticsSource& source;
24+
std::string_view file;
25+
unsigned startLine;
26+
unsigned startColumn;
27+
unsigned endLine;
28+
unsigned endColumn;
29+
30+
// see SwiftDiagnosticsSource::json
31+
nlohmann::json json(const std::chrono::system_clock::time_point& timestamp,
32+
std::string_view message) const;
33+
34+
std::string str() const;
35+
};
36+
1837
// Models a diagnostic source for Swift, holding static information that goes out into a diagnostic
1938
// These are internally stored into a map on id's. A specific error log can use binlog's category
2039
// as id, which will then be used to recover the diagnostic source while dumping.
@@ -29,36 +48,25 @@ struct SwiftDiagnosticsSource {
2948

3049
// for the moment, we only output errors, so no need to store the severity
3150

32-
// registers a diagnostics source for later retrieval with get, if not done yet
33-
template <const SwiftDiagnosticsSource* Source>
34-
static void ensureRegistered() {
35-
static std::once_flag once;
36-
std::call_once(once, registerImpl, Source);
37-
}
38-
39-
// gets a previously inscribed SwiftDiagnosticsSource for the given id. Will abort if none exists
40-
static const SwiftDiagnosticsSource& get(const std::string& id) { return *map().at(id); }
41-
42-
// emit a JSON diagnostics for this source with the given timestamp and message to out
51+
// create a JSON diagnostics for this source with the given timestamp and message to out
4352
// A plaintextMessage is used that includes both the message and the action to take. Dots are
4453
// appended to both. The id is used to construct the source id in the form
45-
// `swift/<prog name>/<id with '-' replacing '_'>`
46-
void emit(std::ostream& out, std::string_view timestamp, std::string_view message) const;
54+
// `swift/<prog name>/<id>`
55+
nlohmann::json json(const std::chrono::system_clock::time_point& timestamp,
56+
std::string_view message) const;
57+
58+
SwiftDiagnosticsSourceWithLocation withLocation(std::string_view file,
59+
unsigned startLine = 0,
60+
unsigned startColumn = 0,
61+
unsigned endLine = 0,
62+
unsigned endColumn = 0) const {
63+
return {*this, file, startLine, startColumn, endLine, endColumn};
64+
}
4765

4866
private:
49-
static void registerImpl(const SwiftDiagnosticsSource* source);
50-
5167
std::string sourceId() const;
52-
using Map = std::unordered_map<std::string, const SwiftDiagnosticsSource*>;
53-
54-
static Map& map() {
55-
static Map ret;
56-
return ret;
57-
}
5868
};
5969

60-
// An output modeling binlog's output stream concept that intercepts binlog entries and translates
61-
// them to appropriate diagnostics JSON entries
6270
class SwiftDiagnosticsDumper {
6371
public:
6472
// opens path for writing out JSON entries. Returns whether the operation was successful.
@@ -69,22 +77,65 @@ class SwiftDiagnosticsDumper {
6977

7078
void flush() { output.flush(); }
7179

72-
// write out binlog entries as corresponding JSON diagnostics entries. Expects all entries to have
73-
// a category equal to an id of a previously created SwiftDiagnosticSource.
74-
void write(const char* buffer, std::size_t bufferSize);
80+
template <typename Source>
81+
void write(const Source& source,
82+
const std::chrono::system_clock::time_point& timestamp,
83+
std::string_view message) {
84+
if (output) {
85+
output << source.json(timestamp, message) << '\n';
86+
}
87+
}
88+
89+
bool good() const { return output.good(); }
90+
explicit operator bool() const { return good(); }
7591

7692
private:
77-
binlog::EventStream events;
7893
std::ofstream output;
79-
binlog::PrettyPrinter timestampedMessagePrinter{"%u %m", "%Y-%m-%dT%H:%M:%S.%NZ"};
8094
};
8195

82-
} // namespace codeql
83-
84-
namespace codeql_diagnostics {
85-
constexpr codeql::SwiftDiagnosticsSource internal_error{
86-
"internal_error",
96+
constexpr codeql::SwiftDiagnosticsSource internalError{
97+
"internal-error",
8798
"Internal error",
8899
"Contact us about this issue",
89100
};
90-
} // namespace codeql_diagnostics
101+
} // namespace codeql
102+
103+
namespace mserialize {
104+
// log diagnostic sources using just their id, using binlog/mserialize internal plumbing
105+
template <>
106+
struct CustomTag<codeql::SwiftDiagnosticsSource, void> : detail::BuiltinTag<std::string> {
107+
using T = codeql::SwiftDiagnosticsSource;
108+
};
109+
110+
template <>
111+
struct CustomSerializer<codeql::SwiftDiagnosticsSource, void> {
112+
template <typename OutputStream>
113+
static void serialize(const codeql::SwiftDiagnosticsSource& source, OutputStream& out) {
114+
mserialize::serialize(source.id, out);
115+
}
116+
117+
static size_t serialized_size(const codeql::SwiftDiagnosticsSource& source) {
118+
return mserialize::serialized_size(source.id);
119+
}
120+
};
121+
122+
template <>
123+
struct CustomTag<codeql::SwiftDiagnosticsSourceWithLocation, void>
124+
: detail::BuiltinTag<std::string> {
125+
using T = codeql::SwiftDiagnosticsSourceWithLocation;
126+
};
127+
128+
template <>
129+
struct CustomSerializer<codeql::SwiftDiagnosticsSourceWithLocation, void> {
130+
template <typename OutputStream>
131+
static void serialize(const codeql::SwiftDiagnosticsSourceWithLocation& source,
132+
OutputStream& out) {
133+
mserialize::serialize(source.str(), out);
134+
}
135+
136+
static size_t serialized_size(const codeql::SwiftDiagnosticsSourceWithLocation& source) {
137+
return mserialize::serialized_size(source.str());
138+
}
139+
};
140+
141+
} // namespace mserialize

swift/logging/SwiftLogging.cpp

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ std::vector<std::string> Log::collectLevelRulesAndReturnProblems(const char* env
6060
// expect comma-separated <glob pattern>:<log severity>
6161
std::regex comma{","};
6262
std::regex levelAssignment{
63-
R"((?:([*./\w]+)|(?:out:(binary|text|console|diagnostics))):()" LEVEL_REGEX_PATTERN ")"};
63+
R"((?:([*./\w]+)|(?:out:(binary|text|console))):()" LEVEL_REGEX_PATTERN ")"};
6464
std::cregex_token_iterator begin{levels, levels + strlen(levels), comma, -1};
6565
std::cregex_token_iterator end{};
6666
for (auto it = begin; it != end; ++it) {
@@ -84,8 +84,6 @@ std::vector<std::string> Log::collectLevelRulesAndReturnProblems(const char* env
8484
text.level = level;
8585
} else if (out == "console") {
8686
console.level = level;
87-
} else if (out == "diagnostics") {
88-
diagnostics.level = level;
8987
}
9088
}
9189
} else {
@@ -132,30 +130,27 @@ void Log::configure() {
132130
text.level = Level::no_logs;
133131
}
134132
}
135-
if (diagnostics) {
136-
std::filesystem::path diagFile =
137-
getEnvOr("CODEQL_EXTRACTOR_SWIFT_DIAGNOSTIC_DIR", "extractor-out/diagnostics");
133+
if (auto diagDir = getEnvOr("CODEQL_EXTRACTOR_SWIFT_DIAGNOSTIC_DIR", nullptr)) {
134+
std::filesystem::path diagFile = diagDir;
138135
diagFile /= programName;
139136
diagFile /= logBaseName;
140137
diagFile.replace_extension(".jsonl");
141138
std::error_code ec;
142139
std::filesystem::create_directories(diagFile.parent_path(), ec);
143140
if (!ec) {
144-
if (!diagnostics.output.open(diagFile)) {
141+
if (!diagnostics.open(diagFile)) {
145142
problems.emplace_back("Unable to open diagnostics json file " + diagFile.string());
146-
diagnostics.level = Level::no_logs;
147143
}
148144
} else {
149145
problems.emplace_back("Unable to create diagnostics directory " +
150146
diagFile.parent_path().string() + ": " + ec.message());
151-
diagnostics.level = Level::no_logs;
152147
}
153148
}
154149
for (const auto& problem : problems) {
155150
LOG_ERROR("{}", problem);
156151
}
157152
LOG_INFO("Logging configured (binary: {}, text: {}, console: {}, diagnostics: {})", binary.level,
158-
text.level, console.level, diagnostics.level);
153+
text.level, console.level, diagnostics.good());
159154
initialized = true;
160155
flushImpl();
161156
}
@@ -169,7 +164,7 @@ void Log::flushImpl() {
169164
binary.output.flush();
170165
}
171166
if (diagnostics) {
172-
diagnostics.output.flush();
167+
diagnostics.flush();
173168
}
174169
}
175170

@@ -190,13 +185,11 @@ Log& Log::write(const char* buffer, std::streamsize size) {
190185
if (text) text.write(buffer, size);
191186
if (binary) binary.write(buffer, size);
192187
if (console) console.write(buffer, size);
193-
if (diagnostics) diagnostics.write(buffer, size);
194188
return *this;
195189
}
196190

197191
Logger& Log::logger() {
198192
static Logger ret{getLoggerConfigurationImpl("logging")};
199193
return ret;
200194
}
201-
202195
} // namespace codeql

0 commit comments

Comments
 (0)