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

tstack / lnav / 23836075406-2909

01 Apr 2026 06:51AM UTC coverage: 69.084% (+0.007%) from 69.077%
23836075406-2909

push

github

tstack
[tags] only save user-provided tags in the session, not format-provided

60 of 76 new or added lines in 14 files covered. (78.95%)

2 existing lines in 2 files now uncovered.

53287 of 77134 relevant lines covered (69.08%)

535481.86 hits per line

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

72.76
/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/map_util.hh"
38
#include "base/paths.hh"
39
#include "line_buffer.hh"
40
#include "lnav.hh"
41
#include "log.annotate.cfg.hh"
42
#include "log_data_helper.hh"
43
#include "md4cpp.hh"
44
#include "readline_highlighters.hh"
45
#include "yajlpp/yajlpp.hh"
46

47
namespace lnav::log::annotate {
48

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

54
struct expressions : lnav_config_listener {
55
    expressions() : lnav_config_listener(__FILE__) {}
1,232✔
56

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

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

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

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

80
            auto stmt_str = fmt::format(FMT_STRING("SELECT 1 WHERE {}"),
3,008✔
81
                                        pair.second.a_condition);
1,504✔
82
            compiled_cond_expr cce;
1,504✔
83

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

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

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

115
            this->e_cond_exprs.insert(pair.first, std::move(cce));
1,503✔
116
        }
1,505✔
117
    }
118

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

121
    lnav::map::small<intern_string_t, compiled_cond_expr> e_cond_exprs;
122
};
123

124
static expressions exprs;
125

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

135
    ldh.load_line(vl, true);
6✔
136
    for (auto expr : exprs.e_cond_exprs) {
22✔
137
        if (!expr.second.cce_enabled) {
16✔
138
            continue;
×
139
        }
140

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

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

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

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

176
    yajlpp_gen gen;
3✔
177

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

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

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

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

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

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

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

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

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

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

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

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

284
        auto child_fds = child_fds_res.unwrap();
1✔
285

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

298
            std::vector<std::filesystem::path> path_v;
×
299

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

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

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

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

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

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

344
                    last_range = li.li_file_range;
1✔
345
                }
3✔
346

347
                return retval;
2✔
348
            });
2✔
349

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

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

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

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

386
                    last_range = li.li_file_range;
×
387
                }
1✔
388

389
                return retval;
2✔
390
            });
2✔
391

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

435
        auto poller = std::make_shared<child_poller>(
436
            iter->second.a_handler.pp_value,
1✔
437
            (*ld)->get_file_ptr()->get_filename(),
1✔
438
            std::move(child),
1✔
439
            std::move(finalizer));
3✔
440
        lnav_data.ld_child_pollers.emplace_back(poller);
1✔
441
    }
1✔
442
    lf->get_bookmark_metadata()[line_number].bm_annotations = la;
3✔
443
    return Ok();
3✔
444
}
3✔
445

446
}  // 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