#! /usr/bin/python3 -s
# -*- coding: utf-8 -*-
# 
# !!! IMPORTANT !!!
# This is just a short term script for use by oca-requesttimes-metric. This has only been tested for
# - running as root
# - remote journals
# - bookmark file + "--since"
# - forwards only
#
# long term, this script will be dropped in lieu of a new ng-journalctl
# 
#
# TODO/KNOWN ISSUES
#  * --since/--until don't support out of range conditions
#  * color highlighting by serverity isn't implemented
#  * auto-filters for sources aren't implemented (eg @backends, @all, @local, @headends, @<hostname> @<ip>)
#  * user/group matching and esclation aren't implemented
#  * --vacuum-* isn't implemented
#  * --bookmark - if using the pager, and you don't get to the end of the results, quitting errors. This error probably
#                 also occurs if piping and you don't get to the end of the results

import sys, os, glob, re
import argparse
import pyparsing
import datetime as dt
import dateutil.relativedelta
import collections
import systemd.journal
import systemd.id128
import itertools
import heapq
import errno
import json
import uuid
import fnmatch
import pytz
import tzlocal
import select
import subprocess
import signal
import ctypes

import pprint

# default timezone, systemd.journal.JournalReader will return datetime objects
# as 'naive' objects in this timezone.
TZ = tzlocal.get_localzone()

#
# Parse an absolute or relative timestamp to a datetime object
# TODO: +/-N<units>[N<units>[...]]
#
def timestamp(x):
	# We want to reference the same now/today for --since and --prev
	if not hasattr(timestamp, 'now'):
		timestamp.now = dt.datetime.now()

	# Keywords: now/yesterday/today/tomorrow
	try:
		return {
			'yesterday': dt.datetime.combine(timestamp.now - dt.timedelta(days=1), dt.time()),
			'today'    : dt.datetime.combine(timestamp.now, dt.time()),
			'now'      : timestamp.now,
			'tomorrow' : dt.datetime.combine(timestamp.now + dt.timedelta(days=1), dt.time())
		}[x]
	except KeyError:		
		pass

	# @<epoch>[.<fractional>]
	try:
		return dt.datetime.fromtimestamp(float(re.match('@([0-9]+(\.[0-9]+)?)$', x).group(1)))
	except (AttributeError, ValueError):
		pass

	# +/- relative
	try:
		m = re.match(
			r'\s*(?P<sign>[-+])\s*('
			r'(?P<microseconds>\d+)\s*(?:us|usec)'
			r'|(?P<millseconds>\d+)\s*(?:ms|msec)'
			r'|(?P<seconds>\d+)\s*(?:s|sec|seconds?)'
			r'|(?P<months>\d+)\s*(?:months?)'
			r'|(?P<minutes>\d+)\s*(?:m|min|minutes?)'
			r'|(?P<hours>\d+)\s*(?:h|hr|hours?)'
			r'|(?P<days>\d+)\s*(?:d|days?)'
			r'|(?P<weeks>\d+)\s*(?:w|weeks?)'
			r'|(?P<years>\d+)\s*(?:y|years?)'
			r')+',
			x
		)
		if m:
			m=m.groupdict()
			mul = -1 if m['sign'] == '-' else +1
			return timestamp.now + dateutil.relativedelta.relativedelta(
				 **{
					k: mul*int(m[k]) for k in m if (k != 'sign' and m[k] is not None)
				 }
			)
	except Exception as e:
		print e

	# ISO 8601-ish options
	for time in ['%H:%M:%S.%f', '%H:%M:%S', '%H:%M', None]:
		for join in ['T', ' ']:
			fmt = join.join(['%Y-%m-%d', time])
			try:
				return dt.datetime.strptime(x, fmt)
			except ValueError as e:
				pass
	raise ValueError


# Parse a priority name, number or range (x..y) into a set of ints
def priority(x):
	prios = {
		'0': 0, 'emerg'  : 0,
		'1': 1, 'alert'  : 1,
		'2': 2, 'crit'   : 2,
		'3': 3, 'err'    : 3,
		'4': 4, 'warning': 4,
		'5': 5, 'notice' : 5,
		'6': 6, 'info'   : 6,
		'7': 7, 'debug'  : 7,
		None: None
	}
	low, high = (x.split('..', 2) + [None])[:2]
	low = prios.get(low, None)
	high = prios.get(high, high)
	if low is None:
		raise ValueError
	elif high is None:
		return set([low])
	else:
		return set(range(min(low,high), max(low,high)+1))

# Parse and int OR the string 'all' (case insensitive) which will yield sys.maxsize
def lines(x):
	return sys.maxsize if x.lower() == 'all' else int(x)

# Merge sets
class MergeSet(argparse.Action):
	def __call__(self, parser, namespace, values, option_string=None):
		x = getattr(namespace, self.dest)
		if x is None:
			x = set()
		# Protect strings from getting exploded
		if not type(values) is set and not type(values) is list:
			values = [values]
		setattr(namespace, self.dest, x.union(values))

parser = argparse.ArgumentParser(description='Remote journal aware journalctl replacement')
# journalctl compatible options
parser.add_argument('--since',            action='store',     type=timestamp)
parser.add_argument('--until',            action='store',     type=timestamp)
parser.add_argument('--unit',       '-u', action='append',    type=str,       default=[])
parser.add_argument('--identifier', '-t', action='append',    type=str,       default=[])
parser.add_argument('--priority',   '-p', action= MergeSet,   type=priority,  default=set()) 
group1 = parser.add_mutually_exclusive_group()
group1.add_argument('--follow',     '-f', action='store_true')
# Note that type=lines is used to handle 'all' which is stored as sys.maxsize (as distinct from
# a value of None which indicates that the flag wasn't specified
parser.add_argument('--lines',      '-n', action='store',     type=lines, default=None)
parser.add_argument('--no-tail',          action='store_true')
group3 = parser.add_mutually_exclusive_group()
group3.add_argument('--pager-end',  '-e', action='store_true')
group3.add_argument('--no-pager',         action='store_true')
group1.add_argument('--reverse',    '-r', action='store_true')
parser.add_argument('--output',     '-o', action='store',     type=str,       default='short', choices=['short', 'short-iso', 'short-iso-precise', 'short-precise', 'short-monotonic', 'verbose', 'export', 'json', 'json-pretty', 'json-sse', 'cat'])
# Note: argparse takes 'default' from the first instance of a given 'dest' so we duplicate between --utc and --timezone
group2 = parser.add_mutually_exclusive_group()
group2.add_argument('--utc',              action='store_const', const=pytz.utc, dest='timezone', default=TZ) 
parser.add_argument('--file', '--files',  action= MergeSet, default=set())
parser.add_argument('--directory',  '-D', action= MergeSet,    type=str,         dest='file')
parser.add_argument('--dmesg',      '-k', action='store_true')
# Note --boot/-b +/-<N> is not supported, only boot UUID format
parser.add_argument('--boot',       '-b', action='store',     type=uuid.UUID,   default=None)
parser.add_argument('filters', nargs='*')

# non journalctl options
group2.add_argument('--timezone',   '-z', action='store',     type=pytz.timezone, default=TZ)
group1.add_argument('--bookmark',   '-B', action='store',     type=str, default=None)


# Two passes so that filters can be interleaved with flags, bascially argparse is missing features vs getopt / optparse
#args, tmp = parser.parse_known_args()
#print args
#print tmp
args = parser.parse_args() # tmp, args)

# Process directory options into file globs
args.file = [os.path.join(x,'*.journal') if os.path.isdir(x) else x for x in args.file]

# '@' filters are journal sets
for f in args.filters:
	if not f.startswith('@'):
		continue
	if f in ['@remote', '@all']:
		# Include remote journals
		args.file += ['/var/log/journal/remote/*.journal']


# --follow implies --lines=10 if unspecified
if args.follow:
	args.lines = 10 if args.lines is None else args.lines

# --pager-end implies --lines=1000 if unspecified
if args.pager_end:
	args.lines = 1000 if args.lines is None else args.lines

if args.no_tail:
	args.lines = sys.maxsize

if args.since and args.until and args.since > args.until:
	print "--since > --until"
	exit(1)

bookmark = None
if args.bookmark:
	try:
		bookmark = open(args.bookmark, 'r').read().strip()
	except:
		pass

	if bookmark:
		bookmark = json.loads(bookmark)
		if bookmark['format'] != 'ng-journalctl-bookmark' or bookmark['version'] != '1':
			sys.stderr.write("Specified bookmark file '%s' does not contain a valid bookmark" % args.bookmark)


# Bash-like brace-expansion
# TODO: range expansion {x..y}?
def braceexpand(pattern):
	lex = pyparsing
	lb     = lex.Suppress('{')
	rb     = lex.Suppress('}')
	text   = lex.Optional(lex.Group(lex.CharsNotIn('{},')))
	expr   = lex.Forward()
	item   = text ^ lex.Group(expr)
	braces = lex.Group(lb + lex.delimitedList(item) + rb)
	expr  << text + lex.ZeroOrMore(braces + text)

	# Parse it
	tokens = expr.parseString(pattern).asList()

	# Expand the combinations out (recursive tree walk)
	results = []
	def _expand(prefix, tokens):
		if len(tokens):
			t = tokens.pop(0)
			if type(t) is list:
				for x in t:
					if type(x) is list:
						_expand(prefix, x + tokens)
					else:
						_expand(prefix + x, tokens)
			else:
				_expand(prefix + t, tokens)
		else:
			results.append(prefix)
	_expand('', tokens)
	return results


# TODO: This needs an option (--system / --no-system ?)
journals = collections.defaultdict(set)
journals['system'].add(None)

# TODO: Dupe detection
for pattern in args.file:
	for x in braceexpand(pattern):
		for y in glob.glob(x):
			fn = os.path.basename(y)
			fn = re.sub('\.journal$', '', fn)
			fn = re.sub('@.*$', '', fn)
			fn = re.sub('^remote-', '', fn)
			journals[fn].add(y)

# Journal Entry wrapper class
class JournalEntry(dict):

	# Override __lt__ for the benefit of heapq.merge() since it can't take a comparison func
	def __lt__(self, other):
		if isinstance(other, self.__class__):
			# TODO: self.source.reverse != other.source.reverse
			#       does this even make sense though?
			k = '__REALTIME_TIMESTAMP'
			if (self.source.reverse):
				return self[k] > other[k]
			else:
				return self[k] < other[k]
		else:
			return super(self.__class__, self).__lt__(other)

	def _fmt(self, field, fmt=' %s', fallback='', indent=0):
		if not type(field) is list:
			field = [field]

		for x in field:
			if x in self:
				if type(self[x]) is str:
					res = (fmt % self[x]).decode('utf-8')
				else:
					res = fmt % self[x]
				if (indent and res.startswith(fmt % '\n')):
					indent = 2
				return res.replace('\n', '\n' + (' ' * indent))
		
		return fallback
		

	def fmt_short(self, datefmt='%b %m %H:%M:%S', tz=None):
		if datefmt == 'monotonic':
			# TODO: Does journalctl check the tail for the max timestamp length?
			res = '[%12.5lf] ' % self['__MONOTONIC_TIMESTAMP'][0].total_seconds()
		else:
			res = self['__REALTIME_TIMESTAMP'].astimezone(tz).strftime(datefmt)
		res += self._fmt('_HOSTNAME')
		res += self._fmt(['SYSLOG_IDENTIFIER', '_COMM'])
		res += self._fmt('_PID', '[%d]')
		res += self._fmt('MESSAGE', ': %s', '', len(res)+2)
		return res
			
	def fmt_json(self, **kwargs):
		data = self
		data['__MONOTONIC_TIMESTAMP'] = data['__MONOTONIC_TIMESTAMP'][0]
		if '_SYSTEMD_UNIT' in data:
			data['UNIT'] = data['_SYSTEMD_UNIT']
			del data['_SYSTEMD_UNIT']
		# Is this a bug?
		if type(data['_BOOT_ID']) is list:
			data['_BOOT_ID'] = data['_BOOT_ID'][0]
		return json.dumps(data, cls=JournalEncoder, **kwargs)

	def fmt_verbose(self, export=False, tz=None):
		if not export:
			res = self['__REALTIME_TIMESTAMP'].astimezone(tz).strftime('%a %Y-%m-%d %H:%M:%S.%f %Z') + ' [%s]' % self['__CURSOR']
			fmt = '\n    %s=%s'
		else:
			res = ''
			fmt = '%s=%s\n'

		for k,v in self.iteritems():
			if not export and k in ['__REALTIME_TIMESTAMP', '__MONOTONIC_TIMESTAMP', '__CURSOR']:
				continue
			if k == '__MONOTONIC_TIMESTAMP':
				v=v[0]
			if type(self[k]) is list:
				v=v[0]
			if isinstance(v, uuid.UUID):
				v = str(v).replace('-', '')
			if isinstance(v, dt.datetime):
				v = v.strftime('%s%f')
			if isinstance(v, dt.timedelta):
				v = str(int(v.total_seconds()*1000000)) 
			if k == '_SYSTEMD_UNIT':
				k = 'UNIT'
			res += fmt % (k, str(v).decode('utf-8'))
		return res
			

	def fmt(self, fmt, tz=None):
		if fmt is None or fmt == '' or fmt == 'short':
			return self.fmt_short(tz=tz)
		elif fmt == 'short-precise':
			return self.fmt_short('%b %m %H:%M:%S.%f', tz=tz)
		elif fmt == 'short-iso':
			return self.fmt_short('%Y-%m-%dT%H:%M:%S', tz=tz)
		elif fmt == 'short-monotonic':
			return self.fmt_short('monotonic', tz=tz)
		elif fmt == 'short-iso-precise':
			# TODO: Add TZ? rename?
			return self.fmt_short('%Y-%m-%dT%H:%M:%S.%f%z', tz=tz)
		elif fmt == 'cat':
			return self['MESSAGE'].decode('utf-8')
		elif fmt == 'json':
			return self.fmt_json(separators=(', ', ' : '))
		elif fmt == 'json-pretty':
			return self.fmt_json(indent=4, separators=(',', ' : '))
		elif fmt == 'json-sse':
			return 'data: ' + self.fmt_json(separators=(',', ' : ')) + '\n'
		elif fmt == 'verbose':
			return self.fmt_verbose(False, tz=tz)
		elif fmt == 'export':
			return self.fmt_verbose(True, tz=tz)
#
# A wrapper for the systemd.journal.Reader class to allow directional control of the
# iterator protocol, it also wraps the entries in the JournalEntry convience class,
# localizes datetimes and provides since/until timestamp filtering
#
class JournalReader(systemd.journal.Reader):
	def __init__(self, flags=0, path=None, files=None, converters=None, since=None, until=None, reverse=False, tz=tzlocal.get_localzone(), cursor=None):

		#conv = {
		#	'MESSAGE_ID': str,
		#	'_MACHINE_ID': str,
		#	'_BOOT_ID': str,
		#	'PRIORITY': int,
		#	'LEADER': int,
		#	'SESSION_ID': int,
		#	'USERSPACE_USEC': int,
		#	'INITRD_USEC': int,
		#	'KERNEL_USEC': int,
		#	'_UID': int,
		#	'_GID': int,
		#	'_PID': int,
		#	'SYSLOG_FACILITY': int,
		#	'SYSLOG_PID': int,
		#	'_AUDIT_SESSION': int,
		#	'_AUDIT_LOGINUID': int,
		#	'_SYSTEMD_SESSION': int,
		#	'_SYSTEMD_OWNER_UID': int,
		#	'CODE_LINE': int,
		#	'ERRNO': int,
		#	'EXIT_STATUS': int,
		#	'_SOURCE_REALTIME_TIMESTAMP': str,
		#	#'__REALTIME_TIMESTAMP': _convert_realtime,
		#	'_SOURCE_MONOTONIC_TIMESTAMP': str,
		#	'__MONOTONIC_TIMESTAMP': str,
		#	'__CURSOR': str, #trhis might cause problkems with --bookmark, _convert_trivial
		#	'COREDUMP': bytes,
		#	'COREDUMP_PID': int,
		#	'COREDUMP_UID': int,
		#	'COREDUMP_GID': int,
		#	'COREDUMP_SESSION': int,
		#	'COREDUMP_SIGNAL': int,
		#	'COREDUMP_TIMESTAMP': int,
		#}

		super(self.__class__, self).__init__(flags, path, files)
		self.since = since
		self.until = until
		self.tz = tz
		self.reverse = reverse
		self.current = None

		# If we have a bookmark cursor start there (+1)
		if cursor:
			try:
				self.seek_cursor(cursor)
				self.next()

				if (self.current and self.since):
					current_timestamp = self._convert_field('__REALTIME_TIMESTAMP',self.current['__REALTIME_TIMESTAMP'])
					if (current_timestamp < self.since):
						self.seek_realtime(since)

				return
			except:
				pp = pprint.PrettyPrinter(indent=4)
				sys.stderr.write(pp.pformat(sys.exc_info()))
				sys.exit(1)
				pass

		# Seek to a sensible starting point based on since/until/reverse
		# TODO: This doesn't yet cater for --lines so might need to be a seperate
		# helper function for that
		if reverse and until:
			self.seek_realtime(until)
		elif not reverse and since:
			self.seek_realtime(since)
		elif reverse:
			self.seek_tail()
		else:
			self.seek_head()

	#completly override - this results in a 300% performance improvement
	#TODO - only do this for 'cat' output, may also need specific items done on demand in JournalEntry as well
	def _convert_entry(self,entry):
		return entry

	# Iterate the journal in the current direction, checking for since/until bounds
	# This also tidies up the timestamp timezones
	def __next__(self):
		if (self.reverse):
			res = self.get_previous()
		else:
			res = self.get_next()
		if res:
			# Check for overrun of the time bound based on the current direction
			if (
				(self.reverse == False and self.until and res['__REALTIME_TIMESTAMP'] > self.until)
				or 
				(self.reverse == True  and self.since and res['__REALTIME_TIMESTAMP'] < self.since)
			):
				raise StopIteration()

			# Localize our timestamps so they actually know thier timezone
#			for k in ['__REALTIME_TIMESTAMP', '_SOURCE_REALTIME_TIMESTAMP']:
#				if k in res and type(res[k]) is dt.datetime:
#					res[k] = self.tz.localize(res[k])
			self.current = res
			res = JournalEntry(res)
			res.source = self
			return res
		else:
			raise StopIteration()

	# We need to override 'next' too
	next = __next__	


handles = {}
for name in journals:
	# Common args for both
	kwargs = {
		'since'   : args.since,
		'until'   : args.until,
		'reverse' : args.reverse
	}
	if (bookmark and name in bookmark['journal']):
		kwargs['cursor'] = bookmark['journal'][name]['cursor']
	if name == 'system':
		handles[name] = JournalReader(**kwargs)
	else:
		handles[name] = JournalReader(files=list(journals[name]), **kwargs)

for name, reader in handles.iteritems():
	# --unit / -i
	if args.unit:
		known_units = reader.query_unique('_SYSTEMD_UNIT')
	for unit in args.unit:
		# Try for pattern matches, should we brace expand here?
		matches = fnmatch.filter(known_units, unit)
		# If that fails try to add .* (unit types), should we try
		# .service on it's own first?
		if len(matches) == 0:
			matches = fnmatch.filter(known_units, '%s.*' % unit)
		# If that fails use the original value, since we still want to filter
		if len (matches) == 0:
			matches = [unit]
		for x in matches:
			reader.add_match('_SYSTEMD_UNIT=%s' % x)

	# --identifier / -t
	if args.identifier:
		# TODO - This used to do the same as "unit" above where it did query_unique and then matches
		#        This was INCREDIBLY slow, eg 5 seconds instead of 0.1 seconds
		#        Suggested behaviour was only do the regex matching if the identifier starts with a slash
		for tag in args.identifier:
			reader.add_match('SYSLOG_IDENTIFIER=%s' % tag)

	# --dmesg / -k
	if args.dmesg:
		# TODO: Should this be AND or OR?
		reader.add_match('_TRANSPORT=kernel')

	# --priority / -p
	for prio in args.priority:
		reader.add_match('PRIORITY=%d' % prio)


	# --since, --until, --reverse
	# are handled directly by the JournalReader wrapper class
	# but we may still need to seek here?

#sys.exit(1)


# JSON Encoding helper for json, json-pretty and json-sse ouput formats
class JournalEncoder(json.JSONEncoder):
	def default(self, obj):
		if isinstance(obj, uuid.UUID):
			return str(obj).replace('-', '')
		if isinstance(obj, dt.timedelta):
			return str(int(obj.total_seconds()*1000000))
		if isinstance(obj, dt.datetime):
			return obj.strftime('%s.%f')
		return super(self.__class__, self).default(obj)





class Pager(object):
	def __init__(self, no_pager=False, pager_end=False):
		self.process = None

		if no_pager:
			return

		if not os.isatty(sys.stdout.fileno()):
			return
	
		if os.environ.get('TERM', 'dumb') in ['', 'dumb']:
			return
	
		# TODO: Do we really want this to be configurable in this context?
		#       how do we ensure LESSSECURE= equivalents or can we run
		#       the pager as the calling user?
		PAGERCMD = os.environ.get('SYSTEMD_PAGER', os.environ.get('PAGER', 'less'))
		if PAGERCMD in ['', 'cat']:
			return

		# -F quit if one screen
		# -R Render ANSI color escapes in raw form
		# -S chop (scroll) long lines
		# -X Disable termcap initialization
		# -M Use verbose prompt
		# -K quit on ^C (vs abort tail etc) - do we really want this in the defaults?
		LESSOPTS = os.environ.get('SYSTEMD_LESS', 'FRSXMK')
		if pager_end:
			LESSOPTS += ' +G'
	
		# TODO: Customise the prompt to include the filters?

		self.process = subprocess.Popen(
			[PAGERCMD],
			env=dict(os.environ, LESS=LESSOPTS, LESSSECURE='1'),
			stdin=subprocess.PIPE,
			close_fds=True,
			preexec_fn=self._pager_start
		)

		if self.process and self.process.returncode is None:
			# TODO: Should we do something with stderr too?
			sys.stdout.flush()
			os.dup2(self.process.stdin.fileno(), sys.stdout.fileno())
			signal.signal(signal.SIGCHLD, self._pager_end)
		else:
			# Hide the evidence
			self.process = None

		# TODO: Should we retry with Shell=yes if the 1st launch failed?

	def _pager_start(self):
		# Set up the pager to die when we do
		PR_SET_PDEATHSIG = 1
		ctypes.cdll['libc.so.6'].prctl(PR_SET_PDEATHSIG, signal.SIGTERM)

	def _pager_end(self, signo, stackframe):
		# Our pager died, since it's now our raison d'être we need to follow
		sys.exit(0)

	def done(self):
		if self.process:
			os.close(sys.stdout.fileno())
			self.process.stdin.close()
			self.process.wait()


# Start an external pager (maybe)
pager = Pager(args.no_pager, args.pager_end)

# Back up by args.lines if set, there's a(nother) race condition here
# if new entries are added while we're backing up we'll actually produce
# --lines + new entries, we could discard the 'later' lines in the general
# case, but that's unreasonable in the --follow case where I think the issue
# is more or less unfixable
# TODO: Need to be smarter about endpoints / start offsets here
if not bookmark and args.lines > 0 and args.lines != sys.maxsize:
	for j in handles.values():
		j.reverse = True
		j.seek_tail()

	MergedJournals = heapq.merge(*(handles.values()))
	n = 0
	for entry in MergedJournals:
		n += 1
		if n > args.lines:
			break

        for j in handles.values():
                j.reverse = False

# Iterate through the relevant journal entries and print them out
MergedJournals = heapq.merge(*(handles.values()))

try:
	for entry in MergedJournals:
		print entry.fmt(args.output, tz=args.timezone).encode('utf-8')
except IOError as e:
	if e.errno != errno.EPIPE:
		raise e

# TODO: There may be a race between finishing the read of the the existing journal
#       messages and setting up the poll()

#
# We use journald's native polling support to follow the journal(s) but this
# is inherently racy and with multiple journals we can't guarantee strict
# chronological ordering, especially for remote journals
#
if args.follow:
	follower = select.poll()
	followed = {}
	for j in handles.values():
		follower.register(j, j.get_events())
		followed[j.fileno()] = j
	while True:
		ready = follower.poll()
		ready = [x[0] for x in ready]
		active= []

		# We could simply process and print all journals here, which in theory
		# should slightly reduce the inherent ordering races, but it would
		# not eliminate them and is significally less efficient
		for fd in ready:
			state = followed[fd].process()
			if state != systemd.journal.NOP:
				active.append(followed[fd])
				
		MergedActiveJournals = heapq.merge(*active)
		for entry in MergedActiveJournals:
			print entry.fmt(args.output, tz=args.timezone).encode('utf-8')


# We're done close our pipe to the pager and then wait for the user to quit
pager.done()

if args.bookmark:
	bookmark = {
		'format': 'ng-journalctl-bookmark',
		'version': '1',
		'journal': {}
	}
	for j in handles.keys():
		entry = handles[j].current
		if not entry:
			# If we don't have a current entry seek to the end
			handles[j].seek_tail()
			entry = handles[j].get_previous()

                #TODO - change this to clear the filters and store a cursor of some sort. Otherwise this will re open the same journal file repeatedly
		if not entry:
			continue
		
		bookmark['journal'][j] = {
			'cursor': entry['__CURSOR']
		}
	try:
		fd = os.open(args.bookmark, os.O_RDWR|os.O_TRUNC|os.O_CREAT, 0o600)
		fo = os.fdopen(fd, 'w')
		json.dump(bookmark, fo, True, indent=4)
	except Exception, e:
		sys.stderr.write("Failed to write bookmark to file '%s'" % args.bookmark)
		pass

