#include "pretty-print.h"
#include "sigbus.h"
#include "signal-util.h"
+#include "time-util.h"
#include "tmpfile-util.h"
#define JOURNAL_WAIT_TIMEOUT (10*USEC_PER_SEC)
OutputMode mode;
char *cursor;
+ usec_t since, until;
int64_t n_skip;
uint64_t n_entries;
- bool n_entries_set;
+ bool n_entries_set, since_set, until_set;
FILE *tmp;
uint64_t delta, size;
return MHD_CONTENT_READER_END_OF_STREAM;
}
+ if (m->until_set) {
+ usec_t usec;
+
+ r = sd_journal_get_realtime_usec(m->journal, &usec);
+ if (r < 0) {
+ log_error_errno(r, "Failed to determine timestamp: %m");
+ return MHD_CONTENT_READER_END_WITH_ERROR;
+ }
+ if (usec > m->until)
+ return MHD_CONTENT_READER_END_OF_STREAM;
+ }
pos -= m->size;
m->delta += m->size;
return 0;
}
-static int request_parse_range(
+static int request_parse_range_skip_and_n_entries(
RequestMeta *m,
- struct MHD_Connection *connection) {
+ const char *colon) {
- const char *range, *colon, *colon2;
+ const char *p, *colon2;
int r;
- assert(m);
- assert(connection);
+ colon2 = strchr(colon + 1, ':');
+ if (colon2) {
+ _cleanup_free_ char *t = NULL;
- range = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Range");
- if (!range)
- return 0;
+ t = strndup(colon + 1, colon2 - colon - 1);
+ if (!t)
+ return -ENOMEM;
- if (!startswith(range, "entries="))
- return 0;
-
- range += 8;
- range += strspn(range, WHITESPACE);
+ r = safe_atoi64(t, &m->n_skip);
+ if (r < 0)
+ return r;
+ }
- colon = strchr(range, ':');
- if (!colon)
- m->cursor = strdup(range);
- else {
- const char *p;
+ p = (colon2 ?: colon) + 1;
+ r = safe_atou64(p, &m->n_entries);
+ if (r < 0)
+ return r;
- colon2 = strchr(colon + 1, ':');
- if (colon2) {
- _cleanup_free_ char *t = NULL;
+ if (m->n_entries <= 0)
+ return -EINVAL;
- t = strndup(colon + 1, colon2 - colon - 1);
- if (!t)
- return -ENOMEM;
+ m->n_entries_set = true;
- r = safe_atoi64(t, &m->n_skip);
- if (r < 0)
- return r;
- }
+ return 0;
+}
- p = (colon2 ?: colon) + 1;
- if (*p) {
- r = safe_atou64(p, &m->n_entries);
- if (r < 0)
- return r;
+static int request_parse_range_entries(
+ RequestMeta *m,
+ const char *entries_request) {
- if (m->n_entries <= 0)
- return -EINVAL;
+ const char *colon;
+ int r;
- m->n_entries_set = true;
- }
+ colon = strchr(entries_request, ':');
+ if (!colon)
+ m->cursor = strdup(entries_request);
+ else {
+ r = request_parse_range_skip_and_n_entries(m, colon);
+ if (r < 0)
+ return r;
- m->cursor = strndup(range, colon - range);
+ m->cursor = strndup(entries_request, colon - entries_request);
}
-
if (!m->cursor)
return -ENOMEM;
return 0;
}
+static int request_parse_range_time(
+ RequestMeta *m,
+ const char *time_request) {
+
+ _cleanup_free_ char *until = NULL;
+ const char *colon;
+ int r;
+
+ colon = strchr(time_request, ':');
+ if (!colon)
+ return -EINVAL;
+
+ if (colon - time_request > 0) {
+ _cleanup_free_ char *t = NULL;
+
+ t = strndup(time_request, colon - time_request);
+ if (!t)
+ return -ENOMEM;
+
+ r = parse_sec(t, &m->since);
+ if (r < 0)
+ return r;
+
+ m->since_set = true;
+ }
+
+ time_request = colon + 1;
+ colon = strchr(time_request, ':');
+ if (!colon)
+ until = strdup(time_request);
+ else {
+ r = request_parse_range_skip_and_n_entries(m, colon);
+ if (r < 0)
+ return r;
+
+ until = strndup(time_request, colon - time_request);
+ }
+ if (!until)
+ return -ENOMEM;
+
+ if (!isempty(until)) {
+ r = parse_sec(until, &m->until);
+ if (r < 0)
+ return r;
+
+ m->until_set = true;
+ if (m->until < m->since)
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int request_parse_range(
+ RequestMeta *m,
+ struct MHD_Connection *connection) {
+
+ const char *range, *range_after_eq;
+
+ assert(m);
+ assert(connection);
+
+ range = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Range");
+ if (!range)
+ return 0;
+
+ m->n_skip = 0;
+ range_after_eq = startswith(range, "entries=");
+ if (range_after_eq) {
+ range_after_eq += strspn(range_after_eq, WHITESPACE);
+ return request_parse_range_entries(m, range_after_eq);
+ }
+
+ range_after_eq = startswith(range, "realtime=");
+ if (startswith(range, "realtime=")) {
+ range_after_eq += strspn(range_after_eq, WHITESPACE);
+ return request_parse_range_time(m, range_after_eq);
+ }
+
+ return 0;
+}
+
static mhd_result request_parse_arguments_iterator(
void *cls,
enum MHD_ValueKind kind,
if (m->cursor)
r = sd_journal_seek_cursor(m->journal, m->cursor);
+ else if (m->since_set)
+ r = sd_journal_seek_realtime_usec(m->journal, m->since);
else if (m->n_skip >= 0)
r = sd_journal_seek_head(m->journal);
+ else if (m->until_set && m->n_skip < 0)
+ r = sd_journal_seek_realtime_usec(m->journal, m->until);
else if (m->n_skip < 0)
r = sd_journal_seek_tail(m->journal);
+
if (r < 0)
return mhd_respond(connection, MHD_HTTP_BAD_REQUEST, "Failed to seek in journal.");
TEST_MESSAGE="-= This is a test message $RANDOM =-"
TEST_TAG="$(systemd-id128 new)"
+BEFORE_TIMESTAMP="$(date +%s)"
echo "$TEST_MESSAGE" | systemd-cat -t "$TEST_TAG"
+sleep 1
journalctl --sync
TEST_CURSOR="$(journalctl -q -t "$TEST_TAG" -n 0 --show-cursor | awk '{ print $3; }')"
BOOT_CURSOR="$(journalctl -q -b -n 0 --show-cursor | awk '{ print $3; }')"
+AFTER_TIMESTAMP="$(date +%s)"
/usr/lib/systemd/systemd-journal-gatewayd --version
/usr/lib/systemd/systemd-journal-gatewayd --help
# Check if the specified cursor refers to an existing entry and return just that entry
curl -Lfs --header "Accept: application/json" --header "Range: entries=$TEST_CURSOR" http://localhost:19531/entries?discrete | \
jq -se "length == 1 and select(.[].MESSAGE == \"$TEST_MESSAGE\")"
+# Check entry is present (resp. absent) when filtering by timestamp
+curl -Lfs --header "Range: realtime=$BEFORE_TIMESTAMP:" http://localhost:19531/entries?SYSLOG_IDENTIFIER="$TEST_TAG" | \
+ grep -qE " $TEST_TAG\[[0-9]+\]: $TEST_MESSAGE"
+curl -Lfs --header "Range: realtime=:$AFTER_TIMESTAMP" http://localhost:19531/entries?SYSLOG_IDENTIFIER="$TEST_TAG" | \
+ grep -qE " $TEST_TAG\[[0-9]+\]: $TEST_MESSAGE"
+curl -Lfs --header "Accept: application/json" --header "Range: realtime=:$BEFORE_TIMESTAMP" http://localhost:19531/entries?SYSLOG_IDENTIFIER="$TEST_TAG" | \
+ jq -se "length == 0"
+curl -Lfs --header "Accept: application/json" --header "Range: realtime=$AFTER_TIMESTAMP:" http://localhost:19531/entries?SYSLOG_IDENTIFIER="$TEST_TAG" | \
+ jq -se "length == 0"
+# Check positive and negative skip when filtering by timestamp
+echo "-= This is a second test message =-" | systemd-cat -t "$TEST_TAG"
+journalctl --sync
+TEST2_CURSOR="$(journalctl -q -t "$TEST_TAG" -n 0 --show-cursor | awk '{ print $3; }')"
+echo "-= This is a third test message =-" | systemd-cat -t "$TEST_TAG"
+journalctl --sync
+sleep 1
+END_TIMESTAMP="$(date +%s)"
+curl -Lfs --header "Accept: application/json" --header "Range: realtime=$BEFORE_TIMESTAMP::1:1" http://localhost:19531/entries?SYSLOG_IDENTIFIER="$TEST_TAG" | \
+ jq -se "length == 1 and select(.[].__CURSOR == \"$TEST2_CURSOR\")"
+curl -Lfs --header "Accept: application/json" --header "Range: realtime=$END_TIMESTAMP::-1:1" http://localhost:19531/entries?SYSLOG_IDENTIFIER="$TEST_TAG" | \
+ jq -se "length == 1 and select(.[].__CURSOR == \"$TEST2_CURSOR\")"
+
# No idea how to properly parse this (jq won't cut it), so let's at least do some sanity checks that every
# line is either empty or begins with data:
curl -Lfs --header "Accept: text/event-stream" http://localhost:19531/entries | \