summaryrefslogtreecommitdiffstats
path: root/src/log.annotate.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/log.annotate.cc')
-rw-r--r--src/log.annotate.cc407
1 files changed, 407 insertions, 0 deletions
diff --git a/src/log.annotate.cc b/src/log.annotate.cc
new file mode 100644
index 0000000..3b4ffcb
--- /dev/null
+++ b/src/log.annotate.cc
@@ -0,0 +1,407 @@
+/**
+ * Copyright (c) 2023, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <future>
+
+#include "log.annotate.hh"
+
+#include "base/auto_fd.hh"
+#include "base/auto_pid.hh"
+#include "base/fs_util.hh"
+#include "base/paths.hh"
+#include "line_buffer.hh"
+#include "lnav.hh"
+#include "log_data_helper.hh"
+#include "md4cpp.hh"
+#include "readline_highlighters.hh"
+#include "yajlpp/yajlpp.hh"
+
+namespace lnav {
+namespace log {
+namespace annotate {
+
+struct compiled_cond_expr {
+ auto_mem<sqlite3_stmt> cce_stmt{sqlite3_finalize};
+ bool cce_enabled{true};
+};
+
+struct expressions : public lnav_config_listener {
+ expressions() : lnav_config_listener(__FILE__) {}
+
+ void reload_config(error_reporter& reporter) override
+ {
+ auto& lnav_db = injector::get<auto_sqlite3&>();
+
+ if (lnav_db.in() == nullptr) {
+ log_warning("db not initialized yet!");
+ return;
+ }
+
+ const auto& cfg = injector::get<const config&>();
+
+ this->e_cond_exprs.clear();
+ for (const auto& pair : cfg.a_definitions) {
+ if (pair.second.a_handler.pp_value.empty()) {
+ auto um
+ = lnav::console::user_message::error(
+ "no handler specified for annotation")
+ .with_reason("Every annotation requires a handler");
+ reporter(&pair.second.a_handler, um);
+ continue;
+ }
+
+ auto stmt_str = fmt::format(FMT_STRING("SELECT 1 WHERE {}"),
+ pair.second.a_condition);
+ compiled_cond_expr cce;
+
+ log_info("preparing annotation condition expression: %s",
+ stmt_str.c_str());
+ auto retcode = sqlite3_prepare_v2(lnav_db,
+ stmt_str.c_str(),
+ stmt_str.size(),
+ cce.cce_stmt.out(),
+ nullptr);
+ if (retcode != SQLITE_OK) {
+ auto sql_al = attr_line_t(pair.second.a_condition)
+ .with_attr_for_all(SA_PREFORMATTED.value())
+ .with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ readline_sqlite_highlighter(sql_al, -1);
+ intern_string_t cond_expr_path = intern_string::lookup(
+ fmt::format(FMT_STRING("/log/annotations/{}/condition"),
+ pair.first));
+ auto snippet = lnav::console::snippet::from(
+ source_location(cond_expr_path), sql_al);
+
+ auto um = lnav::console::user_message::error(
+ "SQL expression is invalid")
+ .with_reason(sqlite3_errmsg(lnav_db))
+ .with_snippet(snippet);
+
+ reporter(&pair.second.a_condition, um);
+ continue;
+ }
+
+ this->e_cond_exprs.emplace(pair.first, std::move(cce));
+ }
+ }
+
+ void unload_config() override { this->e_cond_exprs.clear(); }
+
+ std::map<intern_string_t, compiled_cond_expr> e_cond_exprs;
+};
+
+static expressions exprs;
+
+std::vector<intern_string_t>
+applicable(vis_line_t vl)
+{
+ std::vector<intern_string_t> retval;
+ auto& lss = lnav_data.ld_log_source;
+ auto cl = lss.at(vl);
+ auto ld = lss.find_data(cl);
+ log_data_helper ldh(lss);
+
+ ldh.parse_line(vl, true);
+ for (auto& expr : exprs.e_cond_exprs) {
+ if (!expr.second.cce_enabled) {
+ continue;
+ }
+
+ auto eval_res
+ = lss.eval_sql_filter(expr.second.cce_stmt.in(), ld, ldh.ldh_line);
+
+ if (eval_res.isErr()) {
+ log_error("eval failed: %s",
+ eval_res.unwrapErr().to_attr_line().get_string().c_str());
+ expr.second.cce_enabled = false;
+ } else {
+ if (eval_res.unwrap()) {
+ retval.emplace_back(expr.first);
+ }
+ }
+ }
+ return retval;
+}
+
+Result<void, lnav::console::user_message>
+apply(vis_line_t vl, std::vector<intern_string_t> annos)
+{
+ const auto& cfg = injector::get<const config&>();
+ auto& lss = lnav_data.ld_log_source;
+ auto cl = lss.at(vl);
+ auto ld = lss.find_data(cl);
+ auto lf = (*ld)->get_file();
+ logmsg_annotations la;
+ log_data_helper ldh(lss);
+
+ if (!ldh.parse_line(vl, true)) {
+ log_error("failed to parse line %d", vl);
+ return Err(lnav::console::user_message::error("Failed to parse line"));
+ }
+ auto line_number = content_line_t{ldh.ldh_line_index - ldh.ldh_y_offset};
+ lss.set_user_mark(&textview_curses::BM_META,
+ content_line_t{ldh.ldh_source_line - ldh.ldh_y_offset});
+
+ yajlpp_gen gen;
+
+ {
+ auto bm_opt = lss.find_bookmark_metadata(vl);
+ yajlpp_map root(gen);
+
+ root.gen("log_line");
+ root.gen((int64_t) vl);
+ root.gen("log_tags");
+ {
+ yajlpp_array tag_array(gen);
+
+ if (bm_opt) {
+ const auto& bm = *(bm_opt.value());
+
+ for (const auto& tag : bm.bm_tags) {
+ tag_array.gen(tag);
+ }
+ }
+ }
+ root.gen("log_path");
+ root.gen(lf->get_filename().native());
+ root.gen("log_format");
+ root.gen(lf->get_format_name());
+ root.gen("log_format_regex");
+ root.gen(lf->get_format()->get_pattern_name(line_number));
+ root.gen("log_msg");
+ root.gen(ldh.ldh_line_values.lvv_sbr.to_string_fragment());
+ for (const auto& val : ldh.ldh_line_values.lvv_values) {
+ root.gen(val.lv_meta.lvm_name);
+ switch (val.lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_NULL:
+ root.gen();
+ break;
+ case value_kind_t::VALUE_INTEGER:
+ root.gen(val.lv_value.i);
+ break;
+ case value_kind_t::VALUE_FLOAT:
+ root.gen(val.lv_value.d);
+ break;
+ case value_kind_t::VALUE_BOOLEAN:
+ root.gen(val.lv_value.i ? true : false);
+ break;
+ default:
+ root.gen(val.to_string());
+ break;
+ }
+ }
+ }
+
+ for (const auto& anno : annos) {
+ auto iter = cfg.a_definitions.find(anno);
+ if (iter == cfg.a_definitions.end()) {
+ log_error("unknown annotation: %s", anno.c_str());
+ continue;
+ }
+
+ la.la_pairs[anno.to_string()] = "Loading...";
+ auto child_fds_res = auto_pipe::for_child_fds(
+ STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO);
+ if (child_fds_res.isErr()) {
+ auto um
+ = lnav::console::user_message::error("unable to create pipes")
+ .with_reason(child_fds_res.unwrapErr());
+ return Err(um);
+ }
+
+ auto child_res = lnav::pid::from_fork();
+ if (child_res.isErr()) {
+ auto um
+ = lnav::console::user_message::error("unable to fork() child")
+ .with_reason(child_res.unwrapErr());
+ return Err(um);
+ }
+
+ auto child_fds = child_fds_res.unwrap();
+
+ auto child = child_res.unwrap();
+ for (auto& child_fd : child_fds) {
+ child_fd.after_fork(child.in());
+ }
+ if (child.in_child()) {
+ const char* exec_args[] = {
+ getenv_opt("SHELL").value_or("bash"),
+ "-c",
+ iter->second.a_handler.pp_value.c_str(),
+ nullptr,
+ };
+
+ std::vector<ghc::filesystem::path> path_v;
+
+ auto src_path
+ = ghc::filesystem::path(
+ iter->second.a_handler.pp_location.sl_source.to_string())
+ .parent_path();
+ path_v.push_back(src_path);
+ path_v.push_back(lnav::paths::dotlnav() / "formats/default");
+ auto path_var = lnav::filesystem::build_path(path_v);
+
+ log_debug("annotate PATH: %s", path_var.c_str());
+ setenv("PATH", path_var.c_str(), 1);
+ execvp(exec_args[0], (char**) exec_args);
+ _exit(EXIT_FAILURE);
+ }
+
+ auto out_reader = std::async(
+ std::launch::async,
+ [out_fd = std::move(child_fds[1].read_end())]() mutable {
+ std::string retval;
+ file_range last_range;
+ line_buffer lb;
+
+ lb.set_fd(out_fd);
+ while (true) {
+ auto load_res = lb.load_next_line(last_range);
+ if (load_res.isErr()) {
+ log_error("unable to load next line: %s",
+ load_res.unwrapErr().c_str());
+ break;
+ }
+
+ auto li = load_res.unwrap();
+ if (li.li_file_range.empty()) {
+ break;
+ }
+ auto read_res = lb.read_range(li.li_file_range);
+ if (read_res.isErr()) {
+ log_error("unable to read next line: %s",
+ load_res.unwrapErr().c_str());
+ break;
+ }
+
+ auto sbr = read_res.unwrap();
+ retval.append(sbr.get_data(), sbr.length());
+
+ last_range = li.li_file_range;
+ }
+
+ return retval;
+ });
+
+ auto err_reader = std::async(
+ std::launch::async,
+ [err_fd = std::move(child_fds[2].read_end()),
+ handler = iter->second.a_handler.pp_value]() mutable {
+ std::string retval;
+ file_range last_range;
+ line_buffer lb;
+
+ lb.set_fd(err_fd);
+ while (true) {
+ auto load_res = lb.load_next_line(last_range);
+ if (load_res.isErr()) {
+ log_error("unable to load next line: %s",
+ load_res.unwrapErr().c_str());
+ break;
+ }
+
+ auto li = load_res.unwrap();
+ if (li.li_file_range.empty()) {
+ break;
+ }
+ auto read_res = lb.read_range(li.li_file_range);
+ if (read_res.isErr()) {
+ log_error("unable to read next line: %s",
+ load_res.unwrapErr().c_str());
+ break;
+ }
+
+ auto sbr = read_res.unwrap();
+ retval.append(sbr.get_data(), sbr.length());
+ sbr.rtrim(is_line_ending);
+ log_debug("%s: %.*s",
+ handler.c_str(),
+ sbr.length(),
+ sbr.get_data());
+
+ last_range = li.li_file_range;
+ }
+
+ return retval;
+ });
+
+ auto write_res
+ = child_fds[0].write_end().write_fully(gen.to_string_fragment());
+ if (write_res.isErr()) {
+ log_error("bah %s", write_res.unwrapErr().c_str());
+ }
+ child_fds[0].write_end().reset();
+ auto finalizer = [anno,
+ out_reader1 = out_reader.share(),
+ err_reader1 = err_reader.share(),
+ lf,
+ line_number,
+ handler = iter->second.a_handler.pp_value](
+ auto& fc,
+ auto_pid<process_state::finished>& child) mutable {
+ auto& line_anno
+ = lf->get_bookmark_metadata()[line_number].bm_annotations;
+ auto content = out_reader1.get();
+ if (!child.was_normal_exit()) {
+ content.append(fmt::format(
+ FMT_STRING(
+ "\n\n\u2718 annotation handler \u201c{}\u201d failed "
+ "with signal {}:\n\n<pre>\n{}\n</pre>\n"),
+ handler,
+ child.term_signal(),
+ err_reader1.get()));
+ } else if (child.exit_status() != 0) {
+ content.append(fmt::format(
+ FMT_STRING(
+ "\n\n<span "
+ "class=\"-lnav_log-level-styles_error\">"
+ "\u2718 annotation handler \u201c{}\u201d exited "
+ "with status {}:</span>\n\n<pre>{}</pre>"),
+ handler,
+ child.exit_status(),
+ md4cpp::escape_html(err_reader1.get())));
+ }
+ line_anno.la_pairs[anno.to_string()] = content;
+ lnav_data.ld_views[LNV_LOG].reload_data();
+ lnav_data.ld_views[LNV_LOG].set_needs_update();
+ };
+
+ lnav_data.ld_child_pollers.emplace_back(
+ (*ld)->get_file_ptr()->get_filename(),
+ std::move(child),
+ std::move(finalizer));
+ }
+ lf->get_bookmark_metadata()[line_number].bm_annotations = la;
+ return Ok();
+}
+
+} // namespace annotate
+} // namespace log
+} // namespace lnav