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

tstack / lnav / 17589970077-2502

09 Sep 2025 05:00PM UTC coverage: 65.196% (-5.0%) from 70.225%
17589970077-2502

push

github

tstack
[format] add fields for source file/line

Knowing the source file/line context in a log
message can help find log messages when using
log2src.

56 of 70 new or added lines in 2 files covered. (80.0%)

13954 existing lines in 210 files now uncovered.

45516 of 69814 relevant lines covered (65.2%)

404154.37 hits per line

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

72.83
/src/log.annotate.cc
1
/**
2
 * Copyright (c) 2023, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include <future>
31

32
#include "log.annotate.hh"
33

34
#include "base/auto_fd.hh"
35
#include "base/auto_pid.hh"
36
#include "base/fs_util.hh"
37
#include "base/paths.hh"
38
#include "line_buffer.hh"
39
#include "lnav.hh"
40
#include "log.annotate.cfg.hh"
41
#include "log_data_helper.hh"
42
#include "md4cpp.hh"
43
#include "readline_highlighters.hh"
44
#include "yajlpp/yajlpp.hh"
45

46
namespace lnav::log::annotate {
47

48
struct compiled_cond_expr {
49
    auto_mem<sqlite3_stmt> cce_stmt{sqlite3_finalize};
50
    bool cce_enabled{true};
51
};
52

53
struct expressions : lnav_config_listener {
54
    expressions() : lnav_config_listener(__FILE__) {}
1,091✔
55

56
    void reload_config(error_reporter& reporter) override
607✔
57
    {
58
        auto& lnav_db = injector::get<auto_sqlite3&>();
607✔
59

60
        if (lnav_db.in() == nullptr) {
607✔
UNCOV
61
            log_warning("db not initialized yet!");
×
62
            return;
×
63
        }
64

65
        const auto& cfg = injector::get<const config&>();
607✔
66

67
        this->e_cond_exprs.clear();
607✔
68
        for (const auto& pair : cfg.a_definitions) {
1,871✔
69
            if (pair.second.a_handler.pp_value.empty()) {
1,264✔
70
                auto um
71
                    = lnav::console::user_message::error(
2✔
72
                          "no handler specified for annotation")
73
                          .with_reason("Every annotation requires a handler")
2✔
74
                          .move();
1✔
75
                reporter(&pair.second.a_handler, um);
1✔
76
                continue;
1✔
77
            }
1✔
78

79
            auto stmt_str = fmt::format(FMT_STRING("SELECT 1 WHERE {}"),
2,526✔
80
                                        pair.second.a_condition);
1,263✔
81
            compiled_cond_expr cce;
1,263✔
82

83
            log_info("preparing annotation condition expression: %s",
1,263✔
84
                     stmt_str.c_str());
85
            auto retcode = sqlite3_prepare_v2(lnav_db,
2,526✔
86
                                              stmt_str.c_str(),
87
                                              stmt_str.size(),
1,263✔
88
                                              cce.cce_stmt.out(),
89
                                              nullptr);
90
            if (retcode != SQLITE_OK) {
1,263✔
91
                auto sql_al = attr_line_t(pair.second.a_condition)
2✔
92
                                  .with_attr_for_all(SA_PREFORMATTED.value())
2✔
93
                                  .with_attr_for_all(
1✔
94
                                      VC_ROLE.value(role_t::VCR_QUOTED_CODE))
2✔
95
                                  .move();
1✔
96
                readline_sql_highlighter(
1✔
97
                    sql_al, lnav::sql::dialect::sqlite, std::nullopt);
98
                intern_string_t cond_expr_path = intern_string::lookup(
99
                    fmt::format(FMT_STRING("/log/annotations/{}/condition"),
3✔
100
                                pair.first));
2✔
101
                auto snippet = lnav::console::snippet::from(
102
                    source_location(cond_expr_path), sql_al);
1✔
103

104
                auto um = lnav::console::user_message::error(
2✔
105
                              "SQL expression is invalid")
106
                              .with_reason(sqlite3_errmsg(lnav_db))
2✔
107
                              .with_snippet(snippet)
1✔
108
                              .move();
1✔
109

110
                reporter(&pair.second.a_condition, um);
1✔
111
                continue;
1✔
112
            }
1✔
113

114
            this->e_cond_exprs.emplace(pair.first, std::move(cce));
1,262✔
115
        }
1,264✔
116
    }
117

118
    void unload_config() override { this->e_cond_exprs.clear(); }
693✔
119

120
    std::map<intern_string_t, compiled_cond_expr> e_cond_exprs;
121
};
122

123
static expressions exprs;
124

125
std::vector<intern_string_t>
126
applicable(vis_line_t vl)
3✔
127
{
128
    std::vector<intern_string_t> retval;
3✔
129
    auto& lss = lnav_data.ld_log_source;
3✔
130
    auto cl = lss.at(vl);
3✔
131
    auto ld = lss.find_data(cl);
3✔
132
    log_data_helper ldh(lss);
3✔
133

134
    ldh.parse_line(vl, true);
3✔
135
    for (auto& expr : exprs.e_cond_exprs) {
10✔
136
        if (!expr.second.cce_enabled) {
7✔
UNCOV
137
            continue;
×
138
        }
139

140
        auto eval_res
141
            = lss.eval_sql_filter(expr.second.cce_stmt.in(), ld, ldh.ldh_line);
7✔
142

143
        if (eval_res.isErr()) {
7✔
UNCOV
144
            log_error("eval failed: %s",
×
145
                      eval_res.unwrapErr().to_attr_line().get_string().c_str());
UNCOV
146
            expr.second.cce_enabled = false;
×
147
        } else {
148
            if (eval_res.unwrap()) {
7✔
149
                retval.emplace_back(expr.first);
3✔
150
            }
151
        }
152
    }
7✔
153
    return retval;
6✔
154
}
3✔
155

156
Result<void, lnav::console::user_message>
157
apply(vis_line_t vl, std::vector<intern_string_t> annos)
3✔
158
{
159
    const auto& cfg = injector::get<const config&>();
3✔
160
    auto& lss = lnav_data.ld_log_source;
3✔
161
    auto cl = lss.at(vl);
3✔
162
    auto ld = lss.find_data(cl);
3✔
163
    auto lf = (*ld)->get_file();
3✔
164
    logmsg_annotations la;
3✔
165
    log_data_helper ldh(lss);
3✔
166

167
    if (!ldh.parse_line(vl, true)) {
3✔
UNCOV
168
        log_error("failed to parse line %d", vl);
×
UNCOV
169
        return Err(lnav::console::user_message::error("Failed to parse line"));
×
170
    }
171
    auto line_number = content_line_t{ldh.ldh_line_index - ldh.ldh_y_offset};
3✔
172
    lss.set_user_mark(&textview_curses::BM_META,
3✔
173
                      content_line_t{ldh.ldh_source_line - ldh.ldh_y_offset});
3✔
174

175
    yajlpp_gen gen;
3✔
176

177
    {
178
        auto bm_opt = lss.find_bookmark_metadata(vl);
3✔
179
        yajlpp_map root(gen);
3✔
180

181
        root.gen("log_line");
3✔
182
        root.gen((int64_t) vl);
3✔
183
        root.gen("log_tags");
3✔
184
        {
185
            yajlpp_array tag_array(gen);
3✔
186

187
            if (bm_opt) {
3✔
UNCOV
188
                const auto& bm = *(bm_opt.value());
×
189

UNCOV
190
                for (const auto& tag : bm.bm_tags) {
×
UNCOV
191
                    tag_array.gen(tag);
×
192
                }
193
            }
194
        }
3✔
195
        root.gen("log_path");
3✔
196
        root.gen(lf->get_filename().native());
3✔
197
        root.gen("log_format");
3✔
198
        root.gen(lf->get_format_name());
3✔
199
        root.gen("log_format_regex");
3✔
200
        root.gen(lf->get_format()->get_pattern_name(line_number));
3✔
201
        root.gen("log_msg");
3✔
202
        root.gen(ldh.ldh_line_values.lvv_sbr.to_string_fragment());
3✔
203
        for (const auto& val : ldh.ldh_line_values.lvv_values) {
33✔
204
            root.gen(val.lv_meta.lvm_name);
30✔
205
            switch (val.lv_meta.lvm_kind) {
30✔
206
                case value_kind_t::VALUE_NULL:
13✔
207
                    root.gen();
13✔
208
                    break;
13✔
209
                case value_kind_t::VALUE_INTEGER:
2✔
210
                    root.gen(val.lv_value.i);
2✔
211
                    break;
2✔
UNCOV
212
                case value_kind_t::VALUE_FLOAT:
×
UNCOV
213
                    root.gen(val.lv_value.d);
×
UNCOV
214
                    break;
×
UNCOV
215
                case value_kind_t::VALUE_BOOLEAN:
×
216
                    root.gen(val.lv_value.i ? true : false);
×
217
                    break;
×
218
                default:
15✔
219
                    root.gen(val.to_string());
15✔
220
                    break;
15✔
221
            }
222
        }
223
    }
3✔
224

225
    for (const auto& anno : annos) {
6✔
226
        auto iter = cfg.a_definitions.find(anno);
3✔
227
        if (iter == cfg.a_definitions.end()) {
3✔
UNCOV
228
            log_error("unknown annotation: %s", anno.c_str());
×
229
            continue;
2✔
230
        }
231

232
        if (startswith(iter->second.a_handler.pp_value, "|")) {
3✔
233
            intern_string_t handler_path = intern_string::lookup(
234
                fmt::format(FMT_STRING("/log/annotations/{}/handler"), anno));
8✔
235
            logline_value_vector values;
2✔
236
            exec_context ec(&values, internal_sql_callback, pipe_callback);
2✔
237
            db_label_source anno_label_source;
2✔
238

239
            ec.with_perms(exec_context::perm_t::READ_ONLY);
2✔
240
            ec.ec_local_vars.push(std::map<std::string, scoped_value_t>());
2✔
241
            ec.ec_top_line = vl;
2✔
242
            auto src_guard = ec.enter_db_source(&anno_label_source);
2✔
243
            auto src_loc = source_location{handler_path};
2✔
244

245
            auto exec_res
246
                = ec.execute(src_loc, iter->second.a_handler.pp_value);
2✔
247
            if (exec_res.isErr()) {
2✔
UNCOV
248
                auto err_msg = exec_res.unwrapErr();
×
249

250
                la.la_pairs[anno.to_string()]
×
251
                    = err_msg.to_attr_line().al_string;
×
252
            } else {
×
253
                auto content = exec_res.unwrap();
2✔
254
                la.la_pairs[anno.to_string()] = content;
2✔
255
            }
2✔
256

257
            lnav_data.ld_views[LNV_LOG].reload_data();
2✔
258
            lnav_data.ld_views[LNV_LOG].set_needs_update();
2✔
259
            continue;
2✔
260
        }
2✔
261

262
        la.la_pairs[anno.to_string()] = "Loading...";
1✔
263
        auto child_fds_res = auto_pipe::for_child_fds(
264
            STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO);
1✔
265
        if (child_fds_res.isErr()) {
1✔
266
            auto um
UNCOV
267
                = lnav::console::user_message::error("unable to create pipes")
×
UNCOV
268
                      .with_reason(child_fds_res.unwrapErr())
×
UNCOV
269
                      .move();
×
270
            return Err(um);
×
271
        }
272

273
        auto child_res = lnav::pid::from_fork();
1✔
274
        if (child_res.isErr()) {
1✔
275
            auto um
276
                = lnav::console::user_message::error("unable to fork() child")
×
277
                      .with_reason(child_res.unwrapErr())
×
278
                      .move();
×
UNCOV
279
            return Err(um);
×
280
        }
281

282
        auto child_fds = child_fds_res.unwrap();
1✔
283

284
        auto child = child_res.unwrap();
1✔
285
        for (auto& child_fd : child_fds) {
4✔
286
            child_fd.after_fork(child.in());
3✔
287
        }
288
        if (child.in_child()) {
1✔
UNCOV
289
            const char* exec_args[] = {
×
UNCOV
290
                getenv_opt("SHELL").value_or("bash"),
×
291
                "-c",
UNCOV
292
                iter->second.a_handler.pp_value.c_str(),
×
293
                nullptr,
294
            };
295

UNCOV
296
            std::vector<std::filesystem::path> path_v;
×
297

298
            auto src_path
299
                = std::filesystem::path(
×
UNCOV
300
                      iter->second.a_handler.pp_location.sl_source.to_string())
×
UNCOV
301
                      .parent_path();
×
UNCOV
302
            path_v.push_back(src_path);
×
UNCOV
303
            path_v.push_back(lnav::paths::dotlnav() / "formats/default");
×
UNCOV
304
            auto path_var = lnav::filesystem::build_path(path_v);
×
305

UNCOV
306
            log_debug("annotate PATH: %s", path_var.c_str());
×
UNCOV
307
            setenv("PATH", path_var.c_str(), 1);
×
308
            execvp(exec_args[0], (char**) exec_args);
×
UNCOV
309
            _exit(EXIT_FAILURE);
×
310
        }
×
311

312
        auto out_reader = std::async(
313
            std::launch::async,
314
            [out_fd = std::move(child_fds[1].read_end())]() mutable {
2✔
315
                std::string retval;
1✔
316
                file_range last_range;
1✔
317
                line_buffer lb;
1✔
318

319
                lb.set_fd(out_fd);
1✔
320
                while (true) {
321
                    auto load_res = lb.load_next_line(last_range);
2✔
322
                    if (load_res.isErr()) {
2✔
UNCOV
323
                        log_error("unable to load next line: %s",
×
324
                                  load_res.unwrapErr().c_str());
UNCOV
325
                        break;
×
326
                    }
327

328
                    auto li = load_res.unwrap();
2✔
329
                    if (li.li_file_range.empty()) {
2✔
330
                        break;
1✔
331
                    }
332
                    auto read_res = lb.read_range(li.li_file_range);
1✔
333
                    if (read_res.isErr()) {
1✔
334
                        log_error("unable to read next line: %s",
×
335
                                  load_res.unwrapErr().c_str());
336
                        break;
×
337
                    }
338

339
                    auto sbr = read_res.unwrap();
1✔
340
                    retval.append(sbr.get_data(), sbr.length());
1✔
341

342
                    last_range = li.li_file_range;
1✔
343
                }
3✔
344

345
                return retval;
2✔
346
            });
2✔
347

348
        auto err_reader = std::async(
349
            std::launch::async,
350
            [err_fd = std::move(child_fds[2].read_end()),
2✔
351
             handler = iter->second.a_handler.pp_value]() mutable {
1✔
352
                std::string retval;
1✔
353
                file_range last_range;
1✔
354
                line_buffer lb;
1✔
355

356
                lb.set_fd(err_fd);
1✔
357
                while (true) {
358
                    auto load_res = lb.load_next_line(last_range);
1✔
359
                    if (load_res.isErr()) {
1✔
UNCOV
360
                        log_error("unable to load next line: %s",
×
361
                                  load_res.unwrapErr().c_str());
UNCOV
362
                        break;
×
363
                    }
364

365
                    auto li = load_res.unwrap();
1✔
366
                    if (li.li_file_range.empty()) {
1✔
367
                        break;
1✔
368
                    }
UNCOV
369
                    auto read_res = lb.read_range(li.li_file_range);
×
UNCOV
370
                    if (read_res.isErr()) {
×
UNCOV
371
                        log_error("unable to read next line: %s",
×
372
                                  load_res.unwrapErr().c_str());
UNCOV
373
                        break;
×
374
                    }
375

UNCOV
376
                    auto sbr = read_res.unwrap();
×
UNCOV
377
                    retval.append(sbr.get_data(), sbr.length());
×
UNCOV
378
                    sbr.rtrim(is_line_ending);
×
UNCOV
379
                    log_debug("%s: %.*s",
×
380
                              handler.c_str(),
381
                              sbr.length(),
382
                              sbr.get_data());
383

384
                    last_range = li.li_file_range;
×
385
                }
1✔
386

387
                return retval;
2✔
388
            });
2✔
389

390
        auto write_res
391
            = child_fds[0].write_end().write_fully(gen.to_string_fragment());
1✔
392
        if (write_res.isErr()) {
1✔
393
            log_error("bah %s", write_res.unwrapErr().c_str());
×
394
        }
395
        child_fds[0].write_end().reset();
1✔
396
        auto finalizer = [anno,
1✔
397
                          out_reader1 = out_reader.share(),
398
                          err_reader1 = err_reader.share(),
399
                          lf,
400
                          line_number,
401
                          handler = iter->second.a_handler.pp_value](
2✔
402
                             auto& fc,
403
                             auto_pid<process_state::finished>& child) mutable {
404
            auto& line_anno
1✔
405
                = lf->get_bookmark_metadata()[line_number].bm_annotations;
1✔
406
            auto content = out_reader1.get();
1✔
407
            if (!child.was_normal_exit()) {
1✔
UNCOV
408
                content.append(fmt::format(
×
UNCOV
409
                    FMT_STRING(
×
410
                        "\n\n\u2718 annotation handler \u201c{}\u201d failed "
411
                        "with signal {}:\n\n<pre>\n{}\n</pre>\n"),
UNCOV
412
                    handler,
×
UNCOV
413
                    child.term_signal(),
×
414
                    err_reader1.get()));
415
            } else if (child.exit_status() != 0) {
1✔
UNCOV
416
                content.append(fmt::format(
×
UNCOV
417
                    FMT_STRING(
×
418
                        "\n\n<span "
419
                        "class=\"-lnav_log-level-styles_error\">"
420
                        "\u2718 annotation handler \u201c{}\u201d exited "
421
                        "with status {}:</span>\n\n<pre>{}</pre>"),
UNCOV
422
                    handler,
×
UNCOV
423
                    child.exit_status(),
×
UNCOV
424
                    md4cpp::escape_html(err_reader1.get())));
×
425
            }
426
            line_anno.la_pairs[anno.to_string()] = content;
1✔
427
            lnav_data.ld_views[LNV_LOG].reload_data();
1✔
428
            lnav_data.ld_views[LNV_LOG].set_needs_update();
1✔
429
        };
2✔
430

431
        lnav_data.ld_child_pollers.emplace_back(
1✔
432
            (*ld)->get_file_ptr()->get_filename(),
1✔
433
            std::move(child),
1✔
434
            std::move(finalizer));
1✔
435
    }
1✔
436
    lf->get_bookmark_metadata()[line_number].bm_annotations = la;
3✔
437
    return Ok();
3✔
438
}
3✔
439

440
}  // namespace lnav::log::annotate
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc