[systemd-devel] [PATCH] journalctl: allow customizable output formats
Zbigniew Jędrzejewski-Szmek
zbyszek at in.waw.pl
Thu Oct 2 17:13:51 PDT 2014
On Mon, Sep 22, 2014 at 04:33:28PM +0100, Daniel P. Berrange wrote:
> The current '--output FORMAT' argument defines a number of
> common output formats, but there are some useful cases it
> does cover. In particular when reading application logs it
> is often desirable to display the code file name, line number
> and function name. Rather than defining yet more fixed output
> formats, this patch introduces user defined output formats.
Hi,
I think this makes sense. But I think that the format strings you
propose are damn ugly :). Using %() for variables seems too heavy.
Also, journal fields are all text, so I don't think that specifying
the type is useful.
Maybe we could adopt the {} format from Java and Python, as
implemented in Python [1]. It has a fairly rich and consistent field
formatting language. We would care only about the part relevant
to strings, at least in the beginning.
[1] https://docs.python.org/3.5/library/string.html#formatspec
Zbyszek
> The format string is an arbitrary string which contains a
> mixture of literal text and variable subsistitions. Each
> variable name corresponds to a journal field name. A variable
> name can be optionally followed by a data type, and in the
> case of string types, a length limit.
>
> This is best illustrated with an example:
>
> $ journalctl -o "format:%(__REALTIME_TIMESTAMP) \
> [%(CODE_FILE):%(CODE_LINE):%(CODE_FUNC)] \
> %(MESSAGE:string:80)\n" _COMM=libvirtd
> -- Logs begin at Mon 2013-12-23 16:31:41 GMT, end at Mon 2014-09-22 16:13:00 BST. --
> Dec 23 17:19:25 [util/virlog.c:877:virLogVMessage] libvirt version: 1.1.3.1, package: 2.fc20 (Fedora Project, 2013-11-17-23:28:43, ...
> Dec 23 17:19:25 [conf/storage_conf.c:854:virStoragePoolDefParseXML] XML error: unknown storage pool type btrfs
> Dec 23 17:19:30 [conf/domain_conf.c:12671:virDomainObjParseNode] XML error: unexpected root element <domain>, expecting <domstatus>
> Dec 23 17:24:45 [qemu/qemu_monitor.c:653:qemuMonitorIO] internal error: End of file from monitor
> Dec 23 20:12:00 [qemu/qemu_monitor.c:653:qemuMonitorIO] internal error: End of file from monitor
> -- Reboot --
> Dec 23 21:06:14 [util/virlog.c:877:virLogVMessage] libvirt version: 1.1.3.1, package: 2.fc20 (Fedora Project, 2013-11-17-23:28:43, ...
> Dec 23 21:06:21 [conf/storage_conf.c:854:virStoragePoolDefParseXML] XML error: unknown storage pool type btrfs
>
> Signed-off-by: Daniel P. Berrange <berrange at redhat.com>
> ---
> man/journalctl.xml | 76 +++++
> src/journal-remote/journal-gatewayd.c | 11 +-
> src/journal/journalctl.c | 39 ++-
> src/shared/logs-show.c | 532 ++++++++++++++++++++++++++++++----
> src/shared/logs-show.h | 16 +-
> src/shared/output-mode.h | 1 +
> src/systemctl/systemctl.c | 20 +-
> 7 files changed, 615 insertions(+), 80 deletions(-)
>
> diff --git a/man/journalctl.xml b/man/journalctl.xml
> index acd75a6..bd8c2bd 100644
> --- a/man/journalctl.xml
> +++ b/man/journalctl.xml
> @@ -375,6 +375,21 @@
> </para>
> </listitem>
> </varlistentry>
> +
> + <varlistentry>
> + <term>
> + <option>format:FMT</option>
> + </term>
> + <listitem>
> + <para>generates output
> + according to the format
> + specification given in
> + the FMT string. See the
> + OUTPUT FORMAT STRINGS
> + section for details
> + </para>
> + </listitem>
> + </varlistentry>
> </variablelist>
> </listitem>
> </varlistentry>
> @@ -878,6 +893,64 @@
> </refsect1>
>
> <refsect1>
> + <title>Output Format Strings</title>
> +
> + <para>An output format string provides precise control how journal
> + data records are formatted for output. A format string consists of
> + mixture of literal text and variables to be substituted with journal
> + data records. A variable takes the general form</para>
> +
> + <programlisting>$(NAME:TYPE:LEN)</programlisting>
> +
> + <para>The NAME component corresponds to any journal entry field
> + (eg MESSAGE, _SYSTEMD_UNIT, CODE_FUNC, etc). The TYPE component
> + determines the data format to use for printing the value. If
> + omitted, it defaults to a sensible format for the NAME of the
> + field. The LEN component places an upper limit on the length of
> + strings being printed, beyond which they will be ellipsized.
> + The valid data types for TYPE are:</para>
> +
> + <variablelist>
> + <varlistentry>
> + <term>string</term>
> + <listitem><para>displayed if a printable string. If the value
> + contains non-printable characters, it will be shown as
> + '[blob data]'.</para></listitem>
> + </varlistentry>
> +
> + <varlistentry>
> + <term>string-raw</term>
> + <listitem><para>displayed as a raw string. All characters,
> + including non-printable ones, will be output.</para></listitem>
> + </varlistentry>
> +
> + <varlistentry>
> + <term>date</term>
> + <listitem><para>displayed as a date in traditional syslog
> + format.</para></listitem>
> + </varlistentry>
> +
> + <varlistentry>
> + <term>date-precise</term>
> + <listitem><para>displayed as a date in traditional syslog
> + format, but with microsecond precision.</para></listitem>
> + </varlistentry>
> +
> + <varlistentry>
> + <term>date-iso</term>
> + <listitem><para>displayed as a date in ISO 8601 format.</para></listitem>
> + </varlistentry>
> +
> + <varlistentry>
> + <term>date-raw</term>
> + <listitem><para>displayed as a date with seconds since the
> + epoch and microsecond precision.</para></listitem>
> + </varlistentry>
> + </variablelist>
> +
> + </refsect1>
> +
> + <refsect1>
> <title>Exit status</title>
>
> <para>On success, 0 is returned; otherwise, a non-zero
> @@ -931,6 +1004,9 @@
>
> <programlisting>journalctl -f -u apache</programlisting>
>
> + <para>Display source file locations for libvirt daemon log messages:</para>
> +
> + <programlisting>journalctl -o "format:%(__REALTIME_TIMESTAMP) [%(CODE_FILE):%(CODE_LINE):%(CODE_FUNC)] %(MESSAGE:string:80)\n" _COMM=libvirtd</programlisting>
> </refsect1>
>
> <refsect1>
> diff --git a/src/journal-remote/journal-gatewayd.c b/src/journal-remote/journal-gatewayd.c
> index db81fe3..9d2bf50 100644
> --- a/src/journal-remote/journal-gatewayd.c
> +++ b/src/journal-remote/journal-gatewayd.c
> @@ -129,6 +129,7 @@ static ssize_t request_reader_entries(
> RequestMeta *m = cls;
> int r;
> size_t n, k;
> + _cleanup_output_formatter_ OutputFormatter *output_formatter = NULL;
>
> assert(m);
> assert(buf);
> @@ -137,6 +138,12 @@ static ssize_t request_reader_entries(
>
> pos -= m->delta;
>
> + if ((r = output_formatter_from_mode(m->mode,
> + &output_formatter)) < 0) {
> + log_error("Failed to parse mode: %s", strerror(-r));
> + return MHD_CONTENT_READER_END_WITH_ERROR;
> + }
> +
> while (pos >= m->size) {
> off_t sz;
>
> @@ -203,7 +210,9 @@ static ssize_t request_reader_entries(
> }
> }
>
> - r = output_journal(m->tmp, m->journal, m->mode, 0, OUTPUT_FULL_WIDTH, NULL);
> + r = output_journal(m->tmp, m->journal,
> + output_formatter,
> + 0, OUTPUT_FULL_WIDTH, NULL);
> if (r < 0) {
> log_error("Failed to serialize item: %s", strerror(-r));
> return MHD_CONTENT_READER_END_WITH_ERROR;
> diff --git a/src/journal/journalctl.c b/src/journal/journalctl.c
> index 47206d3..2566e53 100644
> --- a/src/journal/journalctl.c
> +++ b/src/journal/journalctl.c
> @@ -62,7 +62,8 @@
>
> #define DEFAULT_FSS_INTERVAL_USEC (15*USEC_PER_MINUTE)
>
> -static OutputMode arg_output = OUTPUT_SHORT;
> +static const char *arg_output = "short";
> +static OutputFormatter *arg_formatter = NULL;
> static bool arg_pager_end = false;
> static bool arg_follow = false;
> static bool arg_full = true;
> @@ -190,7 +191,8 @@ static void help(void) {
> " -r --reverse Show the newest entries first\n"
> " -o --output=STRING Change journal output mode (short, short-iso,\n"
> " short-precise, short-monotonic, verbose,\n"
> - " export, json, json-pretty, json-sse, cat)\n"
> + " export, json, json-pretty, json-sse, cat,\n"
> + " format:FORMAT-STRING)\n"
> " -x --catalog Add message explanations where available\n"
> " --no-full Ellipsize fields\n"
> " -a --all Show all fields, including long and unprintable\n"
> @@ -303,6 +305,8 @@ static int parse_argv(int argc, char *argv[]) {
> };
>
> int c, r;
> + int err;
> + OutputMode mode;
>
> assert(argc >= 0);
> assert(argv);
> @@ -337,19 +341,7 @@ static int parse_argv(int argc, char *argv[]) {
> break;
>
> case 'o':
> - arg_output = output_mode_from_string(optarg);
> - if (arg_output < 0) {
> - log_error("Unknown output format '%s'.", optarg);
> - return -EINVAL;
> - }
> -
> - if (arg_output == OUTPUT_EXPORT ||
> - arg_output == OUTPUT_JSON ||
> - arg_output == OUTPUT_JSON_PRETTY ||
> - arg_output == OUTPUT_JSON_SSE ||
> - arg_output == OUTPUT_CAT)
> - arg_quiet = true;
> -
> + arg_output = optarg;
> break;
>
> case 'l':
> @@ -646,6 +638,21 @@ static int parse_argv(int argc, char *argv[]) {
> assert_not_reached("Unhandled option");
> }
>
> + if ((err = output_formatter_from_string(arg_output,
> + &arg_formatter)) < 0) {
> + log_error("Unknown/malformed output format '%s'.", arg_output);
> + return err;
> + }
> +
> + mode = output_formatter_get_mode(arg_formatter);
> + if (mode == OUTPUT_EXPORT ||
> + mode == OUTPUT_JSON ||
> + mode == OUTPUT_JSON_PRETTY ||
> + mode == OUTPUT_JSON_SSE ||
> + mode == OUTPUT_CAT)
> + arg_quiet = true;
> +
> +
> if (arg_follow && !arg_no_tail && arg_lines < -1)
> arg_lines = 10;
>
> @@ -1960,7 +1967,7 @@ int main(int argc, char *argv[]) {
> on_tty() * OUTPUT_COLOR |
> arg_catalog * OUTPUT_CATALOG;
>
> - r = output_journal(stdout, j, arg_output, 0, flags, &ellipsized);
> + r = output_journal(stdout, j, arg_formatter, 0, flags, &ellipsized);
> need_seek = true;
> if (r == -EADDRNOTAVAIL)
> break;
> diff --git a/src/shared/logs-show.c b/src/shared/logs-show.c
> index 5a7bbaf..75f0dce 100644
> --- a/src/shared/logs-show.c
> +++ b/src/shared/logs-show.c
> @@ -42,6 +42,36 @@
>
> #define JSON_THRESHOLD 4096
>
> +typedef int (*OutputFunc)(OutputFormatter *formatter,
> + FILE *f,
> + sd_journal*j,
> + unsigned n_columns,
> + OutputFlags flags);
> +
> +typedef enum {
> + OUTPUT_FIELD_LITERAL, /* data is a literal string to print */
> + OUTPUT_FIELD_DATE, /* data is a variable to show as a simple date */
> + OUTPUT_FIELD_DATE_PRECISE, /* data is a variable to show as a precise date */
> + OUTPUT_FIELD_DATE_ISO, /* data is a variable to show as a ISO date */
> + OUTPUT_FIELD_DATE_RAW, /* data is a variable to show as a RAW timestamp */
> + OUTPUT_FIELD_STRING, /* data is a variable to show as a printable string */
> + OUTPUT_FIELD_STRING_RAW, /* data is a variable to show as a raw string */
> +} OutputFieldType;
> +
> +typedef struct _OutputField {
> + OutputFieldType type;
> + size_t vallen; /* Truncate value to this length when printing */
> + const char *data;
> + size_t datalen;
> +} OutputField;
> +
> +struct _OutputFormatter {
> + OutputFunc handler;
> + OutputMode mode;
> + size_t nfields;
> + OutputField *fields;
> +};
> +
> static int print_catalog(FILE *f, sd_journal *j) {
> int r;
> _cleanup_free_ char *t = NULL, *z = NULL;
> @@ -189,10 +219,59 @@ static bool print_multiline(FILE *f, unsigned prefix, unsigned n_columns, Output
> return ellipsized;
> }
>
> +static int print_time(uint64_t ts,
> + OutputFieldType type,
> + FILE *f) {
> + int n, r;
> + char buf[64];
> + time_t t;
> + struct tm tm;
> +
> + assert(f);
> +
> + t = (time_t)(ts / USEC_PER_SEC);
> +
> + switch (type) {
> + case OUTPUT_FIELD_DATE:
> + r = strftime(buf, sizeof(buf), "%b %d %H:%M:%S", localtime_r(&t, &tm));
> + break;
> +
> + case OUTPUT_FIELD_DATE_PRECISE:
> + r = strftime(buf, sizeof(buf), "%b %d %H:%M:%S", localtime_r(&t, &tm));
> + if (r > 0) {
> + snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
> + ".%06llu", (unsigned long long) (ts % USEC_PER_SEC));
> + }
> + break;
> +
> + case OUTPUT_FIELD_DATE_ISO:
> + r = strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S%z", localtime_r(&t, &tm));
> + break;
> +
> + case OUTPUT_FIELD_DATE_RAW:
> + r = snprintf(buf, sizeof(buf), "[%5llu.%06llu]",
> + (unsigned long long) (ts / USEC_PER_SEC),
> + (unsigned long long) (ts % USEC_PER_SEC));
> + break;
> +
> + default:
> + return -EINVAL;
> + }
> +
> + if (r <= 0) {
> + log_error("Failed to format time.");
> + return -EINVAL;
> + }
> + fputs(buf, f);
> + n = strlen(buf);
> +
> + return n;
> +}
> +
> static int output_short(
> + OutputFormatter *formatter,
> FILE *f,
> sd_journal *j,
> - OutputMode mode,
> unsigned n_columns,
> OutputFlags flags) {
>
> @@ -205,6 +284,7 @@ static int output_short(
> int p = LOG_INFO;
> bool ellipsized = false;
>
> + assert(formatter);
> assert(f);
> assert(j);
>
> @@ -283,7 +363,7 @@ static int output_short(
> if (priority_len == 1 && *priority >= '0' && *priority <= '7')
> p = *priority - '0';
>
> - if (mode == OUTPUT_SHORT_MONOTONIC) {
> + if (formatter->mode == OUTPUT_SHORT_MONOTONIC) {
> uint64_t t;
> sd_id128_t boot_id;
>
> @@ -300,17 +380,12 @@ static int output_short(
> return r;
> }
>
> - fprintf(f, "[%5llu.%06llu]",
> - (unsigned long long) (t / USEC_PER_SEC),
> - (unsigned long long) (t % USEC_PER_SEC));
> -
> - n += 1 + 5 + 1 + 6 + 1;
> + if ((r = print_time(t, OUTPUT_FIELD_DATE_RAW, f)) < 0)
> + return r;
>
> + n += r;
> } else {
> - char buf[64];
> uint64_t x;
> - time_t t;
> - struct tm tm;
>
> r = -ENOENT;
>
> @@ -325,30 +400,25 @@ static int output_short(
> return r;
> }
>
> - t = (time_t) (x / USEC_PER_SEC);
> -
> - switch(mode) {
> + switch (formatter->mode) {
> case OUTPUT_SHORT_ISO:
> - r = strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S%z", localtime_r(&t, &tm));
> + if ((r = print_time(x, OUTPUT_FIELD_DATE_ISO, f)) < 0)
> + return r;
> +
> break;
> case OUTPUT_SHORT_PRECISE:
> - r = strftime(buf, sizeof(buf), "%b %d %H:%M:%S", localtime_r(&t, &tm));
> - if (r > 0) {
> - snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
> - ".%06llu", (unsigned long long) (x % USEC_PER_SEC));
> - }
> + if ((r = print_time(x, OUTPUT_FIELD_DATE_PRECISE, f)) < 0)
> + return r;
> +
> break;
> default:
> - r = strftime(buf, sizeof(buf), "%b %d %H:%M:%S", localtime_r(&t, &tm));
> - }
> + if ((r = print_time(x, OUTPUT_FIELD_DATE, f)) < 0)
> + return r;
>
> - if (r <= 0) {
> - log_error("Failed to format time.");
> - return -EINVAL;
> + break;
> }
>
> - fputs(buf, f);
> - n += strlen(buf);
> + n += r;
> }
>
> if (hostname && shall_print(hostname, hostname_len, flags)) {
> @@ -389,9 +459,9 @@ static int output_short(
> }
>
> static int output_verbose(
> + OutputFormatter *formatter,
> FILE *f,
> sd_journal *j,
> - OutputMode mode,
> unsigned n_columns,
> OutputFlags flags) {
>
> @@ -402,6 +472,7 @@ static int output_verbose(
> char ts[FORMAT_TIMESTAMP_MAX + 7];
> int r;
>
> + assert(formatter);
> assert(f);
> assert(j);
>
> @@ -493,9 +564,9 @@ static int output_verbose(
> }
>
> static int output_export(
> + OutputFormatter *formatter,
> FILE *f,
> sd_journal *j,
> - OutputMode mode,
> unsigned n_columns,
> OutputFlags flags) {
>
> @@ -507,6 +578,7 @@ static int output_export(
> const void *data;
> size_t length;
>
> + assert(formatter);
> assert(j);
>
> sd_journal_set_data_threshold(j, 0);
> @@ -631,9 +703,9 @@ void json_escape(
> }
>
> static int output_json(
> + OutputFormatter *formatter,
> FILE *f,
> sd_journal *j,
> - OutputMode mode,
> unsigned n_columns,
> OutputFlags flags) {
>
> @@ -647,6 +719,7 @@ static int output_json(
> Hashmap *h = NULL;
> bool done, separator;
>
> + assert(formatter);
> assert(j);
>
> sd_journal_set_data_threshold(j, flags & OUTPUT_SHOW_ALL ? 0 : JSON_THRESHOLD);
> @@ -669,7 +742,7 @@ static int output_json(
> return r;
> }
>
> - if (mode == OUTPUT_JSON_PRETTY)
> + if (formatter->mode == OUTPUT_JSON_PRETTY)
> fprintf(f,
> "{\n"
> "\t\"__CURSOR\" : \"%s\",\n"
> @@ -681,7 +754,7 @@ static int output_json(
> monotonic,
> sd_id128_to_string(boot_id, sid));
> else {
> - if (mode == OUTPUT_JSON_SSE)
> + if (formatter->mode == OUTPUT_JSON_SSE)
> fputs("data: ", f);
>
> fprintf(f,
> @@ -758,7 +831,7 @@ static int output_json(
> continue;
>
> if (separator) {
> - if (mode == OUTPUT_JSON_PRETTY)
> + if (formatter->mode == OUTPUT_JSON_PRETTY)
> fputs(",\n\t", f);
> else
> fputs(", ", f);
> @@ -833,9 +906,9 @@ static int output_json(
>
> } while (!done);
>
> - if (mode == OUTPUT_JSON_PRETTY)
> + if (formatter->mode == OUTPUT_JSON_PRETTY)
> fputs("\n}\n", f);
> - else if (mode == OUTPUT_JSON_SSE)
> + else if (formatter->mode == OUTPUT_JSON_SSE)
> fputs("}\n\n", f);
> else
> fputs(" }\n", f);
> @@ -852,9 +925,9 @@ finish:
> }
>
> static int output_cat(
> + OutputFormatter *formatter,
> FILE *f,
> sd_journal *j,
> - OutputMode mode,
> unsigned n_columns,
> OutputFlags flags) {
>
> @@ -862,6 +935,7 @@ static int output_cat(
> size_t l;
> int r;
>
> + assert(formatter);
> assert(j);
> assert(f);
>
> @@ -885,13 +959,117 @@ static int output_cat(
> return 0;
> }
>
> -static int (*output_funcs[_OUTPUT_MODE_MAX])(
> +static int output_format(
> + OutputFormatter *formatter,
> FILE *f,
> - sd_journal*j,
> - OutputMode mode,
> + sd_journal *j,
> unsigned n_columns,
> - OutputFlags flags) = {
> + OutputFlags flags) {
> +
> + size_t i;
> + const void *data;
> + size_t l;
> + int r;
> + uint64_t t;
> + bool truncated;
> +
> + assert(formatter);
> + assert(j);
> + assert(f);
> +
> + sd_journal_set_data_threshold(j, 0);
> +
> + for (i = 0 ; i < formatter->nfields ; i++) {
> + switch (formatter->fields[i].type) {
> + case OUTPUT_FIELD_LITERAL:
> + fputs(formatter->fields[i].data, f);
> + break;
> +
> + case OUTPUT_FIELD_DATE:
> + case OUTPUT_FIELD_DATE_PRECISE:
> + case OUTPUT_FIELD_DATE_ISO:
> + case OUTPUT_FIELD_DATE_RAW:
> + if (streq(formatter->fields[i].data,
> + "__MONOTONIC_TIMESTAMP")) {
> + sd_id128_t boot_id;
> + r = sd_journal_get_monotonic_usec(j, &t, &boot_id);
> + } else if (streq(formatter->fields[i].data,
> + "__REALTIME_TIMESTAMP")) {
> + r = sd_journal_get_realtime_usec(j, &t);
> + } else {
> + r = sd_journal_get_data(j, formatter->fields[i].data, &data, &l);
> +
> + if (r > 0) {
> + assert(l >= formatter->fields[i].datalen);
>
> + r = safe_atou64((const char *)data + formatter->fields[i].datalen + 1,
> + &t);
> + }
> + }
> +
> + if (r < 0) {
> + if (r == -ENOENT)
> + continue;
> +
> + log_error("Failed to get %s: %s",
> + formatter->fields[i].data, strerror(-r));
> + return r;
> + }
> +
> + r = print_time(t, formatter->fields[i].type, f);
> + if (r < 0)
> + return r;
> + break;
> +
> + case OUTPUT_FIELD_STRING:
> + case OUTPUT_FIELD_STRING_RAW:
> + sd_journal_set_data_threshold(j, 0);
> +
> + r = sd_journal_get_data(j, formatter->fields[i].data, &data, &l);
> + if (r < 0) {
> + if (r == -ENOENT)
> + continue;
> +
> + log_error("Failed to get data: %s", strerror(-r));
> + return r;
> + }
> +
> + assert(l >= formatter->fields[i].datalen);
> +
> + data = ((const char *)data) + formatter->fields[i].datalen + 1;
> + l -= formatter->fields[i].datalen + 1;
> +
> + if (formatter->fields[i].vallen != 0 &&
> + l > formatter->fields[i].vallen) {
> + l = formatter->fields[i].vallen;
> + truncated = true;
> + } else {
> + truncated = false;
> + }
> +
> + if (formatter->fields[i].type == OUTPUT_FIELD_STRING_RAW) {
> + fwrite((const char*) data, 1, l, f);
> + } else {
> + if (((const char *)data)[l - 1] == '\n')
> + l--;
> +
> + if (!utf8_is_printable(data, l)) {
> + char bytes[FORMAT_BYTES_MAX];
> + fprintf(f, "[%s blob data]", format_bytes(bytes, sizeof(bytes), l));
> + } else {
> + fwrite((const char*) data, 1, l, f);
> + }
> + if (truncated)
> + fputs("...", f);
> + }
> + break;
> + }
> + }
> +
> + return 0;
> +}
> +
> +static OutputFunc output_funcs[_OUTPUT_MODE_MAX] = {
> [OUTPUT_SHORT] = output_short,
> [OUTPUT_SHORT_ISO] = output_short,
> [OUTPUT_SHORT_PRECISE] = output_short,
> @@ -901,25 +1079,25 @@ static int (*output_funcs[_OUTPUT_MODE_MAX])(
> [OUTPUT_JSON] = output_json,
> [OUTPUT_JSON_PRETTY] = output_json,
> [OUTPUT_JSON_SSE] = output_json,
> - [OUTPUT_CAT] = output_cat
> + [OUTPUT_CAT] = output_cat,
> + [OUTPUT_FORMAT] = output_format,
> };
>
> int output_journal(
> FILE *f,
> sd_journal *j,
> - OutputMode mode,
> + OutputFormatter *formatter,
> unsigned n_columns,
> OutputFlags flags,
> bool *ellipsized) {
>
> int ret;
> - assert(mode >= 0);
> - assert(mode < _OUTPUT_MODE_MAX);
> + assert(formatter);
>
> if (n_columns <= 0)
> n_columns = columns();
>
> - ret = output_funcs[mode](f, j, mode, n_columns, flags);
> + ret = formatter->handler(formatter, f, j, n_columns, flags);
> fflush(stdout);
>
> if (ellipsized && ret > 0)
> @@ -945,7 +1123,7 @@ static int maybe_print_begin_newline(FILE *f, OutputFlags *flags) {
>
> static int show_journal(FILE *f,
> sd_journal *j,
> - OutputMode mode,
> + OutputFormatter *formatter,
> unsigned n_columns,
> usec_t not_before,
> unsigned how_many,
> @@ -958,8 +1136,7 @@ static int show_journal(FILE *f,
> int warn_cutoff = flags & OUTPUT_WARN_CUTOFF;
>
> assert(j);
> - assert(mode >= 0);
> - assert(mode < _OUTPUT_MODE_MAX);
> + assert(formatter);
>
> /* Seek to end */
> r = sd_journal_seek_tail(j);
> @@ -1002,7 +1179,7 @@ static int show_journal(FILE *f,
> line ++;
> maybe_print_begin_newline(f, &flags);
>
> - r = output_journal(f, j, mode, n_columns, flags, ellipsized);
> + r = output_journal(f, j, formatter, n_columns, flags, ellipsized);
> if (r < 0)
> goto finish;
> }
> @@ -1250,7 +1427,7 @@ int add_match_this_boot(sd_journal *j, const char *machine) {
> int show_journal_by_unit(
> FILE *f,
> const char *unit,
> - OutputMode mode,
> + OutputFormatter *formatter,
> unsigned n_columns,
> usec_t not_before,
> unsigned how_many,
> @@ -1263,8 +1440,7 @@ int show_journal_by_unit(
> int r;
> int jflags = SD_JOURNAL_LOCAL_ONLY | system * SD_JOURNAL_SYSTEM;
>
> - assert(mode >= 0);
> - assert(mode < _OUTPUT_MODE_MAX);
> + assert(formatter);
> assert(unit);
>
> if (how_many <= 0)
> @@ -1292,7 +1468,7 @@ int show_journal_by_unit(
> log_debug("Journal filter: %s", filter);
> }
>
> - return show_journal(f, j, mode, n_columns, not_before, how_many, flags, ellipsized);
> + return show_journal(f, j, formatter, n_columns, not_before, how_many, flags, ellipsized);
> }
>
> static const char *const output_mode_table[_OUTPUT_MODE_MAX] = {
> @@ -1305,7 +1481,257 @@ static const char *const output_mode_table[_OUTPUT_MODE_MAX] = {
> [OUTPUT_JSON] = "json",
> [OUTPUT_JSON_PRETTY] = "json-pretty",
> [OUTPUT_JSON_SSE] = "json-sse",
> - [OUTPUT_CAT] = "cat"
> + [OUTPUT_CAT] = "cat",
> + [OUTPUT_FORMAT] = "format",
> };
>
> DEFINE_STRING_TABLE_LOOKUP(output_mode, OutputMode);
> +
> +
> +void output_formatter_free(OutputFormatter **formatter) {
> + if (!*formatter)
> + return;
> +
> + free(*formatter);
> + *formatter = NULL;
> +}
> +
> +static int output_formatter_add_field(OutputFormatter *formatter,
> + OutputFieldType type,
> + size_t vallen,
> + char *data,
> + size_t datalen)
> +{
> + OutputField *fields;
> +
> + fields = realloc_multiply(formatter->fields,
> + sizeof(OutputField),
> + formatter->nfields + 1);
> + if (!fields)
> + return -ENOMEM;
> +
> + fields[formatter->nfields].type = type;
> + fields[formatter->nfields].vallen = vallen;
> + fields[formatter->nfields].data = data;
> + fields[formatter->nfields].datalen = datalen;
> + formatter->fields = fields;
> + formatter->nfields++;
> +
> + return 0;
> +}
> +
> +
> +static int output_formatter_expand_escapes(char *data)
> +{
> + size_t i, j;
> +
> + for (i = 0, j = 0; data[i] != '\0'; i++) {
> + if (data[i] == '\\') {
> + i++;
> + switch (data[i]) {
> + case '\'':
> + data[j++] = '\'';
> + break;
> + case '"':
> + data[j++] = '"';
> + break;
> + case '?':
> + data[j++] = '?';
> + break;
> + case '\\':
> + data[j++] = '\\';
> + break;
> + case 'a':
> + data[j++] = '\a';
> + break;
> + case 'b':
> + data[j++] = '\b';
> + break;
> + case 'f':
> + data[j++] = '\f';
> + break;
> + case 'n':
> + data[j++] = '\n';
> + break;
> + case 'r':
> + data[j++] = '\r';
> + break;
> + case 't':
> + data[j++] = '\t';
> + break;
> + case 'v':
> + data[j++] = '\v';
> + break;
> + default:
> + return -EINVAL;
> + }
> + } else {
> + data[j++] = data[i];
> + }
> + }
> + data[j] = '\0';
> +
> + return 0;
> +}
> +
> +static int output_formatter_parse_format(OutputFormatter *formatter,
> + const char *format) {
> + const char *prev = format;
> + int r;
> +
> + do {
> + const char *next;
> + char *data;
> + size_t datalen;
> +
> + next = strstr(format, "%(");
> +
> + if (!next || next != prev) {
> + datalen = next ? (size_t)(next - prev) : strlen(prev);
> + data = strndup(prev, datalen);
> +
> + if ((r = output_formatter_expand_escapes(data)) < 0)
> + return r;
> +
> + if ((r = output_formatter_add_field(formatter,
> + OUTPUT_FIELD_LITERAL, 0,
> + data, datalen)) < 0) {
> + free(data);
> + return r;
> + }
> + }
> +
> + if (next) {
> + const char *end;
> + OutputFieldType type;
> + char *typestr;
> + char *lenstr;
> + uint64_t vallen;
> +
> + end = strchr(next, ')');
> + if (!end)
> + return -EINVAL;
> +
> + datalen = (end - (next + 2));
> + data = strndup(next + 2, datalen);
> +
> + if ((typestr = strchr(data, ':')) != NULL) {
> + datalen = typestr - data;
> + *typestr = '\0';
> + typestr++;
> + lenstr = strchr(typestr, ':');
> + if (lenstr) {
> + *lenstr = '\0';
> + lenstr++;
> + if ((r = safe_atou64(lenstr, &vallen)) < 0) {
> + free(data);
> + return r;
> + }
> + }
> +
> + if (streq(typestr, "string"))
> + type = OUTPUT_FIELD_STRING;
> + else if (streq(typestr, "date"))
> + type = OUTPUT_FIELD_DATE;
> + else if (streq(typestr, "date-precise"))
> + type = OUTPUT_FIELD_DATE_PRECISE;
> + else if (streq(typestr, "date-iso"))
> + type = OUTPUT_FIELD_DATE_ISO;
> + else if (streq(typestr, "date-raw"))
> + type = OUTPUT_FIELD_DATE_RAW;
> + else if (streq(typestr, "string-raw"))
> + type = OUTPUT_FIELD_STRING_RAW;
> + else {
> + free(data);
> + return -EINVAL;
> + }
> + } else {
> + if (streq(data, "__MONOTONIC_TIMESTAMP"))
> + type = OUTPUT_FIELD_DATE_RAW;
> + else if (streq(data, "__REALTIME_TIMESTAMP"))
> + type = OUTPUT_FIELD_DATE;
> + else
> + type = OUTPUT_FIELD_STRING;
> + }
> +
> + if ((r = output_formatter_add_field(formatter,
> + type, vallen,
> + data, datalen)) < 0) {
> + free(data);
> + return r;
> + }
> + format = end + 1;
> + prev = end + 1;
> + } else {
> + format = NULL;
> + }
> + } while (format && *format);
> +
> + return 0;
> +}
> +
> +#define DEFAULT_FORMAT "%(__REALTIME_TIMESTAMP) %(_HOSTNAME) %(SYSLOG_IDENTIFIER)[%(_PID)]: %(MESSAGE)\\n"
> +
> +int output_formatter_from_mode(OutputMode mode,
> + OutputFormatter **formatter)
> +{
> + int r;
> + _cleanup_output_formatter_ OutputFormatter *ret = NULL;
> +
> + assert(mode >= 0);
> + assert(mode < _OUTPUT_MODE_MAX);
> + assert(formatter);
> + assert(!*formatter);
> +
> + if (!(ret = new(OutputFormatter, 1)))
> + return -ENOMEM;
> +
> + ret->mode = mode;
> + if (ret->mode == OUTPUT_FORMAT &&
> + ((r = output_formatter_parse_format(ret, DEFAULT_FORMAT)) < 0))
> + return r;
> + ret->handler = output_funcs[ret->mode];
> +
> + *formatter = ret;
> + ret = NULL;
> + return 0;
> +}
> +
> +
> +int output_formatter_from_string(const char *arg,
> + OutputFormatter **formatter) {
> + int r;
> + _cleanup_output_formatter_ OutputFormatter *ret = NULL;
> +
> + assert(arg);
> + assert(formatter);
> + assert(!*formatter);
> +
> + if (!(ret = new(OutputFormatter, 1)))
> + return -ENOMEM;
> +
> + if (strneq(arg, "format:", 7)) {
> + ret->mode = OUTPUT_FORMAT;
> + if ((r = output_formatter_parse_format(ret, arg + 7)) < 0)
> + return r;
> + } else {
> + if ((ret->mode = output_mode_from_string(arg)) < 0)
> + return -EINVAL;
> +
> + if (ret->mode == OUTPUT_FORMAT &&
> + ((r = output_formatter_parse_format(ret, DEFAULT_FORMAT)) < 0))
> + return r;
> + }
> + ret->handler = output_funcs[ret->mode];
> +
> + *formatter = ret;
> + ret = NULL;
> + return 0;
> +}
> +
> +
> +OutputMode output_formatter_get_mode(OutputFormatter *formatter) {
> + assert(formatter);
> +
> + return formatter->mode;
> +}
> diff --git a/src/shared/logs-show.h b/src/shared/logs-show.h
> index 187ee59..84e3f3d 100644
> --- a/src/shared/logs-show.h
> +++ b/src/shared/logs-show.h
> @@ -30,10 +30,12 @@
> #include "util.h"
> #include "output-mode.h"
>
> +typedef struct _OutputFormatter OutputFormatter;
> +
> int output_journal(
> FILE *f,
> sd_journal *j,
> - OutputMode mode,
> + OutputFormatter *formatter,
> unsigned n_columns,
> OutputFlags flags,
> bool *ellipsized);
> @@ -52,7 +54,7 @@ int add_matches_for_user_unit(
> int show_journal_by_unit(
> FILE *f,
> const char *unit,
> - OutputMode mode,
> + OutputFormatter *formatter,
> unsigned n_columns,
> usec_t not_before,
> unsigned how_many,
> @@ -69,3 +71,13 @@ void json_escape(
>
> const char* output_mode_to_string(OutputMode m) _const_;
> OutputMode output_mode_from_string(const char *s) _pure_;
> +
> +int output_formatter_from_string(const char *arg,
> + OutputFormatter **formatter);
> +int output_formatter_from_mode(OutputMode mode,
> + OutputFormatter **formatter);
> +
> +OutputMode output_formatter_get_mode(OutputFormatter *formatter);
> +
> +void output_formatter_free(OutputFormatter **formatter);
> +#define _cleanup_output_formatter_ _cleanup_(output_formatter_free)
> diff --git a/src/shared/output-mode.h b/src/shared/output-mode.h
> index ac1bb01..0f695ac 100644
> --- a/src/shared/output-mode.h
> +++ b/src/shared/output-mode.h
> @@ -32,6 +32,7 @@ typedef enum OutputMode {
> OUTPUT_JSON_PRETTY,
> OUTPUT_JSON_SSE,
> OUTPUT_CAT,
> + OUTPUT_FORMAT,
> _OUTPUT_MODE_MAX,
> _OUTPUT_MODE_INVALID = -1
> } OutputMode;
> diff --git a/src/systemctl/systemctl.c b/src/systemctl/systemctl.c
> index 9012128..5deeb70 100644
> --- a/src/systemctl/systemctl.c
> +++ b/src/systemctl/systemctl.c
> @@ -134,7 +134,8 @@ static enum action {
> static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
> static char *arg_host = NULL;
> static unsigned arg_lines = 10;
> -static OutputMode arg_output = OUTPUT_SHORT;
> +static const char *arg_output = "short";
> +static OutputFormatter *arg_formatter = NULL;
> static bool arg_plain = false;
>
> static const struct {
> @@ -3486,7 +3487,7 @@ static void print_status_info(
> if (i->id && arg_transport == BUS_TRANSPORT_LOCAL) {
> show_journal_by_unit(stdout,
> i->id,
> - arg_output,
> + arg_formatter,
> 0,
> i->inactive_exit_timestamp_monotonic,
> arg_lines,
> @@ -5491,7 +5492,8 @@ static void systemctl_help(void) {
> " --root=PATH Enable unit files in the specified root directory\n"
> " -n --lines=INTEGER Number of journal entries to show\n"
> " -o --output=STRING Change journal output mode (short, short-monotonic,\n"
> - " verbose, export, json, json-pretty, json-sse, cat)\n"
> + " verbose, export, json, json-pretty, json-sse, cat,\n"
> + " format:FORMAT-STRING)\n"
> " --plain Print unit dependencies as a list instead of a tree\n\n"
> "Unit Commands:\n"
> " list-units [PATTERN...] List loaded units\n"
> @@ -5712,6 +5714,7 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
> };
>
> int c;
> + int err;
>
> assert(argc >= 0);
> assert(argv);
> @@ -5933,11 +5936,7 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
> break;
>
> case 'o':
> - arg_output = output_mode_from_string(optarg);
> - if (arg_output < 0) {
> - log_error("Unknown output '%s'.", optarg);
> - return -EINVAL;
> - }
> + arg_output = optarg;
> break;
>
> case 'i':
> @@ -5996,6 +5995,11 @@ static int systemctl_parse_argv(int argc, char *argv[]) {
> return -EINVAL;
> }
>
> + if ((err = output_formatter_from_string(arg_output, &arg_formatter)) < 0) {
> + log_error("Unknown / malformed output format '%s'.", arg_output);
> + return err;
> + }
> +
> return 1;
> }
>
> --
> 1.9.3
>
> _______________________________________________
> systemd-devel mailing list
> systemd-devel at lists.freedesktop.org
> http://lists.freedesktop.org/mailman/listinfo/systemd-devel
>
More information about the systemd-devel
mailing list