1
0
mirror of https://github.com/facebookincubator/mvfst.git synced 2025-11-28 15:43:57 +03:00
Files
mvfst/quic/logging/FileQLogger.cpp
Matt Joras 88a8b48c3b Migrate mvfst qlog to IETF qlog main schema v0.4
Summary:
Migrates mvfst's qlog implementation from the legacy "draft-00" format
to the official IETF qlog main schema v0.4 specification (draft-ietf-quic-qlog-
main-schema and draft-ietf-quic-qlog-quic-events). This ensures compatibility
with standard qlog tooling and adheres to the ratified IETF format.

Key changes to the qlog structure:

1. Event format: Migrated from array format [time, category, event, data] to
   object format {time: X, name: "category:event_type", data: {...}}. Event
   names now follow the "category:event_type" convention (e.g.,
   "transport:packet_sent", "connectivity:connection_closed").

2. File structure: Updated top-level fields to include file_schema
   (urn:ietf:params:qlog:file:contained), qlog_version ("0.4"),
   serialization_format ("application/qlog+json"), and proper event_schemas
   array. Added summary field with max_duration, total_event_count, and
   trace_count.

3. Common fields: Restructured common_fields to follow IETF spec with ODCID,
   scid, protocol_type, reference_time ("0"), time_format ("relative"), and
   time_units ("microseconds").

4. Vantage point: Changed from name-based to type-based representation
   ({"type": "client"} or {"type": "server"}).

5. Streaming support: Updated finishStream() to properly inject the summary
   field before the final closing brace for both pretty and non-pretty JSON
   formats. Non-pretty JSON is handled as single-line output.

Implementation spans QLoggerTypes.cpp (event name mappings), FileQLogger.cpp
(summary generation and streaming), and updated test expectations throughout
QLoggerTest.cpp to validate the new format.

Reviewed By: sharmafb

Differential Revision: D86882919

fbshipit-source-id: bad3cf3667ce0a3ad8de207bb3a67a7bc5404a2b
2025-11-25 04:42:58 -08:00

654 lines
20 KiB
C++

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <quic/logging/FileQLogger.h>
#include <fstream>
#include <vector>
#include <folly/json/json.h> // @manual=//folly:dynamic
namespace quic {
void FileQLogger::setDcid(Optional<ConnectionId> connID) {
if (connID.has_value()) {
dcid = connID.value();
if (streaming_) {
setupStream();
}
}
}
void FileQLogger::setScid(Optional<ConnectionId> connID) {
if (connID.has_value()) {
scid = connID.value();
}
}
void FileQLogger::setupStream() {
// create the output file
if (!dcid.has_value()) {
LOG(ERROR) << "Error: No dcid found";
return;
}
numEvents_ = 0;
startTime_ = std::chrono::microseconds::zero();
endLine_ = prettyJson_ ? "\n" : "";
auto extension = compress_ ? kCompressedQlogExtension : kQlogExtension;
std::string outputPath =
fmt::format("{}/{}{}", path_, (dcid.value()).hex(), extension);
try {
writer_ = std::make_unique<folly::AsyncFileWriter>(outputPath);
} catch (const std::system_error& err) {
LOG(ERROR) << "Error creating qlog file. " << err.what();
return;
}
if (compress_) {
compressionCodec_ =
folly::compression::getStreamCodec(folly::compression::CodecType::GZIP);
compressionBuffer_ = BufHelpers::createCombined(kCompressionBufferSize);
}
// Create the base json
auto qLog = prettyJson_ ? folly::toPrettyJson(toDynamicBase())
: folly::toJson(toDynamicBase());
baseJson_ << qLog;
// start copying from base to outputFile, stop at events
baseJson_.seekg(0, baseJson_.beg);
token_ = prettyJson_ ? "\"events\": [" : "\"events\":[";
while (getline(baseJson_, eventLine_)) {
pos_ = eventLine_.find(token_);
if (pos_ == std::string::npos) {
writeToStream(eventLine_ + endLine_);
} else {
// Found the token
for (char c : eventLine_) {
// get the amount of spaces each event should be padded
if (c == ' ') {
eventsPadding_ += ' ';
} else {
break;
}
}
// write up to and including the token
writeToStream(std::string(&eventLine_[0], pos_ + token_.size()));
break;
}
}
}
void FileQLogger::writeToStream(folly::StringPiece message) {
if (!writer_) {
return;
}
if (compress_) {
bool inputConsumed = false;
while (!inputConsumed) {
compressionBuffer_->clear();
ByteRange inputRange(message);
auto outputRange = MutableByteRange(
compressionBuffer_->writableData(), compressionBuffer_->capacity());
compressionCodec_->compressStream(inputRange, outputRange);
// Output range has advanced to last compressed byte written
auto outputLen = compressionBuffer_->capacity() - outputRange.size();
// Input range has advanced to last uncompressed byte read
inputConsumed = inputRange.empty();
// Write compressed data to file
writer_->writeMessage(
folly::StringPiece(
(const char*)compressionBuffer_->data(), outputLen));
}
} else {
writer_->writeMessage(message);
}
}
void FileQLogger::finishStream() {
if (!writer_) {
return;
}
// finish copying the line that was stopped on
std::string unfinishedLine(
&eventLine_[pos_ + token_.size()],
eventLine_.size() - pos_ - token_.size() - (prettyJson_ ? 0 : 1));
if (!prettyJson_) {
writeToStream(unfinishedLine);
writeToStream(folly::StringPiece("}"));
} else {
std::vector<std::string> remainingLines;
remainingLines.push_back(eventsPadding_ + unfinishedLine);
while (getline(baseJson_, eventLine_)) {
remainingLines.push_back(eventLine_);
}
for (size_t i = 0; i < remainingLines.size(); ++i) {
if (i > 0) {
writeToStream(endLine_);
}
writeToStream(remainingLines[i]);
}
}
// Finalize compression frame
if (compress_) {
bool ended = false;
while (!ended) {
compressionBuffer_->clear();
ByteRange inputRange(folly::StringPiece(""));
auto outputRange = MutableByteRange(
compressionBuffer_->writableData(), compressionBuffer_->capacity());
ended = compressionCodec_->compressStream(
inputRange,
outputRange,
folly::compression::StreamCodec::FlushOp::END);
auto outputLen = compressionBuffer_->capacity() - outputRange.size();
writer_->writeMessage(
folly::StringPiece(
(const char*)compressionBuffer_->data(), outputLen));
}
}
}
void FileQLogger::handleEvent(std::unique_ptr<QLogEvent> event) {
if (streaming_) {
numEvents_++;
startTime_ = (startTime_ == std::chrono::microseconds::zero())
? event->refTime
: startTime_;
endTime_ = event->refTime;
auto eventJson = prettyJson_ ? folly::toPrettyJson(event->toDynamic())
: folly::toJson(event->toDynamic());
std::stringstream eventBuffer;
std::string line;
eventBuffer << eventJson;
bool firstLine = true;
while (getline(eventBuffer, line)) {
if (firstLine) {
if (numEvents_ > 1) {
writeToStream(folly::StringPiece(","));
}
writeToStream(endLine_);
firstLine = false;
} else {
writeToStream(endLine_);
}
writeToStream(fmt::format("{}{}{}", basePadding_, eventsPadding_, line));
}
} else {
logs.push_back(std::move(event));
}
}
void FileQLogger::addPacket(
const RegularQuicPacket& regularPacket,
uint64_t packetSize) {
handleEvent(createPacketEvent(regularPacket, packetSize));
}
void FileQLogger::addPacket(
const RegularQuicWritePacket& writePacket,
uint64_t packetSize) {
handleEvent(createPacketEvent(writePacket, packetSize));
}
void FileQLogger::addPacket(
const VersionNegotiationPacket& versionPacket,
uint64_t packetSize,
bool isPacketRecvd) {
handleEvent(createPacketEvent(versionPacket, packetSize, isPacketRecvd));
}
void FileQLogger::addPacket(
const RetryPacket& retryPacket,
uint64_t packetSize,
bool isPacketRecvd) {
logs.push_back(createPacketEvent(retryPacket, packetSize, isPacketRecvd));
}
void FileQLogger::addConnectionClose(
std::string error,
std::string reason,
bool drainConnection,
bool sendCloseImmediately) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogConnectionCloseEvent>(
std::move(error),
std::move(reason),
drainConnection,
sendCloseImmediately,
refTime));
}
void FileQLogger::addTransportSummary(const TransportSummaryArgs& args) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogTransportSummaryEvent>(
args.totalBytesSent,
args.totalBytesRecvd,
args.sumCurWriteOffset,
args.sumMaxObservedOffset,
args.sumCurStreamBufferLen,
args.totalBytesRetransmitted,
args.totalStreamBytesCloned,
args.totalBytesCloned,
args.totalCryptoDataWritten,
args.totalCryptoDataRecvd,
args.currentWritableBytes,
args.currentConnFlowControl,
args.totalPacketsSpuriouslyMarkedLost,
args.finalPacketLossReorderingThreshold,
args.finalPacketLossTimeReorderingThreshDividend,
args.usedZeroRtt,
args.quicVersion,
args.initialPacketsReceived,
args.uniqueInitialCryptoFramesReceived,
args.timeUntilLastInitialCryptoFrameReceived,
args.alpn,
args.namedGroup,
args.pskType,
args.echStatus,
refTime));
}
void FileQLogger::addCongestionMetricUpdate(
uint64_t bytesInFlight,
uint64_t currentCwnd,
std::string congestionEvent,
std::string state,
std::string recoveryState) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogCongestionMetricUpdateEvent>(
bytesInFlight,
currentCwnd,
std::move(congestionEvent),
std::move(state),
std::move(recoveryState),
refTime));
}
void FileQLogger::addCongestionStateUpdate(
Optional<std::string> oldState,
std::string newState,
Optional<std::string> trigger) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogCongestionStateUpdateEvent>(
std::move(oldState),
std::move(newState),
std::move(trigger),
refTime));
}
void FileQLogger::addBandwidthEstUpdate(
uint64_t bytes,
std::chrono::microseconds interval) {
handleEvent(
std::make_unique<quic::QLogBandwidthEstUpdateEvent>(
bytes,
interval,
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())));
}
void FileQLogger::addAppLimitedUpdate() {
handleEvent(
std::make_unique<quic::QLogAppLimitedUpdateEvent>(
true,
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())));
}
void FileQLogger::addAppUnlimitedUpdate() {
handleEvent(
std::make_unique<quic::QLogAppLimitedUpdateEvent>(
false,
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch())));
}
void FileQLogger::addPacingMetricUpdate(
uint64_t pacingBurstSizeIn,
std::chrono::microseconds pacingIntervalIn) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogPacingMetricUpdateEvent>(
pacingBurstSizeIn, pacingIntervalIn, refTime));
}
void FileQLogger::addPacingObservation(
std::string actual,
std::string expect,
std::string conclusion) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogPacingObservationEvent>(
std::move(actual),
std::move(expect),
std::move(conclusion),
refTime));
}
void FileQLogger::addAppIdleUpdate(std::string idleEvent, bool idle) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogAppIdleUpdateEvent>(
std::move(idleEvent), idle, refTime));
}
void FileQLogger::addPacketDrop(size_t packetSize, std::string dropReason) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogPacketDropEvent>(
packetSize, std::move(dropReason), refTime));
}
void FileQLogger::addDatagramReceived(uint64_t dataLen) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogDatagramReceivedEvent>(dataLen, refTime));
}
void FileQLogger::addLossAlarm(
PacketNum largestSent,
uint64_t alarmCount,
uint64_t outstandingPackets,
std::string type) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogLossAlarmEvent>(
largestSent,
alarmCount,
outstandingPackets,
std::move(type),
refTime));
}
void FileQLogger::addPacketsLost(
PacketNum largestLostPacketNum,
uint64_t lostBytes,
uint64_t lostPackets) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogPacketsLostEvent>(
largestLostPacketNum, lostBytes, lostPackets, refTime));
}
void FileQLogger::addTransportStateUpdate(std::string update) {
static const std::unordered_set<std::string> validStates = {
"attempted",
"handshake_started",
"handshake_complete",
"closed",
"peer_validated",
"early_write",
"handshake_confirmed",
"closing",
"draining"};
if (validStates.find(update) == validStates.end()) {
return;
}
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogTransportStateUpdateEvent>(
std::move(update), refTime));
}
void FileQLogger::addPacketBuffered(
ProtectionType protectionType,
uint64_t packetSize) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogPacketBufferedEvent>(
protectionType, packetSize, refTime));
}
void FileQLogger::addMetricUpdate(
std::chrono::microseconds latestRtt,
std::chrono::microseconds mrtt,
std::chrono::microseconds srtt,
std::chrono::microseconds ackDelay,
Optional<std::chrono::microseconds> rttVar,
Optional<uint64_t> congestionWindow,
Optional<uint64_t> bytesInFlight,
Optional<uint64_t> ssthresh,
Optional<uint64_t> packetsInFlight,
Optional<uint64_t> pacingRateBytesPerSec,
Optional<uint32_t> ptoCount) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogMetricUpdateEvent>(
latestRtt,
mrtt,
srtt,
ackDelay,
refTime,
rttVar,
congestionWindow,
bytesInFlight,
ssthresh,
packetsInFlight,
pacingRateBytesPerSec,
ptoCount));
}
folly::dynamic FileQLogger::toDynamic() const {
folly::dynamic qlogFile = toDynamicBase();
auto events = folly::dynamic::array();
for (auto& event : logs) {
events.push_back(event->toDynamic());
}
qlogFile["traces"][0]["events"] = events;
return qlogFile;
}
folly::dynamic FileQLogger::toDynamicBase() const {
folly::dynamic qlogFile = folly::dynamic::object();
qlogFile["description"] = kQLogDescription;
qlogFile["file_schema"] = kQLogFileSchemaURI;
qlogFile["serialization_format"] = kQLogSerializationFormat;
qlogFile["title"] = kQLogTitle;
folly::dynamic trace = folly::dynamic::object();
trace["title"] = kQLogTraceTitle;
trace["description"] = kQLogTraceDescription;
auto vpInfo = createVantagePoint(vantagePoint, "");
trace["vantage_point"] = vpInfo.toDynamic();
trace["event_schemas"] =
folly::dynamic::array(kQLogEventSchemaURI, kQLogMvfstEventSchemaURI);
folly::dynamic commonFieldsDyn = folly::dynamic::object();
commonFieldsDyn["odcid"] = dcid.has_value() ? dcid.value().hex() : "";
if (!protocolType.empty()) {
commonFieldsDyn["protocol_type"] = protocolType;
}
folly::dynamic refTime = folly::dynamic::object();
refTime["clock_type"] = kQLogClockTypeMonotonic;
refTime["epoch"] = "unknown";
commonFieldsDyn["reference_time"] = std::move(refTime);
commonFieldsDyn["time_format"] = kQLogTimeFormatRelativeToEpoch;
commonFieldsDyn["scid"] = scid.has_value() ? scid.value().hex() : "";
trace["common_fields"] = std::move(commonFieldsDyn);
trace["events"] = folly::dynamic::array();
qlogFile["traces"] = folly::dynamic::array();
qlogFile["traces"].push_back(std::move(trace));
return qlogFile;
}
folly::dynamic FileQLogger::generateSummary(
size_t numEvents,
std::chrono::microseconds startTime,
std::chrono::microseconds endTime) const {
folly::dynamic summaryObj = folly::dynamic::object();
summaryObj[kQLogTraceCountField] =
1; // hardcoded, we only support 1 trace right now
// max duration is calculated like this:
// if there is <= 1 event, max_duration is 0
// otherwise, it is the (time of the last event - time of the first event)
summaryObj["max_duration"] =
(numEvents <= 1) ? 0 : (endTime - startTime).count();
summaryObj["total_event_count"] = numEvents;
return summaryObj;
}
void FileQLogger::addStreamStateUpdate(
quic::StreamId id,
std::string update,
Optional<std::chrono::milliseconds> timeSinceStreamCreation) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogStreamStateUpdateEvent>(
id,
std::move(update),
std::move(timeSinceStreamCreation),
vantagePoint,
refTime));
}
void FileQLogger::addConnectionMigrationUpdate(bool intentionalMigration) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogConnectionMigrationEvent>(
intentionalMigration, vantagePoint, refTime));
}
void FileQLogger::addPathValidationEvent(bool success) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogPathValidationEvent>(
success, vantagePoint, refTime));
}
void FileQLogger::addPriorityUpdate(
quic::StreamId streamId,
PriorityQueue::PriorityLogFields priority) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogPriorityUpdateEvent>(
streamId, std::move(priority), refTime));
}
void FileQLogger::addL4sWeightUpdate(
double l4sWeight,
uint32_t newEct1,
uint32_t newCe) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogL4sWeightUpdateEvent>(
l4sWeight, newEct1, newCe, refTime));
}
void FileQLogger::addNetworkPathModelUpdate(
uint64_t inflightHi,
uint64_t inflightLo,
uint64_t bandwidthHiBytes,
std::chrono::microseconds bandwidthHiInterval,
uint64_t bandwidthLoBytes,
std::chrono::microseconds bandwidthLoInterval) {
auto refTime = std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch());
handleEvent(
std::make_unique<quic::QLogNetworkPathModelUpdateEvent>(
inflightHi,
inflightLo,
bandwidthHiBytes,
bandwidthHiInterval,
bandwidthLoBytes,
bandwidthLoInterval,
refTime));
}
void FileQLogger::outputLogsToFile(const std::string& path, bool prettyJson) {
if (streaming_) {
return;
}
if (!dcid.has_value()) {
LOG(ERROR) << "Error: No dcid found";
return;
}
auto extension = compress_ ? kCompressedQlogExtension : kQlogExtension;
std::string outputPath =
fmt::format("{}/{}{}", path, (dcid.value()).hex(), extension);
std::ofstream fileObj(outputPath);
if (fileObj) {
auto qLog = prettyJson ? folly::toPrettyJson(toDynamic())
: folly::toJson(toDynamic());
if (compress_) {
try {
auto gzipCodec =
folly::compression::getCodec(folly::compression::CodecType::GZIP);
auto compressed = gzipCodec->compress(qLog);
fileObj << compressed;
} catch (std::invalid_argument& ex) {
LOG(ERROR) << "Failed to compress QLog. " << ex.what();
}
} else {
fileObj << qLog;
}
} else {
LOG(ERROR) << "Error: Can't write to provided path: " << path;
}
fileObj.close();
}
} // namespace quic