• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

llnl / dftracer-utils / 30067416336

24 Jul 2026 04:40AM UTC coverage: 50.693% (-2.0%) from 52.66%
30067416336

Pull #99

github

web-flow
Merge 464586f0d into 06bc84ec9
Pull Request #99: Support CM time_metric (NS/MS/SEC/US) across reader and viz

18063 of 48203 branches covered (37.47%)

Branch coverage included in aggregate %.

1514 of 2204 new or added lines in 58 files covered. (68.69%)

741 existing lines in 114 files now uncovered.

23715 of 34210 relevant lines covered (69.32%)

39842.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

34.45
/src/dftracer/utils/server/trace_api.cpp
1
#include <dftracer/utils/core/common/filesystem.h>
2
#include <dftracer/utils/core/common/logging.h>
3
#include <dftracer/utils/core/common/transparent_string_hash.h>
4
#include <dftracer/utils/core/coro/channel.h>
5
#include <dftracer/utils/core/pipeline/executor.h>
6
#include <dftracer/utils/core/tasks/coro_scope.h>
7
#include <dftracer/utils/server/cursor.h>
8
#include <dftracer/utils/server/http_request.h>
9
#include <dftracer/utils/server/http_response.h>
10
#include <dftracer/utils/server/json_builder.h>
11
#include <dftracer/utils/server/router.h>
12
#include <dftracer/utils/server/trace_api.h>
13
#include <dftracer/utils/server/trace_index.h>
14
#include <dftracer/utils/utilities/common/json/json_doc_guard.h>
15
#include <dftracer/utils/utilities/common/json/json_value.h>
16
#include <dftracer/utils/utilities/common/query/query.h>
17
#include <dftracer/utils/utilities/composites/dft/indexing/index_resolver_utility.h>
18
#include <dftracer/utils/utilities/composites/dft/internal/utils.h>
19
#include <dftracer/utils/utilities/composites/dft/statistics/shared_index_statistics_reader.h>
20
#include <dftracer/utils/utilities/composites/dft/statistics/statistics_aggregator_utility.h>
21
#include <dftracer/utils/utilities/composites/dft/statistics/statistics_query_utility.h>
22
#include <dftracer/utils/utilities/composites/dft/views/view_builder_utility.h>
23
#include <dftracer/utils/utilities/composites/dft/views/view_definition.h>
24
#include <dftracer/utils/utilities/composites/dft/views/view_reader_utility.h>
25
#include <dftracer/utils/utilities/fileio/lines/sources/async_streaming_gz_line_generator.h>
26
#include <simdjson.h>
27

28
#include <cstddef>
29
#include <limits>
30
#include <mutex>
31
#include <string>
32
#include <unordered_map>
33
#include <unordered_set>
34
#include <vector>
35

36
namespace dftracer::utils::server {
37

38
using namespace dftracer::utils::utilities::composites::dft;
39
using namespace dftracer::utils::utilities::composites::dft::indexing;
40
using namespace dftracer::utils::utilities::composites::dft::statistics;
41
using namespace dftracer::utils::utilities::composites::dft::views;
42

43
// Hash metadata types that need smart filtering (FH, HH, SH).
44
static const std::unordered_set<std::string> HASH_METADATA_NAMES = {"FH", "HH",
45
                                                                    "SH"};
46

47
using dftracer::utils::utilities::common::query::Query;
48

49
// --- GET /api/v1/files ---
50
static coro::CoroTask<HttpResponse> handle_files(const HttpRequest& /*req*/,
21!
51
                                                 const QueryParams& /*params*/,
52
                                                 TraceIndex& index) {
53
    auto& b = scratch_json_builder();
54
    b.start_object();
55
    b.escape_and_append_with_quotes("files");
56
    b.append_colon();
57
    b.start_array();
58
    bool first = true;
59
    for (const auto& f : index.files()) {
60
        if (!first) b.append_comma();
61
        first = false;
62
        b.start_object();
63
        b.append_key_value("path", f.path);
64
        b.append_comma();
65
        b.append_key_value("has_bloom_data", f.has_bloom_data);
66
        b.append_comma();
67
        b.append_key_value("has_checkpoint_index", f.has_checkpoint_index);
68
        b.end_object();
69
    }
70
    b.end_array();
71
    b.append_comma();
72
    b.append_key_value("count", static_cast<std::int64_t>(index.file_count()));
73
    b.end_object();
74
    co_return HttpResponse::ok(std::string(b));
75
}
42!
76

77
// --- GET /api/v1/files/info ---
78
static coro::CoroTask<HttpResponse> handle_file_info(const HttpRequest& /*req*/,
2!
79
                                                     const QueryParams& params,
80
                                                     TraceIndex& index) {
81
    auto file_param = params.get("file");
82
    if (file_param.empty()) {
83
        co_return HttpResponse::bad_request("Missing required parameter: file");
84
    }
85

86
    std::string file_path(file_param);
87
    auto* info = index.find_file(file_path);
88
    if (!info) {
89
        co_return HttpResponse::not_found();
90
    }
91

92
    auto& b = scratch_json_builder();
93
    b.start_object();
94
    b.append_key_value("path", info->path);
95
    b.append_comma();
96
    b.append_key_value("has_bloom_data", info->has_bloom_data);
97
    b.append_comma();
98
    b.append_key_value("has_checkpoint_index", info->has_checkpoint_index);
99
    b.append_comma();
100
    b.append_key_value("size_mb", info->size_mb);
101
    b.append_comma();
102
    b.append_key_value("compressed_size",
103
                       static_cast<std::int64_t>(info->compressed_size));
104
    b.append_comma();
105
    b.append_key_value("num_lines", static_cast<std::int64_t>(info->num_lines));
106
    b.append_comma();
107
    b.append_key_value("num_checkpoints",
108
                       static_cast<std::int64_t>(info->num_checkpoints));
109
    b.append_comma();
110
    b.append_key_value("uncompressed_size",
111
                       static_cast<std::int64_t>(info->uncompressed_size));
112
    b.end_object();
113
    co_return HttpResponse::ok(std::string(b));
114
}
4!
115

116
static std::vector<std::string> split_csv(std::string_view s) {
×
117
    std::vector<std::string> result;
×
118
    std::string token;
×
119
    for (char c : s) {
×
120
        if (c == ',') {
×
121
            if (!token.empty()) result.push_back(token);
×
122
            token.clear();
×
123
        } else {
124
            token += c;
×
125
        }
126
    }
127
    if (!token.empty()) result.push_back(token);
×
128
    return result;
×
129
}
×
130

131
static std::string format_in_clause(const std::string& field,
×
132
                                    const std::vector<std::string>& vals) {
133
    if (vals.size() == 1) return field + " == \"" + vals[0] + "\"";
×
134
    std::string s = field + " in [";
×
135
    for (std::size_t i = 0; i < vals.size(); ++i) {
×
136
        if (i > 0) s += ", ";
×
137
        s += "\"" + vals[i] + "\"";
×
138
    }
139
    s += "]";
×
140
    return s;
×
141
}
×
142

143
static std::optional<Query> build_query_from_params(const QueryParams& params) {
6✔
144
    std::string dsl;
6✔
145

146
    auto cat = params.get("cat");
6!
147
    if (!cat.empty()) {
6!
148
        auto vals = split_csv(cat);
×
149
        if (!vals.empty()) dsl += format_in_clause("cat", vals);
×
150
    }
×
151

152
    auto name = params.get("name");
6!
153
    if (!name.empty()) {
6!
154
        auto vals = split_csv(name);
×
155
        if (!vals.empty()) {
×
156
            if (!dsl.empty()) dsl += " and ";
×
157
            dsl += format_in_clause("name", vals);
×
158
        }
159
    }
×
160

161
    auto pid = params.get("pid");
6!
162
    if (!pid.empty()) {
6!
163
        if (!dsl.empty()) dsl += " and ";
×
164
        dsl += "pid == " + std::string(pid);
×
165
    }
166

167
    double ts_min = params.get_double("ts_min", 0);
6!
168
    double ts_max = params.get_double("ts_max", 0);
6!
169
    if (ts_min > 0) {
6!
170
        if (!dsl.empty()) dsl += " and ";
×
171
        dsl += "ts >= " + std::to_string(static_cast<uint64_t>(ts_min));
×
172
    }
173
    if (ts_max > 0) {
6!
174
        if (!dsl.empty()) dsl += " and ";
×
175
        dsl += "ts <= " + std::to_string(static_cast<uint64_t>(ts_max));
×
176
    }
177

178
    double dur_min = params.get_double("dur_min", 0);
6!
179
    double dur_max = params.get_double("dur_max", 0);
6!
180
    if (dur_min > 0) {
6!
181
        if (!dsl.empty()) dsl += " and ";
×
182
        dsl += "dur >= " + std::to_string(static_cast<uint64_t>(dur_min));
×
183
    }
184
    if (dur_max > 0) {
6!
185
        if (!dsl.empty()) dsl += " and ";
×
186
        dsl += "dur <= " + std::to_string(static_cast<uint64_t>(dur_max));
×
187
    }
188

189
    if (dsl.empty()) return std::nullopt;
6!
190
    auto result = Query::from_string(dsl);
×
191
    if (!result) return std::nullopt;
×
192
    return std::move(*result);
×
193
}
6✔
194

195
static ViewDefinition build_view_from_params(const QueryParams& params) {
3✔
196
    ViewDefinition view;
3✔
197
    view.name = "api_query";
3!
198
    view.description = "HTTP API query";
3!
199

200
    auto q = build_query_from_params(params);
3!
201
    if (q) view.with_query(std::move(*q));
3!
202
    return view;
6✔
203
}
3✔
204

205
// ============================================================================
206
// Shared helpers for event streaming endpoints
207
// ============================================================================
208

209
static std::vector<const TraceIndex::FileInfo*> resolve_target_files(
3✔
210
    TraceIndex& index, const QueryParams& params, double ts_min = 0,
211
    double ts_max = 0) {
212
    auto files = collect_candidate_files(index, params);
3✔
213

214
    if (ts_min > 0 || ts_max > 0) {
3!
215
        std::vector<const TraceIndex::FileInfo*> filtered;
×
216
        filtered.reserve(files.size());
×
217
        for (auto* fi : files) {
×
218
            if (fi->min_timestamp_us == 0 && fi->max_timestamp_us == 0) {
×
219
                filtered.push_back(fi);
×
220
                continue;
×
221
            }
222
            double fi_min = static_cast<double>(fi->min_timestamp_us);
×
223
            double fi_max = static_cast<double>(fi->max_timestamp_us);
×
224
            if (fi_max < ts_min || (ts_max > 0 && fi_min > ts_max)) continue;
×
225
            filtered.push_back(fi);
×
226
        }
227
        files = std::move(filtered);
×
228
    }
×
229

230
    return files;
3✔
UNCOV
231
}
×
232

233
using StreamChunk = HttpResponse::StreamChunk;
234

235
static coro::AsyncGenerator<StreamChunk> stream_events(
3!
236
    std::vector<const TraceIndex::FileInfo*> files, ViewDefinition ev_view,
237
    std::optional<Query> /*query_opt*/, double ts_min, double ts_max,
238
    BloomFilterCache* bloom_cache, int limit, std::size_t max_concurrent,
239
    CancelToken cancel) {
240
    int emitted = 0;
241
    const std::size_t slots = std::max<std::size_t>(1, max_concurrent);
242

243
    // Prune a batch of files in parallel (bloom/index I/O dominates and is
244
    // serial otherwise), then stream matches in file order so the `limit`
245
    // early-exit still stops after the first satisfying files.
246
    for (std::size_t base = 0; base < files.size(); base += slots) {
247
        if (cancel.cancelled()) co_return;
248
        if (limit > 0 && emitted >= limit) break;
249

250
        const std::size_t batch_end = std::min(base + slots, files.size());
251
        const std::size_t batch_n = batch_end - base;
252
        std::vector<ViewBuilderOutput> outs(batch_n);
253

254
        {
255
            CoroScope scope(Executor::current());
256
            for (std::size_t i = 0; i < batch_n; ++i) {
257
                auto* fi = files[base + i];
258
                if (fi->uncompressed_size == 0 && fi->num_checkpoints == 0)
259
                    continue;
260
                auto* slot = &outs[i];
261
                scope.spawn([fi, slot, &ev_view, bloom_cache, ts_min,
3!
262
                             ts_max](CoroScope&) -> coro::CoroTask<void> {
263
                    ViewBuilderInput builder_input;
264
                    builder_input.with_view(ev_view)
265
                        .with_file_path(fi->path)
266
                        .with_index_path(fi->has_bloom_data ? fi->index_path
267
                                                            : "")
268
                        .with_uncompressed_size(fi->uncompressed_size)
269
                        .with_num_checkpoints(fi->num_checkpoints)
270
                        .with_bloom_cache(bloom_cache)
271
                        .with_time_range(ts_min, ts_max);
272
                    ViewBuilderUtility builder;
273
                    auto r = co_await builder.process(builder_input);
274
                    if (r) *slot = std::move(*r);
275
                });
6!
276
            }
277
            co_await scope.join();
278
        }
279

280
        for (std::size_t i = 0; i < batch_n; ++i) {
281
            if (cancel.cancelled()) co_return;
282
            if (limit > 0 && emitted >= limit) break;
283
            if (!outs[i].file_may_match) continue;
284
            auto* file_info = files[base + i];
285

286
            for (const auto& candidate : outs[i].candidates) {
287
                if (limit > 0 && emitted >= limit) break;
288

289
                ViewReaderInput reader_input;
290
                reader_input.with_file_path(file_info->path)
291
                    .with_index_path(file_info->index_path)
292
                    .with_byte_range(candidate.start_byte, candidate.end_byte)
293
                    .with_checkpoint_idx(candidate.checkpoint_idx)
294
                    .with_view(ev_view);
295

296
                ViewReaderUtility reader;
297
                auto event_gen = reader.process(reader_input);
298
                while (auto batch = co_await event_gen.next()) {
299
                    if (cancel.cancelled()) co_return;
300
                    int count = std::min(
301
                        static_cast<int>(batch->events.size()),
302
                        limit > 0 ? limit - emitted
303
                                  : static_cast<int>(batch->events.size()));
304
                    if (count > 0) {
305
                        co_yield StreamChunk{std::span<const std::string_view>(
306
                            batch->events.data(),
307
                            static_cast<std::size_t>(count))};
308
                        emitted += count;
309
                    }
310
                }
311
            }
312
        }
313
    }
314
}
6!
315

316
// ============================================================================
317
// Event endpoints
318
// ============================================================================
319

320
// --- GET /api/v1/events ---
321
static coro::CoroTask<HttpResponse> handle_events(const HttpRequest& req,
1!
322
                                                  const QueryParams& params,
323
                                                  TraceIndex& index) {
324
    int limit = params.get_int("limit", 1000);
325
    if (limit <= 0) limit = 1000;
326
    if (limit > 100000) limit = 100000;
327

328
    double ts_min = params.get_double("ts_min", 0);
329
    double ts_max = params.get_double("ts_max", 0);
330
    auto files = resolve_target_files(index, params, ts_min, ts_max);
331
    auto view = build_view_from_params(params);
332
    auto query = build_query_from_params(params);
333

334
    auto gen = std::make_unique<HttpResponse::StreamGenerator>(stream_events(
335
        std::move(files), std::move(view), std::move(query), ts_min, ts_max,
336
        &index.bloom_cache(), limit, index.max_concurrent(), req.cancel_token));
337

338
    auto resp = HttpResponse::streaming(std::move(gen));
339
    resp.headers.push_back({"X-Limit", std::to_string(limit)});
340
    co_return resp;
341
}
2!
342

343
// --- GET /api/v1/events/stream ---
344
static coro::CoroTask<HttpResponse> handle_events_stream(
2!
345
    const HttpRequest& req, const QueryParams& params, TraceIndex& index) {
346
    double ts_min = params.get_double("ts_min", 0);
347
    double ts_max = params.get_double("ts_max", 0);
348
    auto files = resolve_target_files(index, params, ts_min, ts_max);
349
    auto view = build_view_from_params(params);
350
    auto query = build_query_from_params(params);
351
    int limit = params.get_int("limit", 0);
352

353
    auto gen = std::make_unique<HttpResponse::StreamGenerator>(stream_events(
354
        std::move(files), std::move(view), std::move(query), ts_min, ts_max,
355
        &index.bloom_cache(), limit, index.max_concurrent(), req.cancel_token));
356

357
    co_return HttpResponse::streaming(std::move(gen));
358
}
4!
359

360
// --- GET /api/v1/stats ---
361
static coro::CoroTask<HttpResponse> handle_stats(const HttpRequest& req,
1!
362
                                                 const QueryParams& /*params*/,
363
                                                 TraceIndex& index) {
364
    static std::mutex cache_mutex;
365
    static std::unordered_map<std::string, std::string,
366
                              dftracer::utils::TransparentStringHash,
367
                              dftracer::utils::TransparentStringEqual>
368
        stats_cache;
369

370
    {
371
        std::lock_guard<std::mutex> lock(cache_mutex);
372
        auto it = stats_cache.find(req.path);
373
        if (it != stats_cache.end()) {
374
            co_return HttpResponse::ok(it->second);
375
        }
376
    }
377

378
    std::vector<TraceStatistics> all_stats;
379

380
    // Group files by index_path
381
    std::unordered_map<std::string,
382
                       std::vector<std::pair<std::size_t, std::string>>>
383
        files_by_index;
384
    std::size_t file_idx = 0;
385
    for (const auto& file_info : index.files()) {
386
        if (!file_info.has_bloom_data) continue;
387
        files_by_index[file_info.index_path].emplace_back(file_idx++,
388
                                                          file_info.path);
389
    }
390

391
    // Resolve each group and read statistics
392
    for (auto& [idx_path, files] : files_by_index) {
393
        if (req.cancel_token.cancelled()) co_return HttpResponse::ok("{}");
394
        std::vector<std::string> file_paths;
395
        file_paths.reserve(files.size());
396
        for (const auto& [_, path] : files) {
397
            file_paths.push_back(path);
398
        }
399

400
        IndexResolverUtility resolver;
401
        ResolverInput input;
402
        input.files = std::move(file_paths);
403
        input.require_checkpoints = false;
404

405
        auto result = co_await resolver.process(input);
406

407
        if (result.cached.empty()) {
408
            continue;
409
        }
410

411
        try {
412
            SharedIndexStatisticsReader reader;
413
            auto batch_rows = co_await reader.query(
414
                result.index_path, std::move(result.cached),
415
                StatisticsQueryType::SUMMARY);
416
            auto callback = [&all_stats](std::size_t /*file_index*/,
1✔
417
                                         TraceStatistics&& stats) {
1✔
418
                if (stats.success) {
1!
419
                    all_stats.push_back(std::move(stats));
1✔
420
                }
421
            };
1✔
422
            SharedIndexStatisticsReader::process_batch_results(batch_rows,
423
                                                               callback);
424
        } catch (const std::exception& e) {
425
            DFTRACER_UTILS_LOG_WARN("Server stats batch read failed for %s: %s",
426
                                    idx_path.c_str(), e.what());
427
        }
428
    }
429

430
    std::uint64_t total_events = 0;
431
    std::size_t file_count = all_stats.size();
432
    for (const auto& s : all_stats) {
433
        total_events += s.total_events();
434
    }
435

436
    auto& sb = scratch_json_builder();
437
    sb.start_object();
438
    sb.append_key_value("file_count", static_cast<std::int64_t>(file_count));
439
    sb.append_comma();
440
    sb.append_key_value("total_events",
441
                        static_cast<std::int64_t>(total_events));
442
    sb.append_comma();
443
    sb.escape_and_append_with_quotes("files");
444
    sb.append_colon();
445
    sb.start_array();
446
    for (std::size_t i = 0; i < all_stats.size(); ++i) {
447
        if (i > 0) sb.append_comma();
448
        sb.append_raw(all_stats[i].to_json());  // already JSON
449
    }
450
    sb.end_array();
451
    sb.end_object();
452
    std::string body(sb);
453

454
    {
455
        std::lock_guard<std::mutex> lock(cache_mutex);
456
        stats_cache.emplace(std::string(req.path), body);
457
    }
458
    co_return HttpResponse::ok(body);
459
}
2!
460

461
// --- GET /api/v1/info ---
462
static coro::CoroTask<HttpResponse> handle_info(const HttpRequest& /*req*/,
2!
463
                                                const QueryParams& /*params*/,
464
                                                TraceIndex& index) {
465
    auto global_min_native = index.global_min_timestamp_us();
466
    auto global_max_native = index.global_max_timestamp_us();
467
    bool has_time_range =
468
        global_max_native > 0 &&
469
        global_min_native != std::numeric_limits<std::uint64_t>::max();
470
    // Client works in microseconds; scale native (index-unit) bounds to us.
471
    auto global_min = index.native_to_us(global_min_native);
472
    auto global_max = index.native_to_us(global_max_native);
473

474
    auto& b = scratch_json_builder();
475
    b.start_object();
476
    b.append_key_value("file_count",
477
                       static_cast<std::int64_t>(index.file_count()));
478

479
    if (has_time_range) {
480
        b.append_comma();
481
        b.escape_and_append_with_quotes("time_range");
482
        b.append_colon();
483
        b.start_object();
484
        b.append_key_value("min_timestamp_us",
485
                           static_cast<std::int64_t>(global_min));
486
        b.append_comma();
487
        b.append_key_value("max_timestamp_us",
488
                           static_cast<std::int64_t>(global_max));
489
        b.end_object();
490
    }
491

492
    b.append_comma();
493
    b.escape_and_append_with_quotes("files");
494
    b.append_colon();
495
    b.start_array();
496
    bool first = true;
497
    for (const auto& f : index.files()) {
498
        if (!first) b.append_comma();
499
        first = false;
500
        b.start_object();
501
        b.append_key_value("path", f.path);
502
        b.append_comma();
503
        b.append_key_value("has_bloom_data", f.has_bloom_data);
504
        b.append_comma();
505
        b.append_key_value("has_checkpoint_index", f.has_checkpoint_index);
506
        if (f.min_timestamp_us > 0 || f.max_timestamp_us > 0) {
507
            b.append_comma();
508
            b.append_key_value("min_timestamp_us",
509
                               static_cast<std::int64_t>(
510
                                   index.native_to_us(f.min_timestamp_us)));
511
            b.append_comma();
512
            b.append_key_value("max_timestamp_us",
513
                               static_cast<std::int64_t>(
514
                                   index.native_to_us(f.max_timestamp_us)));
515
        }
516
        b.end_object();
517
    }
518
    b.end_array();
519
    b.end_object();
520
    co_return HttpResponse::ok(std::string(b));
521
}
4!
522

523
void register_trace_api(Router& router, TraceIndex& index) {
11✔
524
    auto* index_ptr = &index;
11✔
525

526
    router.get(
11!
527
        "/api/v1/files",
528
        [index_ptr](const HttpRequest& req,
21!
529
                    const QueryParams& params) -> coro::CoroTask<HttpResponse> {
530
            co_return co_await handle_files(req, params, *index_ptr);
531
        },
42!
532
        RouteDoc{
22!
533
            "List the indexed trace files.",
534
            "Trace data",
535
            {},
536
            R"({"files":[{"path":"trace-0.pfw.gz","has_bloom_data":true}],)"
537
            R"("count":1})"});
538

539
    router.get(
11!
540
        "/api/v1/files/info",
541
        [index_ptr](const HttpRequest& req,
2!
542
                    const QueryParams& params) -> coro::CoroTask<HttpResponse> {
543
            co_return co_await handle_file_info(req, params, *index_ptr);
544
        },
4!
545
        RouteDoc{"Metadata for one trace file.",
44!
546
                 "Trace data",
547
                 {{"file", "Trace file path", true, ""}},
548
                 R"({"path":"trace-0.pfw.gz","has_bloom_data":true})"});
22✔
549

550
    router.get(
11!
551
        "/api/v1/events",
552
        [index_ptr](const HttpRequest& req,
1!
553
                    const QueryParams& params) -> coro::CoroTask<HttpResponse> {
554
            co_return co_await handle_events(req, params, *index_ptr);
555
        },
2!
556
        RouteDoc{"Query raw events as NDJSON (filtered, limited).",
77!
557
                 "Trace data",
558
                 {{"file", "Trace file path", false, ""},
559
                  {"name", "Filter by operation name", false, "read"},
560
                  {"dur_min", "Minimum duration (us)", false, ""},
561
                  {"limit", "Max events (0 = all)", false, "20"}},
562
                 R"({"id":1,"name":"read","cat":"POSIX","pid":100,"tid":100,)"
563
                 R"("ts":1000,"dur":150,"args":{"ret":4096}})"});
55✔
564

565
    router.get(
11!
566
        "/api/v1/events/stream",
567
        [index_ptr](const HttpRequest& req,
2!
568
                    const QueryParams& params) -> coro::CoroTask<HttpResponse> {
569
            co_return co_await handle_events_stream(req, params, *index_ptr);
570
        },
4!
571
        RouteDoc{"Stream all matching events as NDJSON (no limit).",
44!
572
                 "Trace data",
573
                 {{"name", "Filter by operation name", false, ""}},
574
                 ""});
22✔
575

576
    router.get(
11!
577
        "/api/v1/stats",
578
        [index_ptr](const HttpRequest& req,
1!
579
                    const QueryParams& params) -> coro::CoroTask<HttpResponse> {
580
            co_return co_await handle_stats(req, params, *index_ptr);
581
        },
2!
582
        RouteDoc{"Aggregate statistics over the index.", "Trace data", {}, ""});
22!
583

584
    router.get(
11!
585
        "/api/v1/info",
586
        [index_ptr](const HttpRequest& req,
2!
587
                    const QueryParams& params) -> coro::CoroTask<HttpResponse> {
588
            co_return co_await handle_info(req, params, *index_ptr);
589
        },
4!
590
        RouteDoc{"Global summary: file count and time bounds.",
22!
591
                 "Trace data",
592
                 {},
593
                 R"({"file_count":2,"global_min_timestamp_us":1000000,)"
594
                 R"("global_max_timestamp_us":6999732})"});
595

596
    router.get(
11!
597
        "/api/v1/resolve",
NEW
598
        [index_ptr](const HttpRequest& /*req*/,
×
599
                    const QueryParams& params) -> coro::CoroTask<HttpResponse> {
600
            std::string hashes(params.get("hash"));
601
            if (hashes.empty())
602
                co_return HttpResponse::bad_request("Missing parameter: hash");
603
            auto kind = params.get("type");
604
            using HashType = TraceIndex::HashType;
605
            HashType type = HashType::FILE;
606
            if (kind == "host")
607
                type = HashType::HOST;
608
            else if (kind == "string")
609
                type = HashType::STRING;
610
            else if (kind == "proc")
611
                type = HashType::PROC;
612
            else if (!kind.empty() && kind != "file")
613
                co_return HttpResponse::bad_request("Invalid type: " +
614
                                                    std::string(kind));
615

616
            auto& b = scratch_json_builder();
617
            b.start_object();
618
            b.escape_and_append_with_quotes("names");
619
            b.append_colon();
620
            b.start_object();
621
            bool first = true;
622
            std::size_t start = 0;
623
            // Comma-separated so one click can resolve its file and host at
624
            // once; unknown hashes are simply absent from the reply.
625
            while (start <= hashes.size()) {
626
                auto end = hashes.find(',', start);
627
                if (end == std::string::npos) end = hashes.size();
628
                std::string one = hashes.substr(start, end - start);
629
                start = end + 1;
630
                if (one.empty()) continue;
631
                auto name = index_ptr->resolve_hash(type, one);
632
                if (name.empty()) continue;
633
                if (!first) b.append_comma();
634
                first = false;
635
                b.escape_and_append_with_quotes(one);
636
                b.append_colon();
637
                b.escape_and_append_with_quotes(name);
638
            }
639
            b.end_object();
640
            b.end_object();
641
            co_return HttpResponse::ok(std::string(b));
NEW
642
        },
×
643
        RouteDoc{
55!
644
            "Resolve content hashes (file/host/string) to their names.",
645
            "Control",
646
            {{"hash", "Hash, or several separated by commas", true, ""},
647
             {"type", "file (default), host, string or proc", false, "file"}},
648
            R"({"names":{"314c1a1cdb22a136":"/data/train/img_0.npz"}})"});
33✔
649

650
    router.post(
11!
651
        "/api/v1/cancel",
652
        [](const HttpRequest& /*req*/,
1!
653
           const QueryParams& params) -> coro::CoroTask<HttpResponse> {
654
            std::string id(params.get("id"));
655
            bool found = CancelRegistry::instance().cancel(id);
656
            co_return HttpResponse::ok(std::string("{\"cancelled\":") +
657
                                       (found ? "true" : "false") + "}");
658
        },
2!
659
        RouteDoc{"Cancel an in-flight request by its X-Request-Id.",
44!
660
                 "Control",
661
                 {{"id", "Request id to cancel", true, ""}},
662
                 R"({"cancelled":true})"});
22✔
663
}
11✔
664

665
}  // namespace dftracer::utils::server
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc