[log-format] add min-width and align options to line-format

Also some fixes for validating the format definitions.

Fixes #338
This commit is contained in:
Timothy Stack 2016-08-21 21:34:52 -07:00
parent 41854cf637
commit 9ee18c26d3
10 changed files with 268 additions and 68 deletions

3
NEWS
View File

@ -3,6 +3,9 @@ lnav v0.8.2:
Features:
* The timestamp format for JSON log files can be specified with the
"timestamp-format" option in the "line-format" array.
* Added "min-width" and "align" options to the "line-format" in format
definitions for JSON log files. These options give you more control
over how the displayed line looks.
lnav v0.8.1:
Features:

View File

@ -74,6 +74,12 @@ fields:
:field: The name of the message field that should be inserted at this
point in the message.
:min-width: The minimum width for the field. If the value for the field
in a given log message is shorter, padding will be added as needed to
meet the minimum-width requirement. (v0.8.2+)
:align: Specifies the alignment for the field, either "left" or "right".
If "left", padding to meet the minimum-width will be added on the right.
If "right", padding will be added on the left. (v0.8.2+)
:timestamp-format: The timestamp format to use when displaying the time
for this log message. (v0.8.2+)
:default-value: The default value to use if the field could not be found

View File

@ -66,6 +66,11 @@ string_attr_type logline::L_PARTITION;
string_attr_type logline::L_MODULE;
string_attr_type logline::L_OPID;
const intern_string_t external_log_format::json_format_element::ALIGN_LEFT =
intern_string::lookup("left");
const intern_string_t external_log_format::json_format_element::ALIGN_RIGHT =
intern_string::lookup("right");
const char *logline::level_names[LEVEL__MAX + 1] = {
"unknown",
"trace",
@ -1128,27 +1133,43 @@ void external_log_format::get_subline(const logline &ll, shared_buffer_ref &sbr,
iter != this->jlf_line_format.end();
++iter) {
static const intern_string_t ts_field = intern_string::lookup("__timestamp__", -1);
json_format_element &jfe = *iter;
switch (iter->jfe_type) {
switch (jfe.jfe_type) {
case JLF_CONSTANT:
this->json_append_to_cache(iter->jfe_default_value.c_str(),
iter->jfe_default_value.size());
this->json_append_to_cache(jfe.jfe_default_value.c_str(),
jfe.jfe_default_value.size());
break;
case JLF_VARIABLE:
lv_iter = find_if(this->jlf_line_values.begin(),
this->jlf_line_values.end(),
logline_value_cmp(&iter->jfe_value));
logline_value_cmp(&jfe.jfe_value));
if (lv_iter != this->jlf_line_values.end()) {
string str = lv_iter->to_string();
size_t nl_pos = str.find('\n');
lr.lr_start = this->jlf_cached_line.size();
this->json_append_to_cache(
str.c_str(), str.size());
if (nl_pos == string::npos)
if (jfe.jfe_align == json_format_element::ALIGN_RIGHT) {
if (str.size() < jfe.jfe_min_width) {
this->json_append_to_cache(jfe.jfe_min_width -
str.size());
}
}
this->json_append_to_cache(str.c_str(), str.size());
if (jfe.jfe_align == json_format_element::ALIGN_LEFT) {
if (str.size() < jfe.jfe_min_width) {
this->json_append_to_cache(jfe.jfe_min_width -
str.size());
}
}
if (nl_pos == string::npos) {
lr.lr_end = this->jlf_cached_line.size();
else
}
else {
lr.lr_end = lr.lr_start + nl_pos;
}
if (lv_iter->lv_name == this->lf_timestamp_field) {
this->jlf_line_attrs.push_back(
string_attr(lr, &logline::L_TIMESTAMP));
@ -1170,13 +1191,12 @@ void external_log_format::get_subline(const logline &ll, shared_buffer_ref &sbr,
used_values[distance(this->jlf_line_values.begin(),
lv_iter)] = true;
}
else if (iter->jfe_value == ts_field ||
!iter->jfe_ts_format.empty()) {
else if (jfe.jfe_value == ts_field) {
struct line_range lr;
ssize_t ts_len;
char ts[64];
if (iter->jfe_ts_format.empty()) {
if (jfe.jfe_ts_format.empty()) {
ts_len = sql_strftime(ts, sizeof(ts),
ll.get_timeval(), 'T');
} else {
@ -1184,7 +1204,7 @@ void external_log_format::get_subline(const logline &ll, shared_buffer_ref &sbr,
ll.to_exttm(et);
ts_len = ftime_fmt(ts, sizeof(ts),
iter->jfe_ts_format.c_str(),
jfe.jfe_ts_format.c_str(),
et);
}
lr.lr_start = this->jlf_cached_line.size();
@ -1195,8 +1215,8 @@ void external_log_format::get_subline(const logline &ll, shared_buffer_ref &sbr,
}
else {
this->json_append_to_cache(
iter->jfe_default_value.c_str(),
iter->jfe_default_value.size());
jfe.jfe_default_value.c_str(),
jfe.jfe_default_value.size());
}
break;
}
@ -1577,6 +1597,16 @@ void external_log_format::build(std::vector<std::string> &errors) {
}
this->lf_value_stats.resize(this->elf_numeric_value_defs.size());
for (vector<json_format_element>::iterator iter = this->jlf_line_format.begin();
iter != this->jlf_line_format.end();
++iter) {
json_format_element &jfe = *iter;
if (jfe.jfe_value.empty() && !jfe.jfe_ts_format.empty()) {
jfe.jfe_value = intern_string::lookup("__timestamp__");
}
}
}
void external_log_format::register_vtabs(log_vtab_manager *vtab_manager,

View File

@ -1041,14 +1041,19 @@ public:
};
struct json_format_element {
static const intern_string_t ALIGN_LEFT;
static const intern_string_t ALIGN_RIGHT;
json_format_element()
: jfe_type(JLF_CONSTANT), jfe_default_value("-"), jfe_min_width(0)
: jfe_type(JLF_CONSTANT), jfe_default_value("-"), jfe_min_width(0),
jfe_align(ALIGN_LEFT)
{ };
json_log_field jfe_type;
intern_string_t jfe_value;
std::string jfe_default_value;
int jfe_min_width;
long long jfe_min_width;
intern_string_t jfe_align;
std::string jfe_ts_format;
};
@ -1058,6 +1063,12 @@ public:
memcpy(&this->jlf_cached_line[old_size], value, len);
};
void json_append_to_cache(size_t len) {
size_t old_size = this->jlf_cached_line.size();
this->jlf_cached_line.resize(old_size + len);
memset(&this->jlf_cached_line[old_size], ' ', len);
};
bool jlf_json;
bool jlf_hide_extra;
std::vector<json_format_element> jlf_line_format;

View File

@ -64,6 +64,7 @@ static map<intern_string_t, external_log_format *> LOG_FORMATS;
struct userdata {
std::string ud_format_path;
vector<intern_string_t> *ud_format_names;
std::vector<std::string> *ud_errors;
};
static external_log_format *ensure_format(yajlpp_parse_context *ypc)
@ -423,6 +424,13 @@ static struct json_path_handler pattern_handlers[] = {
json_path_handler()
};
static const intern_string_t ALIGN_ENUM[] = {
external_log_format::json_format_element::ALIGN_LEFT,
external_log_format::json_format_element::ALIGN_RIGHT,
intern_string_t()
};
static struct json_path_handler line_format_handlers[] = {
json_path_handler("field")
.with_synopsis("<field-name>")
@ -441,10 +449,22 @@ static struct json_path_handler line_format_handlers[] = {
.with_description("The strftime(3) format for this field")
.for_field(&nullobj<external_log_format::json_format_element>()->jfe_ts_format),
json_path_handler("min-width")
.with_min_value(0)
.with_synopsis("<size>")
.with_description("The minimum width of the field")
.for_field(&nullobj<external_log_format::json_format_element>()->jfe_min_width),
json_path_handler("align")
.with_synopsis("left|right")
.with_description("Align the text in the column to the left or right side")
.with_enum_values(ALIGN_ENUM)
.for_field(&nullobj<external_log_format::json_format_element>()->jfe_align),
json_path_handler()
};
static struct json_path_handler format_handlers[] = {
struct json_path_handler format_handlers[] = {
json_path_handler("/\\w+/regex/[^/]+/")
.with_obj_provider(pattern_provider)
.with_children(pattern_handlers),
@ -537,6 +557,13 @@ static void write_sample_file(void)
}
}
static void format_error_reporter(const yajlpp_parse_context &ypc, const char *msg)
{
struct userdata *ud = (userdata *) ypc.ypc_userdata;
ud->ud_errors->push_back(msg);
}
std::vector<intern_string_t> load_format_file(const string &filename, std::vector<string> &errors)
{
std::vector<intern_string_t> retval;
@ -546,6 +573,7 @@ std::vector<intern_string_t> load_format_file(const string &filename, std::vecto
log_info("loading formats from file: %s", filename.c_str());
ud.ud_format_path = filename;
ud.ud_format_names = &retval;
ud.ud_errors = &errors;
yajlpp_parse_context ypc(filename, format_handlers);
ypc.ypc_userdata = &ud;
if ((fd = open(filename.c_str(), O_RDONLY)) == -1) {
@ -563,6 +591,8 @@ std::vector<intern_string_t> load_format_file(const string &filename, std::vecto
ssize_t rc = -1;
handle = yajl_alloc(&ypc.ypc_callbacks, NULL, &ypc);
ypc.with_handle(handle)
.with_error_reporter(format_error_reporter);
yajl_config(handle, yajl_allow_comments, 1);
while (true) {
rc = read(fd, buffer, sizeof(buffer));
@ -580,7 +610,7 @@ std::vector<intern_string_t> load_format_file(const string &filename, std::vecto
// Turn it into a JavaScript comment.
buffer[0] = buffer[1] = '/';
}
if (yajl_parse(handle, (const unsigned char *)buffer, rc) != yajl_status_ok) {
if (ypc.parse((const unsigned char *)buffer, rc) != yajl_status_ok) {
errors.push_back(filename +
": invalid json -- " +
string((char *)yajl_get_error(handle, 1, (unsigned char *)buffer, rc)));
@ -589,7 +619,7 @@ std::vector<intern_string_t> load_format_file(const string &filename, std::vecto
offset += rc;
}
if (rc == 0) {
if (yajl_complete_parse(handle) != yajl_status_ok) {
if (ypc.complete_parse() != yajl_status_ok) {
errors.push_back(filename +
": invalid json -- " +
string((char *)yajl_get_error(handle, 0, NULL, 0)));
@ -640,6 +670,7 @@ void load_formats(const std::vector<std::string> &extra_paths,
log_debug("Loading default formats");
handle = yajl_alloc(&ypc_builtin.ypc_callbacks, NULL, &ypc_builtin);
ud.ud_format_names = &retval;
ud.ud_errors = &errors;
ypc_builtin
.with_handle(handle)
.ypc_userdata = &ud;

View File

@ -64,4 +64,6 @@ void extract_metadata_from_file(struct script_metadata &meta_inout);
void find_format_scripts(const std::vector<std::string> &extra_paths,
std::map<std::string, std::vector<script_metadata> > &scripts);
extern struct json_path_handler format_handlers[];
#endif

View File

@ -32,6 +32,7 @@
#include "config.h"
#include "yajlpp.hh"
#include "yajl/api/yajl_parse.h"
using namespace std;
@ -56,6 +57,8 @@ int yajlpp_static_string(yajlpp_parse_context *ypc, const unsigned char *str, si
(*field_ptr) = string((const char *) str, len);
yajlpp_validator_for_string(*ypc, *ypc->ypc_current_handler);
return 1;
}
@ -66,6 +69,8 @@ int yajlpp_static_intern_string(yajlpp_parse_context *ypc, const unsigned char *
(*field_ptr) = intern_string::lookup((const char *) str, len);
yajlpp_validator_for_intern_string(*ypc, *ypc->ypc_current_handler);
return 1;
}
@ -86,31 +91,85 @@ yajl_gen_status yajlpp_static_gen_string(yajlpp_gen_context &ygc,
return yajl_gen_string(handle, *field_ptr);
}
void yajlpp_validator_for_string(json_schema_validator &validator,
const std::string &path,
void yajlpp_validator_for_string(yajlpp_parse_context &ypc,
const json_path_handler_base &jph)
{
string *field_ptr = resolve_simple_object<string>(validator.jsv_simple_data, jph.jph_simple_offset);
string *field_ptr = (string *) resolve_root(&ypc);
char buffer[1024];
if (field_ptr->empty() && jph.jph_min_length > 0) {
validator.jsv_errors[path].push_back("value must not be empty");
ypc.report_error("value must not be empty");
}
else if (field_ptr->size() < jph.jph_min_length) {
snprintf(buffer, sizeof(buffer),
"value must be at least %lu characters long",
jph.jph_min_length);
validator.jsv_errors[path].push_back(buffer);
ypc.report_error(buffer);
}
}
void yajlpp_validator_for_intern_string(yajlpp_parse_context &ypc,
const json_path_handler_base &jph)
{
intern_string_t *field_ptr = (intern_string_t *) resolve_root(&ypc);
char buffer[1024];
if (field_ptr->empty() && jph.jph_min_length > 0) {
ypc.report_error("value must not be empty");
}
else if (field_ptr->size() < jph.jph_min_length) {
snprintf(buffer, sizeof(buffer),
"value must be at least %lu characters long",
jph.jph_min_length);
ypc.report_error(buffer);
}
if (jph.jph_enum_values != NULL) {
bool matched = false;
for (int lpc = 0; !jph.jph_enum_values[lpc].empty(); lpc++) {
intern_string_t enum_value = jph.jph_enum_values[lpc];
if (enum_value == *field_ptr) {
matched = true;
break;
}
}
if (!matched) {
ypc.report_error("error:%s:line %d\n "
"Unexpected value for path %s -- %s",
ypc.ypc_source.c_str(),
ypc.get_line_number(),
ypc.get_path().get(),
(*field_ptr).get());
ypc.report_error(" Allowed values:\n");
for (int lpc = 0; !jph.jph_enum_values[lpc].empty(); lpc++) {
intern_string_t enum_value = jph.jph_enum_values[lpc];
ypc.report_error(" %s\n", enum_value.get());
}
}
}
}
void yajlpp_validator_for_int(yajlpp_parse_context &ypc,
const json_path_handler_base &jph)
{
long long *field_ptr = (long long int *) resolve_root(&ypc);
char buffer[1024];
if (*field_ptr < jph.jph_min_value) {
snprintf(buffer, sizeof(buffer),
"value must be greater than %lld",
jph.jph_min_value);
ypc.report_error(buffer);
}
}
int yajlpp_static_number(yajlpp_parse_context *ypc, long long num)
{
const json_path_handler_base *jph = ypc->ypc_current_handler;
ptrdiff_t offset = (char *) jph->jph_simple_offset - (char *) NULL;
char *root_ptr = (char *) ypc->ypc_simple_data;
long long *field_ptr = (long long *) (root_ptr + offset);
char *root_ptr = resolve_root(ypc);
long long *field_ptr = (long long *) root_ptr;
*field_ptr = num;
@ -231,6 +290,8 @@ void yajlpp_parse_context::update_callbacks(const json_path_handler_base *orig_h
{
const json_path_handler_base *handlers = orig_handlers;
this->ypc_current_handler = NULL;
if (this->ypc_handlers == NULL) {
return;
}
@ -355,55 +416,57 @@ int yajlpp_parse_context::handle_unused(void *ctx)
const json_path_handler_base *handler = ypc->ypc_current_handler;
int line_number = ypc->get_line_number();
if (handler != NULL && strlen(handler->jph_synopsis) > 0 &&
strlen(handler->jph_description) > 0) {
fprintf(stderr, "warning:%s:%s %s -- %s\n",
fprintf(stderr,
"warning:%s:line %d\n unexpected data for path -- \n",
ypc->ypc_source.c_str(),
line_number);
fprintf(stderr, " %s %s -- %s\n",
&ypc->ypc_path[0],
handler->jph_synopsis,
handler->jph_description
);
}
else {
fprintf(stderr,
"warning:%s:line %d\n unexpected path -- \n",
ypc->ypc_source.c_str(),
line_number);
int line_number = 0;
if (ypc->ypc_handle != NULL && ypc->ypc_json_text != NULL) {
size_t consumed = yajl_get_bytes_consumed(ypc->ypc_handle);
int current_count = count(&ypc->ypc_json_text[0],
&ypc->ypc_json_text[consumed],
'\n');
line_number = ypc->ypc_line_number + current_count;
fprintf(stderr, " %s\n", &ypc->ypc_path[0]);
}
if (ypc->ypc_callbacks.yajl_boolean != (int (*)(void *, int))yajlpp_parse_context::handle_unused ||
ypc->ypc_callbacks.yajl_integer != (int (*)(void *, long long))yajlpp_parse_context::handle_unused ||
ypc->ypc_callbacks.yajl_double != (int (*)(void *, double))yajlpp_parse_context::handle_unused ||
ypc->ypc_callbacks.yajl_string != (int (*)(void *, const unsigned char *, size_t))yajlpp_parse_context::handle_unused) {
fprintf(stderr, " expecting one of the following data types --\n");
}
fprintf(stderr,
"warning:%s:%s:line %d:unexpected data, expecting one of the following data types --\n",
ypc->ypc_source.c_str(),
&ypc->ypc_path[0],
line_number);
if (ypc->ypc_callbacks.yajl_boolean != (int (*)(void *, int))yajlpp_parse_context::handle_unused) {
fprintf(stderr, "warning:%s:%s: boolean\n",
ypc->ypc_source.c_str(), &ypc->ypc_path[0]);
fprintf(stderr, " boolean\n");
}
if (ypc->ypc_callbacks.yajl_integer != (int (*)(void *, long long))yajlpp_parse_context::handle_unused) {
fprintf(stderr, "warning:%s:%s: integer\n",
ypc->ypc_source.c_str(), &ypc->ypc_path[0]);
fprintf(stderr, " integer\n");
}
if (ypc->ypc_callbacks.yajl_double != (int (*)(void *, double))yajlpp_parse_context::handle_unused) {
fprintf(stderr, "warning:%s:%s: float\n",
ypc->ypc_source.c_str(), &ypc->ypc_path[0]);
fprintf(stderr, " float\n");
}
if (ypc->ypc_callbacks.yajl_string != (int (*)(void *, const unsigned char *, size_t))yajlpp_parse_context::handle_unused) {
fprintf(stderr, "warning:%s:%s: string\n",
ypc->ypc_source.c_str(), &ypc->ypc_path[0]);
fprintf(stderr, " string\n");
}
fprintf(stderr, "warning:%s:%s:accepted paths --\n",
ypc->ypc_source.c_str(), &ypc->ypc_path[0]);
for (int lpc = 0; ypc->ypc_handlers[lpc].jph_path[0]; lpc++) {
fprintf(stderr, "warning:%s:%s: %s\n",
ypc->ypc_source.c_str(),
&ypc->ypc_path[0],
ypc->ypc_handlers[lpc].jph_path);
if (handler == NULL) {
fprintf(stderr, " accepted paths --\n");
for (int lpc = 0; ypc->ypc_handlers[lpc].jph_path[0]; lpc++) {
fprintf(stderr, " %s\n",
ypc->ypc_handlers[lpc].jph_path);
}
}
return 1;

View File

@ -98,7 +98,9 @@ struct json_path_handler_base {
jph_simple_offset(NULL),
jph_children(NULL),
jph_min_length(0),
jph_max_length(INT_MAX)
jph_max_length(INT_MAX),
jph_enum_values(NULL),
jph_min_value(LLONG_MIN)
{
memset(&this->jph_callbacks, 0, sizeof(this->jph_callbacks));
};
@ -109,8 +111,7 @@ struct json_path_handler_base {
pcrepp jph_regex;
yajl_callbacks jph_callbacks;
yajl_gen_status (*jph_gen_callback)(yajlpp_gen_context &, const json_path_handler_base &, yajl_gen);
void (*jph_validator)(json_schema_validator &validator,
const std::string &path,
void (*jph_validator)(yajlpp_parse_context &ypc,
const json_path_handler_base &jph);
void *(*jph_obj_provider)(yajlpp_parse_context &ypc, void *root);
const char * jph_synopsis;
@ -119,6 +120,8 @@ struct json_path_handler_base {
json_path_handler_base *jph_children;
size_t jph_min_length;
size_t jph_max_length;
const intern_string_t *jph_enum_values;
long long jph_min_value;
};
int yajlpp_static_string(yajlpp_parse_context *, const unsigned char *, size_t);
@ -126,9 +129,12 @@ int yajlpp_static_intern_string(yajlpp_parse_context *, const unsigned char *, s
yajl_gen_status yajlpp_static_gen_string(yajlpp_gen_context &ygc,
const json_path_handler_base &,
yajl_gen);
void yajlpp_validator_for_string(json_schema_validator &validator,
const std::string &path,
void yajlpp_validator_for_string(yajlpp_parse_context &ypc,
const json_path_handler_base &jph);
void yajlpp_validator_for_intern_string(yajlpp_parse_context &ypc,
const json_path_handler_base &jph);
void yajlpp_validator_for_int(yajlpp_parse_context &ypc,
const json_path_handler_base &jph);
int yajlpp_static_number(yajlpp_parse_context *, long long);
@ -222,6 +228,16 @@ struct json_path_handler : public json_path_handler_base {
return *this;
}
json_path_handler &with_enum_values(const intern_string_t values[]) {
this->jph_enum_values = values;
return *this;
}
json_path_handler &with_min_value(long long val) {
this->jph_min_value = val;
return *this;
}
template<typename R, typename T>
json_path_handler &with_obj_provider(R *(*provider)(yajlpp_parse_context &ypc, T *root)) {
this->jph_obj_provider = (void *(*)(yajlpp_parse_context &, void *)) provider;
@ -240,13 +256,14 @@ struct json_path_handler : public json_path_handler_base {
this->add_cb(yajlpp_static_intern_string);
this->jph_simple_offset = field;
this->jph_gen_callback = yajlpp_static_gen_string;
this->jph_validator = yajlpp_validator_for_string;
this->jph_validator = yajlpp_validator_for_intern_string;
return *this;
};
json_path_handler &for_field(long long *field) {
this->add_cb(yajlpp_static_number);
this->jph_simple_offset = field;
this->jph_validator = yajlpp_validator_for_int;
return *this;
};
@ -272,6 +289,9 @@ struct json_path_handler : public json_path_handler_base {
class yajlpp_parse_context {
public:
typedef void (*error_reporter_t)(const yajlpp_parse_context &ypc,
const char *msg);
yajlpp_parse_context(const std::string &source,
struct json_path_handler *handlers = NULL)
: ypc_source(source),
@ -378,6 +398,11 @@ public:
return *this;
};
yajlpp_parse_context &with_error_reporter(error_reporter_t err) {
this->ypc_error_reporter = err;
return *this;
}
yajl_status parse(const unsigned char *jsonText, size_t jsonTextLen) {
this->ypc_json_text = jsonText;
@ -393,10 +418,38 @@ public:
return retval;
};
int get_line_number() const {
if (this->ypc_handle != NULL && this->ypc_json_text) {
size_t consumed = yajl_get_bytes_consumed(this->ypc_handle);
long current_count = std::count(&this->ypc_json_text[0],
&this->ypc_json_text[consumed],
'\n');
return this->ypc_line_number + current_count;
}
else {
return 0;
}
};
yajl_status complete_parse() {
return yajl_complete_parse(this->ypc_handle);
};
void report_error(const char *format, ...) {
va_list args;
va_start(args, format);
if (this->ypc_error_reporter != NULL) {
char buffer[1024];
vsnprintf(buffer, sizeof(buffer), format, args);
this->ypc_error_reporter(*this, buffer);
}
va_end(args);
}
const std::string ypc_source;
int ypc_line_number;
struct json_path_handler *ypc_handlers;
@ -413,6 +466,7 @@ public:
bool ypc_ignore_unused;
const struct json_path_handler_base *ypc_current_handler;
std::set<std::string> ypc_active_paths;
error_reporter_t ypc_error_reporter;
void update_callbacks(const json_path_handler_base *handlers = NULL,
int child_start = 0);
@ -581,7 +635,7 @@ public:
iter = this->jsv_schema.js_path_to_handler.find(path);
if (iter->second->jph_validator) {
iter->second->jph_validator(*this, path, *iter->second);
// iter->second->jph_validator(*this, path, *iter->second);
}
return *this;

View File

@ -9,7 +9,7 @@
" ",
{ "timestamp-format" : "abc %S def" },
" ",
{ "field" : "lvl" },
{ "field" : "lvl", "min-width": 5 },
" ",
{ "field" : "msg" }
],

View File

@ -54,10 +54,10 @@ run_test ${lnav_test} -n \
${test_dir}/logfile_json2.json
check_output "timestamp-format not working" <<EOF
2013-09-06T20:00:49.124 abc 49 def 0 Starting up service
2013-09-06T22:00:49.124 abc 49 def 0 Shutting down service
2013-09-06T20:00:49.124 abc 49 def 0 Starting up service
2013-09-06T22:00:49.124 abc 49 def 0 Shutting down service
user: steve@example.com
2013-09-06T22:01:49.124 abc 49 def 10 looking bad
2013-09-06T22:01:49.124 abc 49 def 10 looking bad
EOF
run_test ${lnav_test} -n -d /tmp/lnav.err \