[log_format] support for a separate sub-second field

This commit is contained in:
Tim Stack 2022-10-02 21:58:10 -07:00
parent e135cf3334
commit 9eb734ef7e
18 changed files with 170 additions and 12 deletions

View File

@ -22,6 +22,9 @@ Features:
that the implementation relies on libcurl which has some that the implementation relies on libcurl which has some
limitations, like not supporting all types of schemes limitations, like not supporting all types of schemes
(e.g. `mailto:`). (e.g. `mailto:`).
* Added the `subsecond-field` and `subsecond-units` log
format properties to allow for specifying a separate
field for the sub-second portion of a timestamp.
* Added a keymap for Swedish keyboards. * Added a keymap for Swedish keyboards.
Breaking changes: Breaking changes:

View File

@ -101,6 +101,21 @@
"description": "The name of the timestamp field in the log message pattern", "description": "The name of the timestamp field in the log message pattern",
"type": "string" "type": "string"
}, },
"subsecond-field": {
"title": "/<format_name>/subsecond-field",
"description": "The path to the property in a JSON-lines log message that contains the sub-second time value",
"type": "string"
},
"subsecond-units": {
"title": "/<format_name>/subsecond-units",
"description": "The units of the subsecond-field property value",
"type": "string",
"enum": [
"milli",
"micro",
"nano"
]
},
"time-field": { "time-field": {
"title": "/<format_name>/time-field", "title": "/<format_name>/time-field",
"description": "The name of the time field in the log message pattern. This field should only be specified if the timestamp field only contains a date.", "description": "The name of the time field in the log message pattern. This field should only be specified if the timestamp field only contains a date.",

View File

@ -202,6 +202,16 @@ should be another object with the following fields:
to divide the timestamp by to get the number of seconds and fractional to divide the timestamp by to get the number of seconds and fractional
seconds. seconds.
:subsecond-field: (v0.11.1+) The path to the property in a JSON-lines log
message that contains the sub-second time value
:subsecond-units: (v0.11.1+) The units of the subsecond-field property value.
The following values are supported:
:milli: for milliseconds
:micro: for microseconds
:nano: for nanoseconds
:ordered-by-time: (v0.8.3+) Indicates that the order of messages in the file :ordered-by-time: (v0.8.3+) Indicates that the order of messages in the file
is time-based. Files that are not naturally ordered by time will be sorted is time-based. Files that are not naturally ordered by time will be sorted
in order to display them in the correct order. Note that this sorting can in order to display them in the correct order. Note that this sorting can

View File

@ -480,6 +480,24 @@ read_json_int(yajlpp_parse_context* ypc, long long val)
tv.tv_sec = tm2sec(&ltm); tv.tv_sec = tm2sec(&ltm);
} }
jlu->jlu_base_line->set_time(tv); jlu->jlu_base_line->set_time(tv);
} else if (jlu->jlu_format->lf_subsecond_field == field_name) {
uint64_t millis = 0;
switch (jlu->jlu_format->lf_subsecond_unit.value()) {
case log_format::subsecond_unit::milli:
millis = val;
break;
case log_format::subsecond_unit::micro:
millis = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::microseconds(val))
.count();
break;
case log_format::subsecond_unit::nano:
millis = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::nanoseconds(val))
.count();
break;
}
jlu->jlu_base_line->set_millis(millis);
} else if (jlu->jlu_format->elf_level_field == field_name) { } else if (jlu->jlu_format->elf_level_field == field_name) {
if (jlu->jlu_format->elf_level_pairs.empty()) { if (jlu->jlu_format->elf_level_pairs.empty()) {
char level_buf[128]; char level_buf[128];
@ -2041,6 +2059,23 @@ external_log_format::build(std::vector<lnav::console::user_message>& errors)
.with_snippets(this->get_snippets())); .with_snippets(this->get_snippets()));
} }
if (!this->lf_subsecond_field.empty()
&& !this->lf_subsecond_unit.has_value())
{
errors.emplace_back(
lnav::console::user_message::error(
attr_line_t()
.append_quoted(
lnav::roles::symbol(this->elf_name.to_string()))
.append(" is not a valid log format"))
.with_reason(attr_line_t()
.append_quoted("subsecond-unit"_symbol)
.append(" must be set when ")
.append_quoted("subsecond-field"_symbol)
.append(" is used"))
.with_snippets(this->get_snippets()));
}
for (size_t sample_index = 0; sample_index < this->elf_samples.size(); for (size_t sample_index = 0; sample_index < this->elf_samples.size();
sample_index += 1) sample_index += 1)
{ {

View File

@ -498,6 +498,12 @@ public:
return intern_string_t::case_lt(lhs->get_name(), rhs->get_name()); return intern_string_t::case_lt(lhs->get_name(), rhs->get_name());
} }
enum class subsecond_unit {
milli,
micro,
nano,
};
std::string lf_description; std::string lf_description;
uint8_t lf_mod_index{0}; uint8_t lf_mod_index{0};
bool lf_multiline{true}; bool lf_multiline{true};
@ -505,6 +511,8 @@ public:
date_time_scanner lf_time_scanner; date_time_scanner lf_time_scanner;
std::vector<pattern_for_lines> lf_pattern_locks; std::vector<pattern_for_lines> lf_pattern_locks;
intern_string_t lf_timestamp_field{intern_string::lookup("timestamp", -1)}; intern_string_t lf_timestamp_field{intern_string::lookup("timestamp", -1)};
intern_string_t lf_subsecond_field;
nonstd::optional<subsecond_unit> lf_subsecond_unit;
intern_string_t lf_time_field; intern_string_t lf_time_field;
std::vector<const char*> lf_timestamp_format; std::vector<const char*> lf_timestamp_format;
unsigned int lf_timestamp_flags{0}; unsigned int lf_timestamp_flags{0};

View File

@ -419,6 +419,14 @@ static struct json_path_container pattern_handlers = {
.for_field(&external_log_format::pattern::p_module_format), .for_field(&external_log_format::pattern::p_module_format),
}; };
static const json_path_handler_base::enum_value_t SUBSECOND_UNIT_ENUM[] = {
{"milli", log_format::subsecond_unit::milli},
{"micro", log_format::subsecond_unit::micro},
{"nano", log_format::subsecond_unit::nano},
json_path_handler_base::ENUM_TERMINATOR,
};
static const json_path_handler_base::enum_value_t ALIGN_ENUM[] = { static const json_path_handler_base::enum_value_t ALIGN_ENUM[] = {
{"left", external_log_format::json_format_element::align_t::LEFT}, {"left", external_log_format::json_format_element::align_t::LEFT},
{"right", external_log_format::json_format_element::align_t::RIGHT}, {"right", external_log_format::json_format_element::align_t::RIGHT},
@ -839,7 +847,7 @@ struct json_path_container format_handlers = {
json_path_handler("mime-types#", read_format_field) json_path_handler("mime-types#", read_format_field)
.with_description("A list of mime-types this format should be used for") .with_description("A list of mime-types this format should be used for")
.with_enum_values(MIME_TYPE_ENUM), .with_enum_values(MIME_TYPE_ENUM),
json_path_handler("level-field", read_format_field) json_path_handler("level-field")
.with_description( .with_description(
"The name of the level field in the log message pattern") "The name of the level field in the log message pattern")
.for_field(&external_log_format::elf_level_field), .for_field(&external_log_format::elf_level_field),
@ -847,17 +855,25 @@ struct json_path_container format_handlers = {
.with_description("A regular-expression that matches the JSON-pointer " .with_description("A regular-expression that matches the JSON-pointer "
"of the level property") "of the level property")
.for_field(&external_log_format::elf_level_pointer), .for_field(&external_log_format::elf_level_pointer),
json_path_handler("timestamp-field", read_format_field) json_path_handler("timestamp-field")
.with_description( .with_description(
"The name of the timestamp field in the log message pattern") "The name of the timestamp field in the log message pattern")
.for_field(&log_format::lf_timestamp_field), .for_field(&log_format::lf_timestamp_field),
json_path_handler("time-field", read_format_field) json_path_handler("subsecond-field")
.with_description("The path to the property in a JSON-lines log "
"message that contains the sub-second time value")
.for_field(&log_format::lf_subsecond_field),
json_path_handler("subsecond-units")
.with_description("The units of the subsecond-field property value")
.with_enum_values(SUBSECOND_UNIT_ENUM)
.for_field(&log_format::lf_subsecond_unit),
json_path_handler("time-field")
.with_description( .with_description(
"The name of the time field in the log message pattern. This " "The name of the time field in the log message pattern. This "
"field should only be specified if the timestamp field only " "field should only be specified if the timestamp field only "
"contains a date.") "contains a date.")
.for_field(&log_format::lf_time_field), .for_field(&log_format::lf_time_field),
json_path_handler("body-field", read_format_field) json_path_handler("body-field")
.with_description( .with_description(
"The name of the body field in the log message pattern") "The name of the body field in the log message pattern")
.for_field(&external_log_format::elf_body_field), .for_field(&external_log_format::elf_body_field),

View File

@ -541,7 +541,7 @@ nonstd::optional<int>
json_path_handler_base::to_enum_value(const string_fragment& sf) const json_path_handler_base::to_enum_value(const string_fragment& sf) const
{ {
for (int lpc = 0; this->jph_enum_values[lpc].first; lpc++) { for (int lpc = 0; this->jph_enum_values[lpc].first; lpc++) {
const enum_value_t& ev = this->jph_enum_values[lpc]; const auto& ev = this->jph_enum_values[lpc];
if (sf == ev.first) { if (sf == ev.first) {
return ev.second; return ev.second;

View File

@ -50,6 +50,7 @@
#include "base/lnav.console.hh" #include "base/lnav.console.hh"
#include "base/lnav.console.into.hh" #include "base/lnav.console.into.hh"
#include "base/lnav_log.hh" #include "base/lnav_log.hh"
#include "base/opt_util.hh"
#include "json_ptr.hh" #include "json_ptr.hh"
#include "optional.hpp" #include "optional.hpp"
#include "pcrepp/pcre2pp.hh" #include "pcrepp/pcre2pp.hh"
@ -210,6 +211,20 @@ struct json_path_handler_base {
nonstd::optional<int> to_enum_value(const string_fragment& sf) const; nonstd::optional<int> to_enum_value(const string_fragment& sf) const;
const char* to_enum_string(int value) const; const char* to_enum_string(int value) const;
template<typename T>
std::enable_if_t<!detail::is_optional<T>::value, const char*>
to_enum_string(T value) const
{
return this->to_enum_string((int) value);
}
template<typename T>
std::enable_if_t<detail::is_optional<T>::value, const char*> to_enum_string(
T value) const
{
return this->to_enum_string((int) value.value());
}
yajl_gen_status gen(yajlpp_gen_context& ygc, yajl_gen handle) const; yajl_gen_status gen(yajlpp_gen_context& ygc, yajl_gen handle) const;
yajl_gen_status gen_schema(yajlpp_gen_context& ygc) const; yajl_gen_status gen_schema(yajlpp_gen_context& ygc) const;
yajl_gen_status gen_schema_type(yajlpp_gen_context& ygc) const; yajl_gen_status gen_schema_type(yajlpp_gen_context& ygc) const;

View File

@ -553,11 +553,21 @@ struct json_path_handler : public json_path_handler_base {
template<typename T, typename... Args> template<typename T, typename... Args>
struct LastIsEnum { struct LastIsEnum {
using value_type = typename LastIsEnum<Args...>::value_type;
static constexpr bool value = LastIsEnum<Args...>::value; static constexpr bool value = LastIsEnum<Args...>::value;
}; };
template<typename T, typename U> template<typename T, typename U>
struct LastIsEnum<U T::*> { struct LastIsEnum<U T::*> {
using value_type = U;
static constexpr bool value = std::is_enum<U>::value;
};
template<typename T, typename U>
struct LastIsEnum<nonstd::optional<U> T::*> {
using value_type = U;
static constexpr bool value = std::is_enum<U>::value; static constexpr bool value = std::is_enum<U>::value;
}; };
@ -1414,8 +1424,7 @@ struct json_path_handler : public json_path_handler_base {
if (res) { if (res) {
json_path_handler::get_field(obj, args...) json_path_handler::get_field(obj, args...)
= (decltype(json_path_handler::get_field( = (typename LastIsEnum<Args...>::value_type) res.value();
obj, args...))) res.value();
} else { } else {
handler->report_enum_error(ypc, handler->report_enum_error(ypc,
std::string((const char*) str, len)); std::string((const char*) str, len));
@ -1438,13 +1447,17 @@ struct json_path_handler : public json_path_handler_base {
} }
} }
if (!is_field_set(field)) {
return yajl_gen_status_ok;
}
if (ygc.ygc_depth) { if (ygc.ygc_depth) {
yajl_gen_string(handle, jph.jph_property); yajl_gen_string(handle, jph.jph_property);
} }
yajlpp_generator gen(handle); yajlpp_generator gen(handle);
return gen(jph.to_enum_string((int) field)); return gen(jph.to_enum_string(field));
}; };
this->jph_field_getter this->jph_field_getter
= [args...](void* root, nonstd::optional<std::string> name) { = [args...](void* root, nonstd::optional<std::string> name) {

View File

@ -309,6 +309,7 @@ dist_noinst_DATA = \
logfile_json.json \ logfile_json.json \
logfile_json2.json \ logfile_json2.json \
logfile_json3.json \ logfile_json3.json \
logfile_json_subsec.json \
logfile_leveltest.0 \ logfile_leveltest.0 \
logfile_logfmt.0 \ logfile_logfmt.0 \
logfile_multiline.0 \ logfile_multiline.0 \
@ -370,6 +371,7 @@ dist_noinst_DATA = \
formats/jsontest/rewrite-user.lnav \ formats/jsontest/rewrite-user.lnav \
formats/jsontest2/format.json \ formats/jsontest2/format.json \
formats/jsontest3/format.json \ formats/jsontest3/format.json \
formats/jsontest-subsec/format.json \
formats/nestedjson/format.json \ formats/nestedjson/format.json \
formats/scripts/multiline-echo.lnav \ formats/scripts/multiline-echo.lnav \
formats/scripts/redirecting.lnav \ formats/scripts/redirecting.lnav \

View File

@ -8,6 +8,7 @@
} }
}, },
"timestamp-field": "ts", "timestamp-field": "ts",
"subsecond-field": "ts-sub",
"sample": [ "sample": [
{ {
"line": "1428634687123: 1234 abc" "line": "1428634687123: 1234 abc"

View File

@ -280,6 +280,8 @@ EXPECTED_FILES = \
$(srcdir)/%reldir%/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.out \ $(srcdir)/%reldir%/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.out \
$(srcdir)/%reldir%/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.err \ $(srcdir)/%reldir%/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.err \
$(srcdir)/%reldir%/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.out \ $(srcdir)/%reldir%/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.out \
$(srcdir)/%reldir%/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.err \
$(srcdir)/%reldir%/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.out \
$(srcdir)/%reldir%/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.err \ $(srcdir)/%reldir%/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.err \
$(srcdir)/%reldir%/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.out \ $(srcdir)/%reldir%/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.out \
$(srcdir)/%reldir%/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.err \ $(srcdir)/%reldir%/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.err \

View File

@ -3,7 +3,7 @@
 --> /invalid_props_log/tags/badtag3/pattern  --> /invalid_props_log/tags/badtag3/pattern
 | invalid(abc   | invalid(abc 
 |  ^ missing closing parenthesis  |  ^ missing closing parenthesis
 --> {test_dir}/bad-config/formats/invalid-properties/format.json:35  --> {test_dir}/bad-config/formats/invalid-properties/format.json:36
 |  "pattern": "invalid(abc"  |  "pattern": "invalid(abc"
 = help: Property Synopsis  = help: Property Synopsis
/invalid_props_log/tags/badtag3/pattern <regex> /invalid_props_log/tags/badtag3/pattern <regex>
@ -16,7 +16,7 @@
 --> /invalid_props_log/search-table/bad_table_regex/pattern  --> /invalid_props_log/search-table/bad_table_regex/pattern
 | abc(def   | abc(def 
 |  ^ missing closing parenthesis   |  ^ missing closing parenthesis 
 --> {test_dir}/bad-config/formats/invalid-properties/format.json:40  --> {test_dir}/bad-config/formats/invalid-properties/format.json:41
 |  "pattern": "abc(def"   |  "pattern": "abc(def" 
 = help: Property Synopsis  = help: Property Synopsis
/invalid_props_log/search-table/bad_table_regex/pattern <regex> /invalid_props_log/search-table/bad_table_regex/pattern <regex>
@ -139,6 +139,9 @@
✘ error: invalid tag definition “/invalid_props_log/tags/badtag3” ✘ error: invalid tag definition “/invalid_props_log/tags/badtag3”
reason: tag definitions must have a non-empty pattern reason: tag definitions must have a non-empty pattern
 --> {test_dir}/bad-config/formats/invalid-properties/format.json:4  --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
✘ error: “invalid_props_log” is not a valid log format
reason: “subsecond-unit” must be set when “subsecond-field” is used
 --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
✘ error: invalid value for property “/invalid_props_log/timestamp-field” ✘ error: invalid value for property “/invalid_props_log/timestamp-field”
reason: “ts” was not found in the pattern at /invalid_props_log/regex/std reason: “ts” was not found in the pattern at /invalid_props_log/regex/std
 --> {test_dir}/bad-config/formats/invalid-properties/format.json:4  --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
@ -146,10 +149,10 @@
body, pid, timestamp body, pid, timestamp
✘ error: “not a color” is not a valid color value for property “/invalid_props_log/highlights/hl1/color” ✘ error: “not a color” is not a valid color value for property “/invalid_props_log/highlights/hl1/color”
reason: Unknown color: 'not a color'. See https://jonasjacek.github.io/colors/ for a list of supported color names reason: Unknown color: 'not a color'. See https://jonasjacek.github.io/colors/ for a list of supported color names
 --> {test_dir}/bad-config/formats/invalid-properties/format.json:23  --> {test_dir}/bad-config/formats/invalid-properties/format.json:24
✘ error: “also not a color” is not a valid color value for property “/invalid_props_log/highlights/hl1/background-color” ✘ error: “also not a color” is not a valid color value for property “/invalid_props_log/highlights/hl1/background-color”
reason: Unknown color: 'also not a color'. See https://jonasjacek.github.io/colors/ for a list of supported color names reason: Unknown color: 'also not a color'. See https://jonasjacek.github.io/colors/ for a list of supported color names
 --> {test_dir}/bad-config/formats/invalid-properties/format.json:24  --> {test_dir}/bad-config/formats/invalid-properties/format.json:25
✘ error: “no_regexes_log” is not a valid log format ✘ error: “no_regexes_log” is not a valid log format
reason: no regexes specified reason: no regexes specified
 --> {test_dir}/bad-config/formats/no-regexes/format.json:4  --> {test_dir}/bad-config/formats/no-regexes/format.json:4

View File

@ -0,0 +1,2 @@
2022-09-24T00:00:09.484 Hello, World!
2022-09-24T00:00:19.222 Goodbye, World!

View File

@ -0,0 +1,27 @@
{
"$schema": "https://lnav.org/schemas/format-v1.schema.json",
"subsec_json_log": {
"title": "JSON Log with subsecond field",
"json": true,
"file-pattern": "logfile_json_subsec\\.json",
"line-format": [
{
"field": "__timestamp__"
},
" ",
{
"field": "msg"
}
],
"timestamp-field": "instant/epochSecond",
"subsecond-field": "instant/nanoOfSecond",
"subsecond-units": "nano",
"body-field": "msg",
"value": {
"instant": {
"kind": "json",
"hidden": true
}
}
}
}

View File

@ -0,0 +1,2 @@
{"instant":{"epochSecond": 1663977609,"nanoOfSecond": 484000000}, "msg": "Hello, World!"}
{"instant":{"epochSecond": 1663977619,"nanoOfSecond": 222000000}, "msg": "Goodbye, World!"}

View File

@ -135,3 +135,7 @@ run_cap_test ${lnav_test} -n \
run_cap_test ${lnav_test} -n \ run_cap_test ${lnav_test} -n \
-I ${test_dir} \ -I ${test_dir} \
${test_dir}/logfile_mixed_json2.json ${test_dir}/logfile_mixed_json2.json
run_cap_test ${lnav_test} -n \
-I ${test_dir} \
${test_dir}/logfile_json_subsec.json