[systemd-devel] [PATCH] journalctl: allow customizable output formats

Daniel P. Berrange berrange at redhat.com
Mon Sep 22 08:33:28 PDT 2014


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.

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



More information about the systemd-devel mailing list