|
1 | 1 | /* |
2 | | - * Copyright (C) 2025 HERE Europe B.V. |
| 2 | + * Copyright (C) 2025-2026 HERE Europe B.V. |
3 | 3 | * |
4 | 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | 5 | * you may not use this file except in compliance with the License. |
|
19 | 19 |
|
20 | 20 | #include <olp/core/http/adapters/HarCaptureAdapter.h> |
21 | 21 |
|
22 | | -#include <rapidjson/ostreamwrapper.h> |
23 | | -#include <rapidjson/prettywriter.h> |
| 22 | +#include <boost/json/serialize.hpp> |
| 23 | +#include <boost/json/value.hpp> |
| 24 | + |
| 25 | +#include <generated/serializer/SerializerWrapper.h> |
24 | 26 |
|
25 | 27 | #include <deque> |
26 | 28 | #include <fstream> |
@@ -68,56 +70,96 @@ std::string FormatTime(const std::chrono::system_clock::time_point timestamp) { |
68 | 70 | return ss.str(); |
69 | 71 | } |
70 | 72 |
|
71 | | -class JsonFileSerializer { |
72 | | - public: |
73 | | - explicit JsonFileSerializer(std::ofstream& file) |
74 | | - : out_stream_(file), writer_(out_stream_) {} |
| 73 | +// In addition to making result easier to read formats floating point numbers |
| 74 | +// from `1.00281E2` to `100.281` and from `0` to `0.000` |
| 75 | +void PrettyPrint(std::ostream& os, boost::json::value const& jv, |
| 76 | + std::string* indent = nullptr) { |
| 77 | + const auto initial_precision{os.precision()}; |
| 78 | + const auto initial_floatfield{os.floatfield}; |
| 79 | + os.precision(3); |
| 80 | + os.setf(std::ios_base::fixed, std::ios_base::floatfield); |
| 81 | + |
| 82 | + std::string indent_; |
| 83 | + if (!indent) |
| 84 | + indent = &indent_; |
| 85 | + switch (jv.kind()) { |
| 86 | + case boost::json::kind::object: { |
| 87 | + os << "{\n"; |
| 88 | + indent->append(4, ' '); |
| 89 | + auto const& obj = jv.get_object(); |
| 90 | + if (!obj.empty()) { |
| 91 | + auto it = obj.begin(); |
| 92 | + for (;;) { |
| 93 | + os << *indent << boost::json::serialize(it->key()) << ": "; |
| 94 | + PrettyPrint(os, it->value(), indent); |
| 95 | + if (++it == obj.end()) |
| 96 | + break; |
| 97 | + os << ",\n"; |
| 98 | + } |
| 99 | + } |
| 100 | + os << "\n"; |
| 101 | + indent->resize(indent->size() - 4); |
| 102 | + os << *indent << "}"; |
| 103 | + break; |
| 104 | + } |
75 | 105 |
|
76 | | - void Object(const std::function<void()>& body) { |
77 | | - writer_.StartObject(); |
78 | | - body(); |
79 | | - writer_.EndObject(); |
80 | | - } |
| 106 | + case boost::json::kind::array: { |
| 107 | + auto const& arr = jv.get_array(); |
| 108 | + if (arr.empty()) { |
| 109 | + os << "[]"; |
| 110 | + } else { |
| 111 | + os << "[\n"; |
| 112 | + indent->append(4, ' '); |
| 113 | + auto it = arr.begin(); |
| 114 | + for (;;) { |
| 115 | + os << *indent; |
| 116 | + PrettyPrint(os, *it, indent); |
| 117 | + if (++it == arr.end()) |
| 118 | + break; |
| 119 | + os << ",\n"; |
| 120 | + } |
| 121 | + os << "\n"; |
| 122 | + indent->resize(indent->size() - 4); |
| 123 | + os << *indent << "]"; |
| 124 | + } |
| 125 | + break; |
| 126 | + } |
81 | 127 |
|
82 | | - void Object(const char* key, const std::function<void()>& body) { |
83 | | - writer_.Key(key); |
84 | | - writer_.StartObject(); |
85 | | - body(); |
86 | | - writer_.EndObject(); |
87 | | - } |
| 128 | + case boost::json::kind::string: { |
| 129 | + os << boost::json::serialize(jv.get_string()); |
| 130 | + break; |
| 131 | + } |
88 | 132 |
|
89 | | - void Array(const char* key, const std::function<void()>& body) { |
90 | | - writer_.Key(key); |
91 | | - writer_.StartArray(); |
92 | | - body(); |
93 | | - writer_.EndArray(); |
94 | | - } |
| 133 | + case boost::json::kind::uint64: |
| 134 | + os << jv.get_uint64(); |
| 135 | + break; |
95 | 136 |
|
96 | | - void String(const char* key, const std::string& value) { |
97 | | - writer_.Key(key); |
98 | | - writer_.String(value.c_str(), value.size()); |
99 | | - } |
| 137 | + case boost::json::kind::int64: |
| 138 | + os << jv.get_int64(); |
| 139 | + break; |
100 | 140 |
|
101 | | - void Int(const char* key, const int value) { |
102 | | - writer_.Key(key); |
103 | | - writer_.Int(value); |
104 | | - } |
| 141 | + case boost::json::kind::double_: |
| 142 | + os << jv.get_double(); |
| 143 | + break; |
105 | 144 |
|
106 | | - void Double(const char* key, const double value) { |
107 | | - writer_.Key(key); |
108 | | - writer_.Double(value); |
109 | | - } |
| 145 | + case boost::json::kind::bool_: |
| 146 | + if (jv.get_bool()) |
| 147 | + os << "true"; |
| 148 | + else |
| 149 | + os << "false"; |
| 150 | + break; |
110 | 151 |
|
111 | | - void EmptyArray(const char* key) { |
112 | | - writer_.Key(key); |
113 | | - writer_.StartArray(); |
114 | | - writer_.EndArray(); |
| 152 | + case boost::json::kind::null: |
| 153 | + os << "null"; |
| 154 | + break; |
115 | 155 | } |
116 | 156 |
|
117 | | - private: |
118 | | - rapidjson::OStreamWrapper out_stream_; |
119 | | - rapidjson::PrettyWriter<rapidjson::OStreamWrapper> writer_{}; |
120 | | -}; |
| 157 | + if (indent->empty()) |
| 158 | + os << "\n"; |
| 159 | + |
| 160 | + os.precision(initial_precision); |
| 161 | + os.setf(std::ios_base::fixed, initial_floatfield); |
| 162 | +} |
121 | 163 |
|
122 | 164 | } // namespace |
123 | 165 |
|
@@ -229,118 +271,118 @@ class HarCaptureAdapter::HarCaptureAdapterImpl final : public Network { |
229 | 271 | } |
230 | 272 |
|
231 | 273 | void SaveSessionToFile() const { |
| 274 | + boost::json::object log; |
| 275 | + log["version"] = "1.2"; |
| 276 | + log["creator"] = boost::json::object( |
| 277 | + {{"name", "DataSDK"}, {"version", OLP_SDK_VERSION_STRING}}); |
| 278 | + |
| 279 | + log["entries"] = [&]() { |
| 280 | + boost::json::array entries; |
| 281 | + entries.reserve(requests_.size()); |
| 282 | + for (auto request_index = 0u; request_index < requests_.size(); |
| 283 | + ++request_index) { |
| 284 | + const auto& request = requests_[request_index]; |
| 285 | + const auto diagnostics = diagnostics_.size() > request_index |
| 286 | + ? diagnostics_[request_index] |
| 287 | + : Diagnostics{}; |
| 288 | + |
| 289 | + // return duration in milliseconds as float |
| 290 | + auto duration = [&](const Diagnostics::Timings timing, |
| 291 | + const double default_value = -1.0) { |
| 292 | + return diagnostics.available_timings[timing] |
| 293 | + ? diagnostics.timings[timing].count() / 1000.0 |
| 294 | + : default_value; |
| 295 | + }; |
| 296 | + |
| 297 | + const double total_time = |
| 298 | + duration(Diagnostics::Total, |
| 299 | + static_cast<double>( |
| 300 | + std::chrono::duration_cast<std::chrono::microseconds>( |
| 301 | + request.end_time - request.start_time) |
| 302 | + .count()) / |
| 303 | + 1000.0); |
| 304 | + |
| 305 | + auto output_headers = [&](const uint16_t headers_offset, |
| 306 | + const uint16_t headers_count) { |
| 307 | + boost::json::array headers; |
| 308 | + headers.reserve(headers_count); |
| 309 | + for (auto i = 0u; i < headers_count; ++i) { |
| 310 | + const auto& header = headers_[headers_offset + i]; |
| 311 | + headers.emplace_back( |
| 312 | + boost::json::object({{"name", cache_.at(header.first)}, |
| 313 | + {"value", cache_.at(header.second)}})); |
| 314 | + } |
| 315 | + return headers; |
| 316 | + }; |
| 317 | + |
| 318 | + boost::json::object entry; |
| 319 | + entry["startedDateTime"] = FormatTime(request.start_time); |
| 320 | + entry["time"] = total_time; |
| 321 | + |
| 322 | + entry.emplace("request", [&]() { |
| 323 | + boost::json::object value; |
| 324 | + |
| 325 | + value["method"] = VerbToString( |
| 326 | + static_cast<NetworkRequest::HttpVerb>(request.method)); |
| 327 | + value["url"] = cache_.at(request.url); |
| 328 | + value["httpVersion"] = "UNSPECIFIED"; |
| 329 | + value["cookies"] = boost::json::array{}; |
| 330 | + value["headers"] = output_headers(request.request_headers_offset, |
| 331 | + request.request_headers_count); |
| 332 | + value["queryString"] = boost::json::array{}; |
| 333 | + value["headersSize"] = -1; |
| 334 | + value["bodySize"] = -1; |
| 335 | + |
| 336 | + return value; |
| 337 | + }()); |
| 338 | + |
| 339 | + entry.emplace("response", [&]() { |
| 340 | + boost::json::object value; |
| 341 | + value["status"] = request.status_code; |
| 342 | + value["statusText"] = ""; |
| 343 | + value["httpVersion"] = "UNSPECIFIED"; |
| 344 | + value["cookies"] = boost::json::array{}; |
| 345 | + value["headers"] = output_headers(request.response_headers_offset, |
| 346 | + request.response_headers_count); |
| 347 | + value["content"] = |
| 348 | + boost::json::object({{"size", 0}, {"mimeType", ""}}); |
| 349 | + value["redirectURL"] = ""; |
| 350 | + value["headersSize"] = -1; |
| 351 | + value["bodySize"] = -1; |
| 352 | + value["_transferSize"] = static_cast<int>(request.transfer_size); |
| 353 | + return value; |
| 354 | + }()); |
| 355 | + |
| 356 | + entry.emplace("timings", [&]() { |
| 357 | + using Timings = Diagnostics::Timings; |
| 358 | + boost::json::object value; |
| 359 | + value["blocked"] = duration(Timings::Queue); |
| 360 | + value["dns"] = duration(Timings::NameLookup); |
| 361 | + value["connect"] = duration(Timings::Connect); |
| 362 | + value["ssl"] = duration(Timings::SSL_Handshake); |
| 363 | + value["send"] = duration(Timings::Send, 0.0); |
| 364 | + value["wait"] = duration(Timings::Wait, 0.0); |
| 365 | + value["receive"] = duration(Timings::Receive, total_time); |
| 366 | + return value; |
| 367 | + }()); |
| 368 | + |
| 369 | + entries.emplace_back(std::move(entry)); |
| 370 | + } |
| 371 | + |
| 372 | + return entries; |
| 373 | + }(); |
| 374 | + |
| 375 | + boost::json::object doc; |
| 376 | + doc.emplace("log", std::move(log)); |
| 377 | + |
232 | 378 | std::ofstream file(har_out_path_); |
233 | 379 | if (!file.is_open()) { |
234 | 380 | OLP_SDK_LOG_ERROR("HarCaptureAdapter::SaveSession", |
235 | 381 | "Failed to save session."); |
236 | 382 | return; |
237 | 383 | } |
238 | 384 |
|
239 | | - JsonFileSerializer serializer{file}; |
240 | | - |
241 | | - serializer.Object([&] { |
242 | | - serializer.Object("log", [&] { |
243 | | - serializer.String("version", "1.2"); |
244 | | - |
245 | | - serializer.Object("creator", [&] { |
246 | | - serializer.String("name", "DataSDK"); |
247 | | - serializer.String("version", OLP_SDK_VERSION_STRING); |
248 | | - }); |
249 | | - |
250 | | - serializer.Array("entries", [&] { |
251 | | - for (auto request_index = 0u; request_index < requests_.size(); |
252 | | - ++request_index) { |
253 | | - const auto& request = requests_[request_index]; |
254 | | - const auto diagnostics = diagnostics_.size() > request_index |
255 | | - ? diagnostics_[request_index] |
256 | | - : Diagnostics{}; |
257 | | - |
258 | | - // return duration in milliseconds as float |
259 | | - auto duration = [&](const Diagnostics::Timings timing, |
260 | | - const double default_value = -1.0) { |
261 | | - return diagnostics.available_timings[timing] |
262 | | - ? diagnostics.timings[timing].count() / 1000.0 |
263 | | - : default_value; |
264 | | - }; |
265 | | - |
266 | | - const double total_time = duration( |
267 | | - Diagnostics::Total, |
268 | | - static_cast<double>( |
269 | | - std::chrono::duration_cast<std::chrono::microseconds>( |
270 | | - request.end_time - request.start_time) |
271 | | - .count()) / |
272 | | - 1000.0); |
273 | | - |
274 | | - auto output_headers = [&](const uint16_t headers_offset, |
275 | | - const uint16_t headers_count) { |
276 | | - serializer.Array("headers", [&] { |
277 | | - for (auto i = 0u; i < headers_count; ++i) { |
278 | | - serializer.Object([&] { |
279 | | - auto header = headers_[headers_offset + i]; |
280 | | - serializer.String("name", cache_.at(header.first)); |
281 | | - serializer.String("value", cache_.at(header.second)); |
282 | | - }); |
283 | | - } |
284 | | - }); |
285 | | - }; |
286 | | - |
287 | | - serializer.Object([&] { |
288 | | - serializer.String("startedDateTime", |
289 | | - FormatTime(request.start_time)); |
290 | | - serializer.Double("time", total_time); |
291 | | - |
292 | | - serializer.Object("request", [&] { |
293 | | - serializer.String( |
294 | | - "method", |
295 | | - VerbToString( |
296 | | - static_cast<NetworkRequest::HttpVerb>(request.method))); |
297 | | - serializer.String("url", cache_.at(request.url)); |
298 | | - serializer.String("httpVersion", "UNSPECIFIED"); |
299 | | - serializer.EmptyArray("cookies"); |
300 | | - output_headers(request.request_headers_offset, |
301 | | - request.request_headers_count); |
302 | | - serializer.EmptyArray("queryString"); |
303 | | - serializer.Int("headersSize", -1); |
304 | | - serializer.Int("bodySize", -1); |
305 | | - }); |
306 | | - |
307 | | - // response |
308 | | - serializer.Object("response", [&] { |
309 | | - serializer.Int("status", request.status_code); |
310 | | - serializer.String("statusText", ""); |
311 | | - serializer.String("httpVersion", "UNSPECIFIED"); |
312 | | - serializer.EmptyArray("cookies"); |
313 | | - output_headers(request.response_headers_offset, |
314 | | - request.response_headers_count); |
315 | | - serializer.Object("content", [&] { |
316 | | - serializer.Int("size", 0); |
317 | | - serializer.String("mimeType", ""); |
318 | | - }); |
319 | | - serializer.String("redirectURL", ""); |
320 | | - serializer.Int("headersSize", -1); |
321 | | - serializer.Int("bodySize", -1); |
322 | | - serializer.Int("_transferSize", |
323 | | - static_cast<int>(request.transfer_size)); |
324 | | - }); |
325 | | - |
326 | | - // timings |
327 | | - serializer.Object("timings", [&] { |
328 | | - using Timings = Diagnostics::Timings; |
329 | | - serializer.Double("blocked", duration(Timings::Queue)); |
330 | | - serializer.Double("dns", duration(Timings::NameLookup)); |
331 | | - serializer.Double("connect", duration(Timings::Connect)); |
332 | | - serializer.Double("ssl", duration(Timings::SSL_Handshake)); |
333 | | - serializer.Double("send", duration(Timings::Send, 0.0)); |
334 | | - serializer.Double("wait", duration(Timings::Wait, 0.0)); |
335 | | - serializer.Double("receive", |
336 | | - duration(Timings::Receive, total_time)); |
337 | | - }); |
338 | | - }); |
339 | | - } |
340 | | - }); |
341 | | - }); |
342 | | - }); |
343 | | - |
| 385 | + PrettyPrint(file, doc); |
344 | 386 | file.close(); |
345 | 387 |
|
346 | 388 | OLP_SDK_LOG_INFO("HarCaptureAdapter::SaveSession", |
|
0 commit comments