[gantt] add a gantt chart view

pull/1179/head
Tim Stack 10 months ago
parent 7c8d32308a
commit f71300ba1d

@ -1,6 +1,11 @@
## lnav v0.12.0
Features:
* Added a Gantt Chart view to visualize operations over time
based on the "opid" in log messages. The view shows
the operation IDs, a description of the operation captured
from log messages, and a bar representing the period of
time that the operation was running.
* Added the `:sh` command and `-e` option to execute a shell
command-line and display its output within **lnav**. The
captured output will be displayed in the TEXT view. The

@ -199,6 +199,53 @@
"description": "The name of the operation-id field in the log message pattern",
"type": "string"
},
"opid": {
"title": "/<format_name>/opid",
"type": "object",
"properties": {
"description": {
"title": "/<format_name>/opid/description",
"type": "object",
"patternProperties": {
"([\\w\\.\\-]+)": {
"title": "/<format_name>/opid/description/<opid_descriptor>",
"type": "object",
"properties": {
"format": {
"title": "/<format_name>/opid/description/<opid_descriptor>/format",
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {
"title": "/<format_name>/opid/description/<opid_descriptor>/format/field",
"type": "string"
},
"extractor": {
"title": "/<format_name>/opid/description/<opid_descriptor>/format/extractor",
"type": "string"
},
"prefix": {
"title": "/<format_name>/opid/description/<opid_descriptor>/format/prefix",
"type": "string"
},
"suffix": {
"title": "/<format_name>/opid/description/<opid_descriptor>/format/suffix",
"type": "string"
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"ordered-by-time": {
"title": "/<format_name>/ordered-by-time",
"description": "Indicates that the order of messages in the file is time-based.",

@ -272,7 +272,7 @@ or interrupting the viewing experience. An annotation is defined by a
condition and a handler in the **lnav** configuration. The condition is
tested against a log message to determine if the annotation is applicable.
If it is, the handler script will be executed for that log message when
the user runs the :ref:`:annotation<annotation>` command.
the user runs the :ref:`:annotate<annotate>` command.
Conditions are SQLite expressions like the ones passed to
:ref:`:filter-expr<filter_expr>` where the expression is appended to

@ -296,6 +296,34 @@ can also press :kbd:`Shift` + :kbd:`i` to toggle the histogram view
while synchronizing the top time. While in the histogram view,
pressing :kbd:`z` / :kbd:`Shift` + :kbd:`z` will zoom in/out.
GANTT
^^^^^
.. note:: This feature is available in v0.12.0+.
The Gantt Chart view visualizes operations over time. The operations
are identified by the "opid" field defined in the log format. In the
view, there is a header that shows the overall time span, the
narrowed time span around the focused line, and the column headers.
Each row in the view shows the following:
* The duration of the operation
* Sparklines showing the number of errors and warnings relative to the
total number of messages associated with the OPID.
* The OPID itself.
* A description of the operation as captured from the log messages.
The rows are sorted by the start time of each operation.
If an operation row is in the focused time span, a reverse-video
bar will show when the operation started and finished (unless it
extends outside the time span). As you move the focused line, the
focused time span will be adjusted to keep the preceding and following
five operations within the span.
The preview panel at the bottom of the display will show the
messages associated with the operation.
PRETTY
^^^^^^

@ -364,6 +364,7 @@ add_library(
fs-extension-functions.cc
fstat_vtab.cc
fts_fuzzy_match.cc
gantt_source.cc
help_text.cc
help_text_formatter.cc
highlighter.cc
@ -477,6 +478,7 @@ add_library(
filter_sub_source.hh
fstat_vtab.hh
fts_fuzzy_match.hh
gantt_source.hh
grep_highlighter.hh
hasher.hh
help_text.hh

@ -214,6 +214,7 @@ noinst_HEADERS = \
filter_sub_source.hh \
fstat_vtab.hh \
fts_fuzzy_match.hh \
gantt_source.hh \
grep_highlighter.hh \
grep_proc.hh \
hasher.hh \
@ -409,6 +410,7 @@ libdiag_a_SOURCES = \
fstat_vtab.cc \
fs-extension-functions.cc \
fts_fuzzy_match.cc \
gantt_source.cc \
grep_proc.cc \
help_text.cc \
help_text_formatter.cc \

@ -518,6 +518,13 @@ inline std::pair<std::string, string_attr_pair> operator"" _error(
VC_ROLE.template value(role_t::VCR_ERROR));
}
inline std::pair<std::string, string_attr_pair> operator"" _warning(
const char* str, std::size_t len)
{
return std::make_pair(std::string(str, len),
VC_ROLE.template value(role_t::VCR_WARNING));
}
inline std::pair<std::string, string_attr_pair> operator"" _info(
const char* str, std::size_t len)
{

@ -138,6 +138,13 @@ operator<(const struct timeval& left, const struct timeval& right)
|| ((left.tv_sec == right.tv_sec) && (left.tv_usec < right.tv_usec));
}
inline bool
operator<=(const struct timeval& left, const struct timeval& right)
{
return left.tv_sec <= right.tv_sec
|| ((left.tv_sec == right.tv_sec) && (left.tv_usec <= right.tv_usec));
}
inline bool
operator!=(const struct timeval& left, const struct timeval& right)
{

@ -370,9 +370,13 @@ execute_sql(exec_context& ec, const std::string& sql, std::string& alt_msg)
switch (retcode) {
case SQLITE_OK:
case SQLITE_DONE:
case SQLITE_DONE: {
auto changes = sqlite3_changes(lnav_data.ld_db.in());
log_info("sqlite3_changes() -> %d", changes);
done = true;
break;
}
case SQLITE_ROW:
ec.ec_sql_callback(ec, stmt.in());

@ -442,7 +442,7 @@ db_overlay_source::list_static_overlay(const listview_curses& lv,
} else {
attrs.ta_attrs = A_UNDERLINE;
}
sa.emplace_back(header_range, VC_STYLE.value(text_attrs{attrs}));
sa.emplace_back(header_range, VC_STYLE.value(attrs));
}
struct line_range lr(0);

@ -53,6 +53,31 @@
"fatal": "^(?i)(?:alert|fatal|panic|Al|Em)$"
},
"opid-field": "opid",
"opid": {
"description": {
"vum": {
"format": [
{
"field": "sub",
"extractor": "^(com\\..*)$"
}
]
},
"hostd": {
"format": [
{
"field": "body",
"extractor": "target='([^']+)'"
},
{
"prefix": ".",
"field": "body",
"extractor": "method='([^']+)'"
}
]
}
}
},
"value": {
"prc": {
"kind": "string",

@ -0,0 +1,479 @@
/**
* 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 <chrono>
#include "gantt_source.hh"
#include "base/humanize.hh"
#include "base/humanize.time.hh"
#include "base/math_util.hh"
#include "md4cpp.hh"
#include "sql_util.hh"
using namespace std::chrono_literals;
using namespace lnav::roles::literals;
using namespace md4cpp::literals;
static const std::vector<std::chrono::seconds> TIME_SPANS = {
5min,
15min,
1h,
8h,
24h,
7 * 24h,
30 * 24h,
365 * 24h,
};
gantt_header_overlay::gantt_header_overlay(std::shared_ptr<gantt_source> src)
: gho_src(src)
{
}
bool
gantt_header_overlay::list_static_overlay(const listview_curses& lv,
int y,
int bottom,
attr_line_t& value_out)
{
if (y >= 3) {
return false;
}
if (this->gho_src->gs_time_order.empty()) {
if (y == 0) {
value_out.append("No operations found"_error);
return true;
}
return false;
}
auto bounds = this->gho_src->get_time_bounds_for(lv.get_selection());
auto width = lv.get_dimensions().second - 1;
char datebuf[64];
if (y == 0) {
auto lb = this->gho_src->gs_lower_bound;
auto ub = this->gho_src->gs_upper_bound;
double span = ub.tv_sec - lb.tv_sec;
double per_ch = span / (double) width;
sql_strftime(datebuf, sizeof(datebuf), lb, 'T');
value_out.appendf(FMT_STRING(" {}"), datebuf);
auto upper_size = sql_strftime(datebuf, sizeof(datebuf), ub, 'T');
value_out.append(width - value_out.length() - upper_size - 1, ' ')
.append(datebuf);
auto lr = line_range{};
if (lb.tv_sec < bounds.first.tv_sec) {
auto start_diff = bounds.first.tv_sec - lb.tv_sec;
lr.lr_start = start_diff / per_ch;
} else {
lr.lr_start = 0;
}
if (lb.tv_sec < bounds.second.tv_sec) {
auto start_diff = bounds.second.tv_sec - lb.tv_sec;
lr.lr_end = start_diff / per_ch;
} else {
lr.lr_end = 1;
}
if (lr.lr_start == lr.lr_end) {
lr.lr_end += 1;
}
value_out.get_attrs().emplace_back(
lr, VC_ROLE.value(role_t::VCR_CURSOR_LINE));
value_out.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO));
} else if (y == 1) {
sql_strftime(datebuf, sizeof(datebuf), bounds.first, 'T');
value_out.appendf(FMT_STRING(" {}"), datebuf);
auto upper_size
= sql_strftime(datebuf, sizeof(datebuf), bounds.second, 'T');
value_out.append(width - value_out.length() - upper_size - 5, ' ')
.append(datebuf);
value_out.with_attr_for_all(VC_ROLE.value(role_t::VCR_CURSOR_LINE));
} else {
value_out.append(" Duration "_h1)
.append("|", VC_GRAPHIC.value(ACS_VLINE))
.append(" ")
.append("\u2718"_error)
.append("\u25b2"_warning)
.append(" ")
.append("|", VC_GRAPHIC.value(ACS_VLINE))
.append(" Operation"_h1);
auto hdr_attrs = text_attrs{};
hdr_attrs.ta_attrs = A_UNDERLINE;
value_out.get_attrs().emplace_back(line_range{0, -1},
VC_STYLE.value(hdr_attrs));
value_out.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO));
}
return true;
}
gantt_source::gantt_source(textview_curses& log_view,
logfile_sub_source& lss,
plain_text_source& preview_source,
preview_status_source& preview_status_source)
: gs_log_view(log_view), gs_lss(lss), gs_preview_source(preview_source),
gs_preview_status_source(preview_status_source)
{
}
std::pair<timeval, timeval>
gantt_source::get_time_bounds_for(int line)
{
static const int CONTEXT_LINES = 5;
const auto& low_row
= this->gs_time_order[std::max(0, line - CONTEXT_LINES)];
const auto& sel_row = this->gs_time_order[line];
const auto& high_row = this->gs_time_order[std::min(
line + CONTEXT_LINES, (int) this->gs_time_order.size() - 1)];
auto high_tv_sec = std::max(sel_row.or_value.otr_end.tv_sec,
high_row.or_value.otr_begin.tv_sec);
auto duration
= std::chrono::seconds{high_tv_sec - low_row.or_value.otr_begin.tv_sec};
auto span_iter
= std::upper_bound(TIME_SPANS.begin(), TIME_SPANS.end(), duration);
if (span_iter == TIME_SPANS.end()) {
--span_iter;
}
auto span_secs = span_iter->count() - 60;
struct timeval lower_tv = {
rounddown(low_row.or_value.otr_begin.tv_sec, 60),
0,
};
lower_tv.tv_sec -= span_secs / 2;
struct timeval upper_tv = {
static_cast<time_t>(roundup_size(high_tv_sec, 60)),
0,
};
upper_tv.tv_sec += span_secs / 2;
return {lower_tv, upper_tv};
}
size_t
gantt_source::text_line_count()
{
return this->gs_time_order.size();
}
void
gantt_source::text_value_for_line(textview_curses& tc,
int line,
std::string& value_out,
text_sub_source::line_flags_t flags)
{
if (line < this->gs_time_order.size()) {
const auto& row = this->gs_time_order[line];
auto duration = row.or_value.otr_end - row.or_value.otr_begin;
auto duration_str = fmt::format(
FMT_STRING(" {: >13}"),
humanize::time::duration::from_tv(duration).to_string());
this->gs_rendered_line.clear();
auto total_msgs = row.or_value.get_total_msgs();
this->gs_rendered_line
.append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
.append(" ")
.append(lnav::roles::error(humanize::sparkline(
row.or_value.get_error_count(), total_msgs)))
.append(lnav::roles::warning(humanize::sparkline(
row.or_value.otr_level_counts[log_level_t::LEVEL_WARNING],
total_msgs)))
.append(" ")
.append(lnav::roles::identifier(row.or_name.to_string()))
.append(this->gs_opid_width - row.or_name.length(), ' ');
for (const auto& desc_pair : row.or_value.otr_description) {
this->gs_rendered_line.append(" ");
this->gs_rendered_line.append(desc_pair.second);
}
this->gs_rendered_line.with_attr_for_all(
VC_ROLE.value(role_t::VCR_COMMENT));
value_out = this->gs_rendered_line.get_string();
}
}
void
gantt_source::text_attrs_for_line(textview_curses& tc,
int line,
string_attrs_t& value_out)
{
if (line < this->gs_time_order.size()) {
const auto& row = this->gs_time_order[line];
value_out = this->gs_rendered_line.get_attrs();
auto lr = line_range{};
auto sel_bounds = this->get_time_bounds_for(tc.get_selection());
if (row.or_value.otr_begin <= sel_bounds.second
&& sel_bounds.first <= row.or_value.otr_end)
{
static const int INDENT = 22;
auto width = tc.get_dimensions().second;
if (width > INDENT) {
width -= INDENT;
double span
= sel_bounds.second.tv_sec - sel_bounds.first.tv_sec;
double per_ch = span / (double) width;
if (row.or_value.otr_begin <= sel_bounds.first) {
lr.lr_start = INDENT;
} else {
auto start_diff = row.or_value.otr_begin.tv_sec
- sel_bounds.first.tv_sec;
lr.lr_start = INDENT + start_diff / per_ch;
}
if (sel_bounds.second < row.or_value.otr_end) {
lr.lr_end = -1;
} else {
auto end_diff
= row.or_value.otr_end.tv_sec - sel_bounds.first.tv_sec;
lr.lr_end = INDENT + end_diff / per_ch;
if (lr.lr_start == lr.lr_end) {
lr.lr_end += 1;
}
}
auto block_attrs = text_attrs{};
block_attrs.ta_attrs = A_REVERSE;
value_out.emplace_back(lr, VC_STYLE.value(block_attrs));
}
}
auto alt_row_index = line % 4;
if (alt_row_index == 2 || alt_row_index == 3) {
value_out.emplace_back(line_range{0, -1},
VC_ROLE.value(role_t::VCR_ALT_ROW));
}
}
}
size_t
gantt_source::text_size_for_line(textview_curses& tc,
int line,
text_sub_source::line_flags_t raw)
{
return this->gs_total_width;
}
void
gantt_source::rebuild_indexes()
{
auto& bm = this->tss_view->get_bookmarks();
auto& bm_errs = bm[&logfile_sub_source::BM_ERRORS];
auto& bm_warns = bm[&logfile_sub_source::BM_WARNINGS];
bm_errs.clear();
bm_warns.clear();
this->gs_lower_bound = {};
this->gs_upper_bound = {};
this->gs_opid_width = 0;
this->gs_total_width = 0;
auto max_desc_width = size_t{0};
log_opid_map active_opids;
for (const auto& ld : this->gs_lss) {
if (ld->get_file_ptr() == nullptr) {
continue;
}
safe::ReadAccess<logfile::safe_opid_map> r_opid_map(
ld->get_file_ptr()->get_opids());
for (const auto& pair : *r_opid_map) {
auto iter = this->gs_opid_map.find(pair.first);
if (iter == this->gs_opid_map.end()) {
auto opid = pair.first.to_owned(this->gs_allocator);
auto emp_res
= this->gs_opid_map.emplace(opid, opid_description_defs{});
iter = emp_res.first;
}
auto active_iter = active_opids.find(pair.first);
if (active_iter == active_opids.end()) {
active_opids.emplace(iter->first, pair.second);
} else {
active_iter->second |= pair.second;
}
}
}
std::multimap<struct timeval, opid_row> time_order_map;
for (const auto& pair : active_opids) {
if (this->gs_lower_bound.tv_sec == 0
|| pair.second.otr_begin < this->gs_lower_bound)
{
this->gs_lower_bound = pair.second.otr_begin;
}
if (this->gs_upper_bound.tv_sec == 0
|| this->gs_upper_bound < pair.second.otr_end)
{
this->gs_upper_bound = pair.second.otr_end;
}
if (pair.first.length() > this->gs_opid_width) {
this->gs_opid_width = pair.first.length();
}
time_order_map.emplace(pair.second.otr_begin,
opid_row{pair.first, pair.second});
}
this->gs_time_order.clear();
for (const auto& pair : time_order_map) {
if (pair.second.or_value.get_error_count() > 0) {
bm_errs.insert_once(vis_line_t(this->gs_time_order.size()));
} else if (pair.second.or_value
.otr_level_counts[log_level_t::LEVEL_WARNING]
> 0)
{
bm_warns.insert_once(vis_line_t(this->gs_time_order.size()));
}
auto total_desc_width = size_t{0};
for (const auto& desc : pair.second.or_value.otr_description) {
total_desc_width += 1 + desc.second.length();
}
if (total_desc_width > max_desc_width) {
max_desc_width = total_desc_width;
}
this->gs_time_order.emplace_back(pair.second);
}
this->gs_total_width = 8 + this->gs_opid_width + max_desc_width;
}
nonstd::optional<vis_line_t>
gantt_source::row_for_time(struct timeval time_bucket)
{
auto iter = std::lower_bound(this->gs_time_order.begin(),
this->gs_time_order.end(),
time_bucket,
[](const opid_row& lhs, const timeval& rhs) {
return lhs.or_value.otr_begin < rhs;
});
if (iter == this->gs_time_order.end()) {
return nonstd::nullopt;
}
return vis_line_t(std::distance(this->gs_time_order.begin(), iter));
}
nonstd::optional<struct timeval>
gantt_source::time_for_row(vis_line_t row)
{
if (row >= this->gs_time_order.size()) {
return nonstd::nullopt;
}
return this->gs_time_order[row].or_value.otr_begin;
}
size_t
gantt_source::text_line_width(textview_curses& curses)
{
return this->gs_total_width;
}
void
gantt_source::text_selection_changed(textview_curses& tc)
{
static const size_t MAX_PREVIEW_LINES = 5;
auto sel = tc.get_selection();
this->gs_preview_source.clear();
if (sel >= this->gs_time_order.size()) {
return;
}
const auto& row = this->gs_time_order[sel];
auto low_vl = this->gs_lss.row_for_time(row.or_value.otr_begin);
auto high_tv = row.or_value.otr_end;
high_tv.tv_sec += 1;
auto high_vl = this->gs_lss.row_for_time(high_tv);
if (!low_vl || !high_vl) {
return;
}
auto preview_content = attr_line_t();
auto msgs_remaining = size_t{MAX_PREVIEW_LINES};
auto win = this->gs_lss.window_at(low_vl.value(), high_vl.value());
auto id_hash = hash_str(row.or_name.data(), row.or_name.length());
for (const auto& msg_line : win) {
if (!msg_line.get_logline().match_opid_hash(id_hash)) {
continue;
}
const auto& sa = msg_line.get_attrs();
auto opid_opt = get_string_attr(sa, logline::L_OPID);
if (opid_opt) {
auto opid_range = opid_opt.value().saw_string_attr->sa_range;
auto opid_sf = msg_line.to_string(opid_range);
if (opid_sf == row.or_name) {
std::vector<attr_line_t> rows_al(1);
this->gs_log_view.listview_value_for_rows(
this->gs_log_view, msg_line.get_vis_line(), rows_al);
preview_content.append(rows_al[0]).append("\n");
msgs_remaining -= 1;
if (msgs_remaining == 0) {
break;
}
}
}
}
while (msgs_remaining > 0) {
preview_content.append("\u2800\n");
msgs_remaining -= 1;
}
this->gs_preview_source.replace_with(preview_content);
this->gs_preview_status_source.get_description().set_value(
"Messages with opid: %.*s", row.or_name.length(), row.or_name.data());
}

@ -0,0 +1,116 @@
/**
* 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.
*/
#ifndef lnav_gantt_source_hh
#define lnav_gantt_source_hh
#include "logfile_sub_source.hh"
#include "plain_text_source.hh"
#include "preview_status_source.hh"
#include "textview_curses.hh"
class gantt_source
: public text_sub_source
, public text_time_translator {
public:
explicit gantt_source(textview_curses& log_view,
logfile_sub_source& lss,
plain_text_source& preview_source,
preview_status_source& preview_status_source);
size_t text_line_count() override;
size_t text_line_width(textview_curses& curses) override;
void text_value_for_line(textview_curses& tc,
int line,
std::string& value_out,
line_flags_t flags) override;
void text_attrs_for_line(textview_curses& tc,
int line,
string_attrs_t& value_out) override;
size_t text_size_for_line(textview_curses& tc,
int line,
line_flags_t raw) override;
void text_selection_changed(textview_curses& tc) override;
nonstd::optional<vis_line_t> row_for_time(
struct timeval time_bucket) override;
nonstd::optional<struct timeval> time_for_row(vis_line_t row) override;
void rebuild_indexes();
std::pair<timeval, timeval> get_time_bounds_for(int line);
textview_curses& gs_log_view;
logfile_sub_source& gs_lss;
plain_text_source& gs_preview_source;
preview_status_source& gs_preview_status_source;
ArenaAlloc::Alloc<char> gs_allocator{64 * 1024};
struct opid_description_defs {};
using gantt_opid_map
= robin_hood::unordered_map<string_fragment,
opid_description_defs,
frag_hasher,
std::equal_to<string_fragment>>;
gantt_opid_map gs_opid_map;
struct opid_row {
string_fragment or_name;
opid_time_range or_value;
};
attr_line_t gs_rendered_line;
size_t gs_opid_width{0};
size_t gs_total_width{0};
std::vector<opid_row> gs_time_order;
struct timeval gs_lower_bound {};
struct timeval gs_upper_bound {};
};
class gantt_header_overlay : public list_overlay_source {
public:
gantt_header_overlay(std::shared_ptr<gantt_source> src);
bool list_static_overlay(const listview_curses& lv,
int y,
int bottom,
attr_line_t& value_out) override;
private:
std::shared_ptr<gantt_source> gho_src;
};
#endif

@ -654,9 +654,32 @@ listview_curses::shift_selection(shift_amount_t sa)
if (this->is_selectable()) {
auto new_selection = this->lv_selection + vis_line_t(offset);
if (new_selection >= 0_vl && new_selection < this->get_inner_height()) {
this->set_selection(new_selection);
if (new_selection < 0_vl) {
new_selection = 0_vl;
} else if (new_selection >= this->get_inner_height()) {
auto rows_avail
= this->rows_available(this->lv_top, RD_DOWN) - 1_vl;
auto top_for_last = this->get_top_for_last_row();
if ((this->lv_top < top_for_last)
&& (this->lv_top + rows_avail > top_for_last))
{
this->set_top(top_for_last);
if (this->lv_selection <= top_for_last) {
this->set_selection(top_for_last + 1_vl);
}
} else {
this->shift_top(rows_avail);
auto inner_height = this->get_inner_height();
if (this->lv_selectable && this->lv_top >= top_for_last
&& inner_height > 0_vl)
{
this->set_selection(inner_height - 1_vl);
}
}
}
this->set_selection(new_selection);
} else {
this->shift_top(vis_line_t{offset});
}

@ -98,6 +98,7 @@
#include "file_converter_manager.hh"
#include "filter_sub_source.hh"
#include "fstat_vtab.hh"
#include "gantt_source.hh"
#include "grep_proc.hh"
#include "hist_source.hh"
#include "init-sql.h"
@ -2744,6 +2745,23 @@ SELECT tbl_name FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE%'
.add_input_delegate(*lnav_data.ld_spectro_source)
.set_tail_space(4_vl);
lnav_data.ld_views[LNV_SPECTRO].set_selectable(true);
auto gantt_view_source
= std::make_shared<gantt_source>(lnav_data.ld_views[LNV_LOG],
lnav_data.ld_log_source,
lnav_data.ld_preview_source,
lnav_data.ld_preview_status_source);
auto gantt_header_source
= std::make_shared<gantt_header_overlay>(gantt_view_source);
lnav_data.ld_views[LNV_GANTT]
.set_sub_source(gantt_view_source.get())
.set_overlay_source(gantt_header_source.get())
.set_tail_space(4_vl);
lnav_data.ld_views[LNV_GANTT].set_selectable(true);
auto _gantt_cleanup = finally([] {
lnav_data.ld_views[LNV_GANTT].set_sub_source(nullptr);
lnav_data.ld_views[LNV_GANTT].set_overlay_source(nullptr);
});
lnav_data.ld_doc_view.set_sub_source(&lnav_data.ld_doc_source);
lnav_data.ld_example_view.set_sub_source(&lnav_data.ld_example_source);
@ -3335,6 +3353,7 @@ SELECT tbl_name FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE%'
*tc, y, tc->get_inner_height(), ov_al))
{
write_line_to(stdout, ov_al);
ov_al.clear();
++y;
}

@ -194,7 +194,7 @@ rebuild_indexes(nonstd::optional<ui_clock::time_point> deadline)
bool scroll_downs[LNV__MAX];
size_t retval = 0;
for (int lpc = 0; lpc < LNV__MAX; lpc++) {
for (auto lpc : {LNV_LOG, LNV_TEXT}) {
auto& view = lnav_data.ld_views[lpc];
if (view.is_selectable()) {
@ -333,7 +333,7 @@ rebuild_indexes(nonstd::optional<ui_clock::time_point> deadline)
retval += 1;
}
for (int lpc = 0; lpc < LNV__MAX; lpc++) {
for (auto lpc : {LNV_LOG, LNV_TEXT}) {
auto& scroll_view = lnav_data.ld_views[lpc];
if (scroll_downs[lpc]) {

@ -1323,6 +1323,7 @@ com_save_to(exec_context& ec,
&& los->list_static_overlay(*tc, y, tc->get_inner_height(), ov_al))
{
write_line_to(outfile, ov_al);
ov_al.clear();
++y;
}
tc->listview_value_for_rows(*tc, top, rows);
@ -1335,8 +1336,8 @@ com_save_to(exec_context& ec,
write_line_to(outfile, al);
++y;
std::vector<attr_line_t> row_overlay_content;
if (los != nullptr) {
std::vector<attr_line_t> row_overlay_content;
los->list_value_for_overlay(*tc, top, row_overlay_content);
for (const auto& ov_row : row_overlay_content) {
write_line_to(outfile, ov_row);
@ -1474,6 +1475,7 @@ com_save_to(exec_context& ec,
&& los->list_static_overlay(*tc, y, tc->get_inner_height(), ov_al))
{
write_line_to(outfile, ov_al);
ov_al.clear();
++y;
}
for (auto iter = all_user_marks.begin(); iter != all_user_marks.end();
@ -1490,8 +1492,8 @@ com_save_to(exec_context& ec,
write_line_to(outfile, rows[0]);
y = 0_vl;
std::vector<attr_line_t> row_overlay_content;
if (los != nullptr) {
std::vector<attr_line_t> row_overlay_content;
los->list_value_for_overlay(*tc, (*iter), row_overlay_content);
for (const auto& ov_row : row_overlay_content) {
write_line_to(outfile, ov_row);

@ -27,6 +27,7 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <algorithm>
#include <memory>
#include <fnmatch.h>
@ -68,6 +69,47 @@ external_log_format::mod_map_t external_log_format::MODULE_FORMATS;
std::vector<std::shared_ptr<external_log_format>>
external_log_format::GRAPH_ORDERED_FORMATS;
size_t
opid_time_range::get_total_msgs() const
{
return std::accumulate(this->otr_level_counts.begin(),
this->otr_level_counts.end(),
size_t{0});
}
size_t
opid_time_range::get_error_count() const
{
size_t retval = 0;
for (const auto level : {
log_level_t::LEVEL_ERROR,
log_level_t::LEVEL_CRITICAL,
log_level_t::LEVEL_FATAL,
})
{
retval += this->otr_level_counts[level];
}
return retval;
}
opid_time_range&
opid_time_range::operator|=(const opid_time_range& rhs)
{
if (rhs.otr_begin < this->otr_begin) {
this->otr_begin = rhs.otr_begin;
}
if (this->otr_end < rhs.otr_end) {
this->otr_end = rhs.otr_end;
}
if (this->otr_description.size() < rhs.otr_description.size()) {
this->otr_description = rhs.otr_description;
}
return *this;
}
struct line_range
logline_value::origin_in_full_msg(const char* msg, ssize_t len) const
{
@ -451,6 +493,7 @@ struct json_log_userdata {
uint32_t jlu_quality{0};
shared_buffer_ref& jlu_shared_buffer;
scan_batch_context* jlu_batch_context;
nonstd::optional<string_fragment> jlu_opid_frag;
};
static int read_json_field(yajlpp_parse_context* ypc,
@ -780,6 +823,21 @@ external_log_format::scan(logfile& lf,
"JSON message does not have expected timestamp property"};
}
if (jlu.jlu_opid_frag) {
auto opid_iter = sbc.sbc_opids.find(jlu.jlu_opid_frag.value());
if (opid_iter == sbc.sbc_opids.end()) {
auto otr
= opid_time_range{ll.get_timeval(), ll.get_timeval()};
auto emplace_res
= sbc.sbc_opids.emplace(jlu.jlu_opid_frag.value(), otr);
opid_iter = emplace_res.first;
} else {
opid_iter->second.otr_end = ll.get_timeval();
}
opid_iter->second.otr_level_counts[ll.get_msg_level()] += 1;
}
jlu.jlu_sub_line_count += this->jlf_line_format_init_count;
for (int lpc = 0; lpc < jlu.jlu_sub_line_count; lpc++) {
ll.set_sub_offset(lpc);
@ -905,17 +963,127 @@ external_log_format::scan(logfile& lf,
}
if (opid_cap && !opid_cap->empty()) {
{
auto opid_iter = sbc.sbc_opids.find(opid_cap.value());
auto opid_iter = sbc.sbc_opids.find(opid_cap.value());
if (opid_iter == sbc.sbc_opids.end()) {
auto opid_copy = opid_cap->to_owned(sbc.sbc_allocator);
auto otr = opid_time_range{log_tv, log_tv};
sbc.sbc_opids.emplace(opid_copy, otr);
} else {
opid_iter->second.otr_end = log_tv;
if (opid_iter == sbc.sbc_opids.end()) {
auto opid_copy = opid_cap->to_owned(sbc.sbc_allocator);
auto otr = opid_time_range{log_tv, log_tv};
auto emplace_res = sbc.sbc_opids.emplace(opid_copy, otr);
opid_iter = emplace_res.first;
} else {
opid_iter->second.otr_end = log_tv;
}
opid_iter->second.otr_level_counts[level] += 1;
auto& otr = opid_iter->second;
if (!otr.otr_description_id) {
for (const auto& desc_def_pair : *this->lf_opid_description_def)
{
if (otr.otr_description_id) {
break;
}
for (const auto& desc_def :
desc_def_pair.second.od_descriptors)
{
auto desc_field_index_iter
= fpat->p_value_name_to_index.find(
desc_def.od_field.pp_value);
if (desc_field_index_iter
!= fpat->p_value_name_to_index.end())
{
auto desc_cap_opt
= md[desc_field_index_iter->second];
if (desc_cap_opt) {
if (desc_def.od_extractor.pp_value) {
static thread_local auto desc_md = lnav::
pcre2pp::match_data::unitialized();
auto desc_match_res
= desc_def.od_extractor.pp_value
->capture_from(
desc_cap_opt.value())
.into(desc_md)
.matches(PCRE2_NO_UTF_CHECK)
.ignore_error();
if (desc_match_res) {
otr.otr_description_id
= desc_def_pair.first;
}
} else {
otr.otr_description_id
= desc_def_pair.first;
}
}
}
}
}
}
if (otr.otr_description_id) {
const auto& desc_def_v
= this->lf_opid_description_def
->find(opid_iter->second.otr_description_id.value())
->second.od_descriptors;
auto& desc_v = opid_iter->second.otr_description;
if (desc_def_v.size() != desc_v.size()) {
for (size_t desc_def_index = 0;
desc_def_index < desc_def_v.size();
desc_def_index++)
{
const auto& desc_def = desc_def_v[desc_def_index];
auto found_desc = false;
for (const auto& desc_pair : desc_v) {
if (desc_pair.first == desc_def_index) {
found_desc = true;
break;
}
}
if (!found_desc) {
auto desc_field_index_iter
= fpat->p_value_name_to_index.find(
desc_def.od_field.pp_value);
if (desc_field_index_iter
!= fpat->p_value_name_to_index.end())
{
auto desc_cap_opt
= md[desc_field_index_iter->second];
if (desc_cap_opt) {
if (desc_def.od_extractor.pp_value) {
static thread_local auto desc_md
= lnav::pcre2pp::match_data::
unitialized();
auto match_res
= desc_def.od_extractor.pp_value
->capture_from(
desc_cap_opt.value())
.into(desc_md)
.matches(PCRE2_NO_UTF_CHECK)
.ignore_error();
if (match_res) {
desc_v.emplace_back(
desc_def_index,
desc_md.to_string());
}
} else {
desc_v.emplace_back(
desc_def_index,
desc_cap_opt->to_string());
}
}
}
}
}
}
}
opid = hash_str(opid_cap->data(), opid_cap->length());
}
@ -1326,6 +1494,7 @@ read_json_field(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
const intern_string_t field_name = ypc->get_path();
struct exttm tm_out;
struct timeval tv_out;
auto frag = string_fragment::from_bytes(str, len);
if (jlu->jlu_format->lf_timestamp_field == field_name) {
jlu->jlu_format->lf_date_time.scan(
@ -1344,17 +1513,25 @@ read_json_field(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
.ignore_error()
.has_value())
{
jlu->jlu_base_line->set_level(jlu->jlu_format->convert_level(
string_fragment::from_bytes(str, len), jlu->jlu_batch_context));
jlu->jlu_base_line->set_level(
jlu->jlu_format->convert_level(frag, jlu->jlu_batch_context));
}
}
if (jlu->jlu_format->elf_level_field == field_name) {
jlu->jlu_base_line->set_level(jlu->jlu_format->convert_level(
string_fragment::from_bytes(str, len), jlu->jlu_batch_context));
jlu->jlu_base_line->set_level(
jlu->jlu_format->convert_level(frag, jlu->jlu_batch_context));
}
if (jlu->jlu_format->elf_opid_field == field_name) {
uint8_t opid = hash_str((const char*) str, len);
jlu->jlu_base_line->set_opid(opid);
auto& sbc = *jlu->jlu_batch_context;
auto opid_iter = sbc.sbc_opids.find(frag);
if (opid_iter == sbc.sbc_opids.end()) {
jlu->jlu_opid_frag = frag.to_owned(sbc.sbc_allocator);
} else {
jlu->jlu_opid_frag = opid_iter->first;
}
}
jlu->add_sub_lines_for(
@ -1789,6 +1966,11 @@ external_log_format::get_subline(const logline& ll,
break;
}
}
lv.lv_origin.lr_end = this->jlf_cached_line.size() - 1;
if (lv.lv_meta.lvm_name == this->elf_opid_field) {
this->jlf_line_attrs.emplace_back(lv.lv_origin,
logline::L_OPID.value());
}
}
}
@ -2073,6 +2255,7 @@ external_log_format::build(std::vector<lnav::console::user_message>& errors)
ivd.ivd_value_def = vd;
pat.p_value_by_index.push_back(ivd);
}
pat.p_value_name_to_index[name] = named_cap.get_index();
}
stable_sort(pat.p_value_by_index.begin(), pat.p_value_by_index.end());
@ -2310,6 +2493,23 @@ external_log_format::build(std::vector<lnav::console::user_message>& errors)
}
}
for (const auto& opid_desc_pair : *this->lf_opid_description_def) {
for (const auto& opid_desc : opid_desc_pair.second.od_descriptors) {
auto iter = this->elf_value_defs.find(opid_desc.od_field.pp_value);
if (iter == this->elf_value_defs.end()) {
errors.emplace_back(
lnav::console::user_message::error(
attr_line_t("invalid opid description field ")
.append_quoted(lnav::roles::symbol(
opid_desc.od_field.pp_path.to_string())))
.with_reason(
attr_line_t("unknown value name ")
.append_quoted(opid_desc.od_field.pp_value))
.with_snippets(this->get_snippets()));
}
}
}
if (this->elf_type == elf_type_t::ELF_TYPE_TEXT
&& this->elf_samples.empty())
{

@ -541,6 +541,21 @@ public:
std::map<const intern_string_t, std::shared_ptr<format_tag_def>>
lf_tag_defs;
struct opid_descriptor {
positioned_property<intern_string_t> od_field;
factory_container<lnav::pcre2pp::code> od_extractor;
std::string od_prefix{" "};
std::string od_suffix;
};
struct opid_descriptors {
std::vector<opid_descriptor> od_descriptors;
};
std::shared_ptr<std::map<intern_string_t, opid_descriptors>>
lf_opid_description_def{
std::make_shared<std::map<intern_string_t, opid_descriptors>>()};
protected:
static std::vector<std::shared_ptr<log_format>> lf_root_formats;

@ -104,6 +104,7 @@ public:
int>::with_default_args<PCRE2_DOTALL>
p_pcre;
std::vector<indexed_value_def> p_value_by_index;
std::map<intern_string_t, int> p_value_name_to_index;
std::vector<int> p_numeric_value_indexes;
int p_timestamp_field_index{-1};
int p_time_field_index{-1};

@ -51,6 +51,14 @@ class log_format;
struct opid_time_range {
struct timeval otr_begin;
struct timeval otr_end;
std::array<size_t, log_level_t::LEVEL__MAX> otr_level_counts;
nonstd::optional<intern_string_t> otr_description_id;
std::vector<std::pair<size_t, std::string>> otr_description;
size_t get_total_msgs() const;
size_t get_error_count() const;
opid_time_range& operator|=(const opid_time_range& rhs);
};
using log_opid_map = robin_hood::unordered_map<string_fragment,
@ -230,6 +238,17 @@ public:
uint8_t get_opid() const { return this->ll_opid; }
bool match_opid_hash(unsigned long hash) const
{
struct {
unsigned int value : 6;
} reduced = {
(unsigned int) hash,
};
return this->ll_opid == reduced.value;
}
/**
* @return True if there is a schema value set for this log line.
*/

@ -847,6 +847,34 @@ static const struct json_path_container converter_handlers = {
.for_field(&external_log_format::converter::c_command),
};
static const struct json_path_container opid_descriptor_handlers = {
yajlpp::property_handler("field").for_field(
&log_format::opid_descriptor::od_field),
yajlpp::property_handler("extractor")
.for_field(&log_format::opid_descriptor::od_extractor),
yajlpp::property_handler("prefix").for_field(
&log_format::opid_descriptor::od_prefix),
yajlpp::property_handler("suffix").for_field(
&log_format::opid_descriptor::od_suffix),
};
static const struct json_path_container opid_description_format_handlers = {
yajlpp::property_handler("format#")
.for_field(&log_format::opid_descriptors::od_descriptors)
.with_children(opid_descriptor_handlers),
};
static const struct json_path_container opid_description_handlers = {
yajlpp::pattern_property_handler(R"((?<opid_descriptor>[\w\.\-]+))")
.for_field(&log_format::lf_opid_description_def)
.with_children(opid_description_format_handlers),
};
static const struct json_path_container opid_handlers = {
yajlpp::property_handler("description")
.with_children(opid_description_handlers),
};
const struct json_path_container format_handlers = {
yajlpp::property_handler("regex")
.with_description(
@ -930,6 +958,7 @@ const struct json_path_container format_handlers = {
.with_description(
"The name of the operation-id field in the log message pattern")
.for_field(&external_log_format::elf_opid_field),
yajlpp::property_handler("opid").with_children(opid_handlers),
yajlpp::property_handler("ordered-by-time")
.with_synopsis("<bool>")
.with_description(

@ -55,6 +55,29 @@ quote(const char* unquoted)
return retval;
}
std::string
match_data::to_string() const
{
std::string retval;
if (this->get_count() == 1) {
auto cap = (*this)[0];
retval.append(cap->data(), cap->length());
} else {
for (size_t lpc = 1; lpc < this->get_count(); lpc++) {
auto cap = (*this)[lpc];
if (!cap) {
continue;
}
retval.append(cap->data(), cap->length());
}
}
return retval;
}
matcher
capture_builder::into(lnav::pcre2pp::match_data& md) &&
{

@ -108,6 +108,8 @@ public:
uint32_t get_capacity() const { return this->md_ovector_count; }
std::string to_string() const;
private:
friend matcher;
friend code;

@ -60,22 +60,19 @@ public:
this->tss_fields[TSF_TOGGLE].set_width(strlen(TOGGLE_MSG) + 1);
this->tss_fields[TSF_TOGGLE].set_value(TOGGLE_MSG);
this->tss_fields[TSF_TOGGLE].right_justify(true);
};
}
size_t statusview_fields() override
{
return TSF__MAX;
};
size_t statusview_fields() override { return TSF__MAX; }
status_field& statusview_value_for_field(int field) override
{
return this->tss_fields[field];
};
}
status_field& get_description()
{
return this->tss_fields[TSF_DESCRIPTION];
};
}
private:
status_field tss_fields[TSF__MAX];

@ -1682,7 +1682,7 @@ lnav::session::restore_view_states()
log_info("restoring %s view top: %d",
lnav_view_strings[view_index],
vs.vs_top);
lnav_data.ld_views[view_index].set_top(vis_line_t(vs.vs_top));
lnav_data.ld_views[view_index].set_top(vis_line_t(vs.vs_top), true);
}
if (vs.vs_selection) {
log_info("restoring %s view selection: %d",

@ -47,9 +47,8 @@
},
"cursor-line": {
"color": "$cyan",
"background-color": "$red",
"bold": true,
"underline": true
"background-color": "#d7005f",
"bold": true
},
"adjusted-time": {
"color": "$magenta"

@ -35,6 +35,7 @@
#include "document.sections.hh"
#include "environ_vtab.hh"
#include "filter_sub_source.hh"
#include "gantt_source.hh"
#include "help-md.h"
#include "intervaltree/IntervalTree.h"
#include "lnav.hh"
@ -63,6 +64,7 @@ const char* lnav_view_strings[LNV__MAX + 1] = {
"schema",
"pretty",
"spectro",
"gantt",
nullptr,
};
@ -76,6 +78,7 @@ const char* lnav_view_titles[LNV__MAX] = {
"SCHEMA",
"PRETTY",
"SPECTRO",
"GANTT",
};
nonstd::optional<lnav_view_t>
@ -124,6 +127,17 @@ open_schema_view()
schema_tc->redo_search();
}
static void
open_gantt_view()
{
auto* gantt_tc = &lnav_data.ld_views[LNV_GANTT];
auto* gantt_src = dynamic_cast<gantt_source*>(gantt_tc->get_sub_source());
gantt_src->rebuild_indexes();
gantt_tc->reload_data();
gantt_tc->redo_search();
}
class pretty_sub_source : public plain_text_source {
public:
void text_crumbs_for_line(int line,
@ -907,6 +921,9 @@ toggle_view(textview_curses* toggle_tc)
require(toggle_tc >= &lnav_data.ld_views[0]);
require(toggle_tc < &lnav_data.ld_views[LNV__MAX]);
lnav_data.ld_preview_source.clear();
lnav_data.ld_preview_status_source.get_description().clear();
if (tc == toggle_tc) {
if (lnav_data.ld_view_stack.size() == 1) {
return false;
@ -923,6 +940,8 @@ toggle_view(textview_curses* toggle_tc)
open_schema_view();
} else if (toggle_tc == &lnav_data.ld_views[LNV_PRETTY]) {
open_pretty_view();
} else if (toggle_tc == &lnav_data.ld_views[LNV_GANTT]) {
open_gantt_view();
} else if (toggle_tc == &lnav_data.ld_views[LNV_HISTOGRAM]) {
// Rebuild to reflect changes in marks.
rebuild_hist();

@ -51,6 +51,7 @@ typedef enum {
LNV_SCHEMA,
LNV_PRETTY,
LNV_SPECTRO,
LNV_GANTT,
LNV__MAX
} lnav_view_t;

@ -234,7 +234,8 @@ CREATE TABLE lnav_views (
= dynamic_cast<text_time_translator*>(tc.get_sub_source());
if (time_source != nullptr && tc.get_inner_height() > 0) {
auto top_time_opt = time_source->time_for_row(tc.get_top());
auto top_time_opt
= time_source->time_for_row(tc.get_selection());
if (top_time_opt) {
char timestamp[64];
@ -303,7 +304,7 @@ CREATE TABLE lnav_views (
top_line_meta tlm;
if (time_source != nullptr) {
auto top_time_opt
= time_source->time_for_row(tc.get_top());
= time_source->time_for_row(tc.get_selection());
if (top_time_opt) {
char timestamp[64];
@ -386,6 +387,8 @@ CREATE TABLE lnav_views (
= dynamic_cast<text_time_translator*>(tc.get_sub_source());
if (tc.get_top() != top_row) {
log_debug(
"setting top for %s to %d", tc.get_title().c_str(), top_row);
tc.set_top(vis_line_t(top_row));
if (!tc.is_selectable()) {
selection = top_row;
@ -394,18 +397,31 @@ CREATE TABLE lnav_views (
date_time_scanner dts;
struct timeval tv;
log_debug("setting top time for %s to %s",
tc.get_title().c_str(),
top_time);
if (dts.convert_to_timeval(top_time, -1, nullptr, tv)) {
auto last_time_opt = time_source->time_for_row(tc.get_top());
auto last_time_opt
= time_source->time_for_row(tc.get_selection());
if (last_time_opt) {
auto last_time = last_time_opt.value();
if (tv != last_time) {
time_source->row_for_time(tv) |
[&tc](auto row) { tc.set_top(row); };
[&tc, &selection](auto row) {
log_debug("setting top for %s to %d from time",
tc.get_title().c_str(),
row);
selection = row;
tc.set_selection(row);
};
if (!tc.is_selectable()) {
selection = tc.get_top();
}
}
} else {
log_warning(" could not get for time top row of %s",
tc.get_title().c_str());
}
} else {
auto um = lnav::console::user_message::error(
@ -549,6 +565,9 @@ CREATE TABLE lnav_view_stack (
lnav_data.ld_last_view = *lnav_data.ld_view_stack.top();
lnav_data.ld_view_stack.pop_back();
lnav_data.ld_preview_source.clear();
lnav_data.ld_preview_status_source.get_description().clear();
return SQLITE_OK;
}

@ -272,6 +272,12 @@ struct json_path_handler : public json_path_handler_base {
return ypc->ypc_current_handler->jph_double_cb(ypc, val);
}
template<typename T, typename U>
static inline U& get_field(T& input, std::shared_ptr<U>(T::*field))
{
return *(input.*field);
}
template<typename T, typename U>
static inline U& get_field(T& input, U(T::*field))
{
@ -371,12 +377,36 @@ struct json_path_handler : public json_path_handler_base {
static constexpr bool value = true;
};
template<typename T, typename U>
struct LastIsVector<std::shared_ptr<std::vector<U>> T::*> {
using value_type = U;
static constexpr bool value = true;
};
template<typename T, typename U>
struct LastIsVector<U T::*> {
using value_type = void;
static constexpr bool value = false;
};
template<typename T, typename... Args>
struct LastIsMap {
using value_type = typename LastIsMap<Args...>::value_type;
static constexpr bool value = LastIsMap<Args...>::value;
};
template<typename T, typename K, typename U>
struct LastIsMap<std::shared_ptr<std::map<K, U>> T::*> {
using value_type = U;
static constexpr bool value = true;
};
template<typename T, typename U>
struct LastIsMap<U T::*> {
using value_type = void;
static constexpr bool value = false;
};
template<typename T>
static bool is_field_set(const nonstd::optional<T>& field)
{
@ -579,6 +609,27 @@ struct json_path_handler : public json_path_handler_base {
return *this;
}
template<typename... Args,
std::enable_if_t<LastIsMap<Args...>::value, bool> = true>
json_path_handler& for_field(Args... args)
{
this->jph_path_provider =
[args...](void* root, std::vector<std::string>& paths_out) {
const auto& field = json_path_handler::get_field(root, args...);
for (const auto& pair : field) {
paths_out.emplace_back(std::to_string(pair.first));
}
};
this->jph_obj_provider
= [args...](const yajlpp_provider_context& ypc, void* root) {
auto& field = json_path_handler::get_field(root, args...);
return &(field[ypc.get_substr_i(0)]);
};
return *this;
}
template<typename... Args,
std::enable_if_t<
LastIs<std::map<std::string, nonstd::optional<std::string>>,

@ -1686,8 +1686,8 @@
},
"cursor-line": {
"color": "$cyan",
"background-color": "$red",
"underline": true,
"background-color": "#d7005f",
"underline": false,
"bold": true
},
"adjusted-time": {

@ -416,112 +416,111 @@
/ui/theme-defs/grayscale/vars/red -> grayscale.json:8
/ui/theme-defs/grayscale/vars/white -> grayscale.json:14
/ui/theme-defs/grayscale/vars/yellow -> grayscale.json:10
/ui/theme-defs/monocai/log-level-styles/critical/color -> monocai.json:261
/ui/theme-defs/monocai/log-level-styles/error/color -> monocai.json:258
/ui/theme-defs/monocai/log-level-styles/fatal/color -> monocai.json:264
/ui/theme-defs/monocai/log-level-styles/warning/color -> monocai.json:255
/ui/theme-defs/monocai/status-styles/active/background-color -> monocai.json:242
/ui/theme-defs/monocai/status-styles/active/color -> monocai.json:241
/ui/theme-defs/monocai/status-styles/alert/background-color -> monocai.json:238
/ui/theme-defs/monocai/status-styles/alert/color -> monocai.json:237
/ui/theme-defs/monocai/status-styles/disabled-title/background-color -> monocai.json:202
/ui/theme-defs/monocai/status-styles/disabled-title/bold -> monocai.json:203
/ui/theme-defs/monocai/status-styles/disabled-title/color -> monocai.json:201
/ui/theme-defs/monocai/status-styles/hotkey/color -> monocai.json:225
/ui/theme-defs/monocai/status-styles/hotkey/underline -> monocai.json:226
/ui/theme-defs/monocai/status-styles/inactive-alert/background-color -> monocai.json:250
/ui/theme-defs/monocai/status-styles/inactive-alert/color -> monocai.json:249
/ui/theme-defs/monocai/status-styles/inactive/background-color -> monocai.json:246
/ui/theme-defs/monocai/status-styles/inactive/color -> monocai.json:245
/ui/theme-defs/monocai/status-styles/info/background-color -> monocai.json:217
/ui/theme-defs/monocai/status-styles/info/color -> monocai.json:216
/ui/theme-defs/monocai/status-styles/subtitle/background-color -> monocai.json:212
/ui/theme-defs/monocai/status-styles/subtitle/bold -> monocai.json:213
/ui/theme-defs/monocai/status-styles/subtitle/color -> monocai.json:211
/ui/theme-defs/monocai/status-styles/text/background-color -> monocai.json:230
/ui/theme-defs/monocai/status-styles/text/color -> monocai.json:229
/ui/theme-defs/monocai/status-styles/title-hotkey/background-color -> monocai.json:221
/ui/theme-defs/monocai/status-styles/title-hotkey/color -> monocai.json:220
/ui/theme-defs/monocai/status-styles/title-hotkey/underline -> monocai.json:222
/ui/theme-defs/monocai/status-styles/title/background-color -> monocai.json:207
/ui/theme-defs/monocai/status-styles/title/bold -> monocai.json:208
/ui/theme-defs/monocai/status-styles/title/color -> monocai.json:206
/ui/theme-defs/monocai/status-styles/warn/background-color -> monocai.json:234
/ui/theme-defs/monocai/status-styles/warn/color -> monocai.json:233
/ui/theme-defs/monocai/styles/adjusted-time/color -> monocai.json:55
/ui/theme-defs/monocai/log-level-styles/critical/color -> monocai.json:260
/ui/theme-defs/monocai/log-level-styles/error/color -> monocai.json:257
/ui/theme-defs/monocai/log-level-styles/fatal/color -> monocai.json:263
/ui/theme-defs/monocai/log-level-styles/warning/color -> monocai.json:254
/ui/theme-defs/monocai/status-styles/active/background-color -> monocai.json:241
/ui/theme-defs/monocai/status-styles/active/color -> monocai.json:240
/ui/theme-defs/monocai/status-styles/alert/background-color -> monocai.json:237
/ui/theme-defs/monocai/status-styles/alert/color -> monocai.json:236
/ui/theme-defs/monocai/status-styles/disabled-title/background-color -> monocai.json:201
/ui/theme-defs/monocai/status-styles/disabled-title/bold -> monocai.json:202
/ui/theme-defs/monocai/status-styles/disabled-title/color -> monocai.json:200
/ui/theme-defs/monocai/status-styles/hotkey/color -> monocai.json:224
/ui/theme-defs/monocai/status-styles/hotkey/underline -> monocai.json:225
/ui/theme-defs/monocai/status-styles/inactive-alert/background-color -> monocai.json:249
/ui/theme-defs/monocai/status-styles/inactive-alert/color -> monocai.json:248
/ui/theme-defs/monocai/status-styles/inactive/background-color -> monocai.json:245
/ui/theme-defs/monocai/status-styles/inactive/color -> monocai.json:244
/ui/theme-defs/monocai/status-styles/info/background-color -> monocai.json:216
/ui/theme-defs/monocai/status-styles/info/color -> monocai.json:215
/ui/theme-defs/monocai/status-styles/subtitle/background-color -> monocai.json:211
/ui/theme-defs/monocai/status-styles/subtitle/bold -> monocai.json:212
/ui/theme-defs/monocai/status-styles/subtitle/color -> monocai.json:210
/ui/theme-defs/monocai/status-styles/text/background-color -> monocai.json:229
/ui/theme-defs/monocai/status-styles/text/color -> monocai.json:228
/ui/theme-defs/monocai/status-styles/title-hotkey/background-color -> monocai.json:220
/ui/theme-defs/monocai/status-styles/title-hotkey/color -> monocai.json:219
/ui/theme-defs/monocai/status-styles/title-hotkey/underline -> monocai.json:221
/ui/theme-defs/monocai/status-styles/title/background-color -> monocai.json:206
/ui/theme-defs/monocai/status-styles/title/bold -> monocai.json:207
/ui/theme-defs/monocai/status-styles/title/color -> monocai.json:205
/ui/theme-defs/monocai/status-styles/warn/background-color -> monocai.json:233
/ui/theme-defs/monocai/status-styles/warn/color -> monocai.json:232
/ui/theme-defs/monocai/styles/adjusted-time/color -> monocai.json:54
/ui/theme-defs/monocai/styles/alt-text/background-color -> monocai.json:26
/ui/theme-defs/monocai/styles/breadcrumb/color -> monocai.json:112
/ui/theme-defs/monocai/styles/breadcrumb/color -> monocai.json:111
/ui/theme-defs/monocai/styles/cursor-line/background-color -> monocai.json:50
/ui/theme-defs/monocai/styles/cursor-line/bold -> monocai.json:51
/ui/theme-defs/monocai/styles/cursor-line/color -> monocai.json:49
/ui/theme-defs/monocai/styles/cursor-line/underline -> monocai.json:52
/ui/theme-defs/monocai/styles/disabled-focused/background-color -> monocai.json:72
/ui/theme-defs/monocai/styles/disabled-focused/color -> monocai.json:71
/ui/theme-defs/monocai/styles/disabled-focused/background-color -> monocai.json:71
/ui/theme-defs/monocai/styles/disabled-focused/color -> monocai.json:70
/ui/theme-defs/monocai/styles/error/bold -> monocai.json:38
/ui/theme-defs/monocai/styles/error/color -> monocai.json:37
/ui/theme-defs/monocai/styles/focused/background-color -> monocai.json:68
/ui/theme-defs/monocai/styles/focused/color -> monocai.json:67
/ui/theme-defs/monocai/styles/footnote-border/background-color -> monocai.json:129
/ui/theme-defs/monocai/styles/footnote-border/color -> monocai.json:128
/ui/theme-defs/monocai/styles/footnote-text/background-color -> monocai.json:133
/ui/theme-defs/monocai/styles/footnote-text/color -> monocai.json:132
/ui/theme-defs/monocai/styles/h1/bold -> monocai.json:84
/ui/theme-defs/monocai/styles/h1/color -> monocai.json:83
/ui/theme-defs/monocai/styles/h2/color -> monocai.json:87
/ui/theme-defs/monocai/styles/h2/underline -> monocai.json:88
/ui/theme-defs/monocai/styles/h3/color -> monocai.json:91
/ui/theme-defs/monocai/styles/h4/underline -> monocai.json:94
/ui/theme-defs/monocai/styles/h5/underline -> monocai.json:97
/ui/theme-defs/monocai/styles/h6/underline -> monocai.json:100
/ui/theme-defs/monocai/styles/focused/background-color -> monocai.json:67
/ui/theme-defs/monocai/styles/focused/color -> monocai.json:66
/ui/theme-defs/monocai/styles/footnote-border/background-color -> monocai.json:128
/ui/theme-defs/monocai/styles/footnote-border/color -> monocai.json:127
/ui/theme-defs/monocai/styles/footnote-text/background-color -> monocai.json:132
/ui/theme-defs/monocai/styles/footnote-text/color -> monocai.json:131
/ui/theme-defs/monocai/styles/h1/bold -> monocai.json:83
/ui/theme-defs/monocai/styles/h1/color -> monocai.json:82
/ui/theme-defs/monocai/styles/h2/color -> monocai.json:86
/ui/theme-defs/monocai/styles/h2/underline -> monocai.json:87
/ui/theme-defs/monocai/styles/h3/color -> monocai.json:90
/ui/theme-defs/monocai/styles/h4/underline -> monocai.json:93
/ui/theme-defs/monocai/styles/h5/underline -> monocai.json:96
/ui/theme-defs/monocai/styles/h6/underline -> monocai.json:99
/ui/theme-defs/monocai/styles/hidden/bold -> monocai.json:46
/ui/theme-defs/monocai/styles/hidden/color -> monocai.json:45
/ui/theme-defs/monocai/styles/hr/color -> monocai.json:103
/ui/theme-defs/monocai/styles/hyperlink/underline -> monocai.json:106
/ui/theme-defs/monocai/styles/hr/color -> monocai.json:102
/ui/theme-defs/monocai/styles/hyperlink/underline -> monocai.json:105
/ui/theme-defs/monocai/styles/identifier/color -> monocai.json:19
/ui/theme-defs/monocai/styles/info/bold -> monocai.json:34
/ui/theme-defs/monocai/styles/info/color -> monocai.json:33
/ui/theme-defs/monocai/styles/invalid-msg/color -> monocai.json:64
/ui/theme-defs/monocai/styles/list-glyph/color -> monocai.json:109
/ui/theme-defs/monocai/styles/offset-time/color -> monocai.json:61
/ui/theme-defs/monocai/styles/invalid-msg/color -> monocai.json:63
/ui/theme-defs/monocai/styles/list-glyph/color -> monocai.json:108
/ui/theme-defs/monocai/styles/offset-time/color -> monocai.json:60
/ui/theme-defs/monocai/styles/ok/bold -> monocai.json:30
/ui/theme-defs/monocai/styles/ok/color -> monocai.json:29
/ui/theme-defs/monocai/styles/popup/background-color -> monocai.json:76
/ui/theme-defs/monocai/styles/popup/color -> monocai.json:75
/ui/theme-defs/monocai/styles/quote-border/background-color -> monocai.json:122
/ui/theme-defs/monocai/styles/quote-border/color -> monocai.json:121
/ui/theme-defs/monocai/styles/quoted-text/background-color -> monocai.json:125
/ui/theme-defs/monocai/styles/scrollbar/background-color -> monocai.json:80
/ui/theme-defs/monocai/styles/scrollbar/color -> monocai.json:79
/ui/theme-defs/monocai/styles/skewed-time/color -> monocai.json:58
/ui/theme-defs/monocai/styles/snippet-border/color -> monocai.json:136
/ui/theme-defs/monocai/styles/table-border/color -> monocai.json:115
/ui/theme-defs/monocai/styles/table-header/bold -> monocai.json:118
/ui/theme-defs/monocai/styles/popup/background-color -> monocai.json:75
/ui/theme-defs/monocai/styles/popup/color -> monocai.json:74
/ui/theme-defs/monocai/styles/quote-border/background-color -> monocai.json:121
/ui/theme-defs/monocai/styles/quote-border/color -> monocai.json:120
/ui/theme-defs/monocai/styles/quoted-text/background-color -> monocai.json:124
/ui/theme-defs/monocai/styles/scrollbar/background-color -> monocai.json:79
/ui/theme-defs/monocai/styles/scrollbar/color -> monocai.json:78
/ui/theme-defs/monocai/styles/skewed-time/color -> monocai.json:57
/ui/theme-defs/monocai/styles/snippet-border/color -> monocai.json:135
/ui/theme-defs/monocai/styles/table-border/color -> monocai.json:114
/ui/theme-defs/monocai/styles/table-header/bold -> monocai.json:117
/ui/theme-defs/monocai/styles/text/background-color -> monocai.json:23
/ui/theme-defs/monocai/styles/text/color -> monocai.json:22
/ui/theme-defs/monocai/styles/warning/bold -> monocai.json:42
/ui/theme-defs/monocai/styles/warning/color -> monocai.json:41
/ui/theme-defs/monocai/syntax-styles/code-border/background-color -> monocai.json:146
/ui/theme-defs/monocai/syntax-styles/code-border/color -> monocai.json:145
/ui/theme-defs/monocai/syntax-styles/comment/color -> monocai.json:157
/ui/theme-defs/monocai/syntax-styles/diff-add/color -> monocai.json:178
/ui/theme-defs/monocai/syntax-styles/diff-delete/color -> monocai.json:175
/ui/theme-defs/monocai/syntax-styles/diff-section/color -> monocai.json:181
/ui/theme-defs/monocai/syntax-styles/doc-directive/color -> monocai.json:160
/ui/theme-defs/monocai/syntax-styles/file/color -> monocai.json:193
/ui/theme-defs/monocai/syntax-styles/keyword/bold -> monocai.json:150
/ui/theme-defs/monocai/syntax-styles/keyword/color -> monocai.json:149
/ui/theme-defs/monocai/syntax-styles/number/bold -> monocai.json:196
/ui/theme-defs/monocai/syntax-styles/quoted-code/background-color -> monocai.json:142
/ui/theme-defs/monocai/syntax-styles/quoted-code/color -> monocai.json:141
/ui/theme-defs/monocai/syntax-styles/re-repeat/color -> monocai.json:172
/ui/theme-defs/monocai/syntax-styles/re-special/color -> monocai.json:169
/ui/theme-defs/monocai/syntax-styles/spectrogram-high/background-color -> monocai.json:190
/ui/theme-defs/monocai/syntax-styles/spectrogram-low/background-color -> monocai.json:184
/ui/theme-defs/monocai/syntax-styles/spectrogram-medium/background-color -> monocai.json:187
/ui/theme-defs/monocai/syntax-styles/string/bold -> monocai.json:154
/ui/theme-defs/monocai/syntax-styles/string/color -> monocai.json:153
/ui/theme-defs/monocai/syntax-styles/symbol/color -> monocai.json:166
/ui/theme-defs/monocai/syntax-styles/variable/color -> monocai.json:163
/ui/theme-defs/monocai/syntax-styles/code-border/background-color -> monocai.json:145
/ui/theme-defs/monocai/syntax-styles/code-border/color -> monocai.json:144
/ui/theme-defs/monocai/syntax-styles/comment/color -> monocai.json:156
/ui/theme-defs/monocai/syntax-styles/diff-add/color -> monocai.json:177
/ui/theme-defs/monocai/syntax-styles/diff-delete/color -> monocai.json:174
/ui/theme-defs/monocai/syntax-styles/diff-section/color -> monocai.json:180
/ui/theme-defs/monocai/syntax-styles/doc-directive/color -> monocai.json:159
/ui/theme-defs/monocai/syntax-styles/file/color -> monocai.json:192
/ui/theme-defs/monocai/syntax-styles/keyword/bold -> monocai.json:149
/ui/theme-defs/monocai/syntax-styles/keyword/color -> monocai.json:148
/ui/theme-defs/monocai/syntax-styles/number/bold -> monocai.json:195
/ui/theme-defs/monocai/syntax-styles/quoted-code/background-color -> monocai.json:141
/ui/theme-defs/monocai/syntax-styles/quoted-code/color -> monocai.json:140
/ui/theme-defs/monocai/syntax-styles/re-repeat/color -> monocai.json:171
/ui/theme-defs/monocai/syntax-styles/re-special/color -> monocai.json:168
/ui/theme-defs/monocai/syntax-styles/spectrogram-high/background-color -> monocai.json:189
/ui/theme-defs/monocai/syntax-styles/spectrogram-low/background-color -> monocai.json:183
/ui/theme-defs/monocai/syntax-styles/spectrogram-medium/background-color -> monocai.json:186
/ui/theme-defs/monocai/syntax-styles/string/bold -> monocai.json:153
/ui/theme-defs/monocai/syntax-styles/string/color -> monocai.json:152
/ui/theme-defs/monocai/syntax-styles/symbol/color -> monocai.json:165
/ui/theme-defs/monocai/syntax-styles/variable/color -> monocai.json:162
/ui/theme-defs/monocai/vars/black -> monocai.json:7
/ui/theme-defs/monocai/vars/blue -> monocai.json:11
/ui/theme-defs/monocai/vars/cyan -> monocai.json:13

@ -121,7 +121,7 @@ run_test ${lnav_test} -n \
check_output "delete from lnav_views table works?" <<EOF
count(*)
8
9
EOF
@ -133,7 +133,7 @@ run_test ${lnav_test} -n \
check_output "insert into lnav_views table works?" <<EOF
count(*)
8
9
EOF
run_cap_test ${lnav_test} -n \

Loading…
Cancel
Save