Made channels log to a log file by default. Added a default /history switch to the channel commands (for viewing past entries) and implemented a very efficient tail_log_file function to scan the log and display it.

This commit is contained in:
Griatch 2016-04-07 22:57:55 +02:00
parent d00ff9ae32
commit 5583a8d758
3 changed files with 124 additions and 54 deletions

View file

@ -27,6 +27,7 @@ from builtins import object
from evennia.comms.models import ChannelDB from evennia.comms.models import ChannelDB
from evennia.commands import cmdset, command from evennia.commands import cmdset, command
from evennia.utils.logger import tail_log_file
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -34,11 +35,21 @@ class ChannelCommand(command.Command):
""" """
{channelkey} channel {channelkey} channel
{channeldesc}
Usage: Usage:
{lower_channelkey} <message> {lower_channelkey} <message>
{lower_channelkey} history <num>[-<num>] {lower_channelkey}/history [start]
Switch:
history: View the 20 last messages, optionally
beginning <start> messages from the end.
Example:
{lower_channelkey} Hello World!
{lower_channelkey}/history
{lower_channelkey}/history 30
{channeldesc}
""" """
# this flag is what identifies this cmd as a channel cmd # this flag is what identifies this cmd as a channel cmd
# and branches off to the system send-to-channel command # and branches off to the system send-to-channel command
@ -54,6 +65,13 @@ class ChannelCommand(command.Command):
""" """
# cmdhandler sends channame:msg here. # cmdhandler sends channame:msg here.
channelname, msg = self.args.split(":", 1) channelname, msg = self.args.split(":", 1)
self.history_start = None
if msg.startswith("/history"):
arg = msg[8:]
try:
self.history_start = int(arg) if arg else 0
except ValueError:
pass
self.args = (channelname.strip(), msg.strip()) self.args = (channelname.strip(), msg.strip())
def func(self): def func(self):
@ -79,7 +97,13 @@ class ChannelCommand(command.Command):
string = _("You are not permitted to send to channel '%s'.") string = _("You are not permitted to send to channel '%s'.")
self.msg(string % channelkey) self.msg(string % channelkey)
return return
channel.msg(msg, senders=self.caller, online=True) if self.history_start is not None:
# Try to view history
log_file = channel.attributes.get("log_file", default="channel_%s.log" % channel.key)
self.msg("".join(line.split("[-]", 1)[1] if "[-]" in line else line
for line in tail_log_file(log_file, self.history_start, 20)))
else:
channel.msg(msg, senders=self.caller, online=True)
def get_extra_info(self, caller, **kwargs): def get_extra_info(self, caller, **kwargs):
""" """
@ -175,7 +199,7 @@ class ChannelHandler(object):
locks="cmd:all();%s" % channel.locks, locks="cmd:all();%s" % channel.locks,
help_category="Channel names", help_category="Channel names",
obj=channel, obj=channel,
arg_regex=r"\s.*?", arg_regex=r"\s.*?|/history.*?",
is_channel=True) is_channel=True)
# format the help entry # format the help entry
key = channel.key key = channel.key

View file

@ -27,7 +27,7 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
""" """
self.at_channel_creation() self.at_channel_creation()
self.attributes.add("log_file", "channel_%s.log" % self.key)
if hasattr(self, "_createdict"): if hasattr(self, "_createdict"):
# this is only set if the channel was created # this is only set if the channel was created
# with the utils.create.create_channel function. # with the utils.create.create_channel function.
@ -37,8 +37,6 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
self.db_key = "#i" % self.dbid self.db_key = "#i" % self.dbid
elif cdict["key"] and self.key != cdict["key"]: elif cdict["key"] and self.key != cdict["key"]:
self.key = cdict["key"] self.key = cdict["key"]
if cdict.get("keep_log"):
self.db_keep_log = cdict["keep_log"]
if cdict.get("aliases"): if cdict.get("aliases"):
self.aliases.add(cdict["aliases"]) self.aliases.add(cdict["aliases"])
if cdict.get("locks"): if cdict.get("locks"):
@ -183,14 +181,18 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
msg.message = body msg.message = body
return msg return msg
def distribute_message(self, msg, online=False): def distribute_message(self, msgobj, online=False):
""" """
Method for grabbing all listeners that a message should be Method for grabbing all listeners that a message should be
sent to on this channel, and sending them a message. sent to on this channel, and sending them a message.
msg (str): Message to distribute. Args:
online (bool): Only send to receivers who are actually online msgobj (Msg or TempMsg): Message to distribute.
(not currently used): online (bool): Only send to receivers who are actually online
(not currently used):
Notes:
This is also where logging happens, if enabled.
""" """
# get all players connected to this channel and send to them # get all players connected to this channel and send to them
@ -198,12 +200,16 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
try: try:
# note our addition of the from_channel keyword here. This could be checked # note our addition of the from_channel keyword here. This could be checked
# by a custom player.msg() to treat channel-receives differently. # by a custom player.msg() to treat channel-receives differently.
entity.msg(msg.message, from_obj=msg.senders, options={"from_channel":self.id}) entity.msg(msgobj.message, from_obj=msgobj.senders, options={"from_channel":self.id})
except AttributeError as e: except AttributeError as e:
logger.log_trace("%s\nCannot send msg to '%s'." % (e, entity)) logger.log_trace("%s\nCannot send msg to '%s'." % (e, entity))
if msgobj.keep_log:
# log to file
logger.log_file(msgobj.message, self.attributes.get("log_file") or "channel_%s.log" % self.key)
def msg(self, msgobj, header=None, senders=None, sender_strings=None, def msg(self, msgobj, header=None, senders=None, sender_strings=None,
persistent=False, online=False, emit=False, external=False): keep_log=None, online=False, emit=False, external=False):
""" """
Send the given message to all players connected to channel. Note that Send the given message to all players connected to channel. Note that
no permission-checking is done here; it is assumed to have been no permission-checking is done here; it is assumed to have been
@ -222,9 +228,11 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
sender_strings (list, optional): Name strings of senders. Used for external sender_strings (list, optional): Name strings of senders. Used for external
connections where the sender is not a player or object. connections where the sender is not a player or object.
When this is defined, external will be assumed. When this is defined, external will be assumed.
persistent (bool, optional): Ignored if msgobj is a Msg or TempMsg. keep_log (bool or None, optional): This allows to temporarily change the logging status of
If True, a Msg will be created, using header and senders this channel message. If `None`, the Channel's `keep_log` Attribute will
keywords. If False, other keywords will be ignored. be used. If `True` or `False`, that logging status will be used for this
message only (note that for unlogged channels, a `True` value here will
create a new log file only for this message).
online (bool, optional) - If this is set true, only messages people who are online (bool, optional) - If this is set true, only messages people who are
online. Otherwise, messages all players connected. This can online. Otherwise, messages all players connected. This can
make things faster, but may not trigger listeners on players make things faster, but may not trigger listeners on players
@ -239,25 +247,14 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
successful, `False` otherwise. successful, `False` otherwise.
""" """
if senders: senders = make_iter(senders) if senders else []
senders = make_iter(senders)
else:
senders = []
if isinstance(msgobj, basestring): if isinstance(msgobj, basestring):
# given msgobj is a string # given msgobj is a string - convert to msgobject (always TempMsg)
msg = msgobj msgobj = TempMsg(senders=senders, header=header, message=msgobj, channels=[self])
if persistent and self.db.keep_log: # we store the logging setting for use in distribute_message()
msgobj = Msg() msgobj.keep_log = keep_log if keep_log is not None else self.db.keep_log
msgobj.save()
else:
# Use TempMsg, so this message is not stored.
msgobj = TempMsg()
msgobj.header = header
msgobj.message = msg
msgobj.channels = [self] # add this channel
if not msgobj.senders: # start the sending
msgobj.senders = senders
msgobj = self.pre_send_message(msgobj) msgobj = self.pre_send_message(msgobj)
if not msgobj: if not msgobj:
return False return False

View file

@ -152,7 +152,32 @@ log_depmsg = log_dep
# Arbitrary file logger # Arbitrary file logger
LOG_FILE_HANDLES = {} # holds open log handles _LOG_FILE_HANDLES = {} # holds open log handles
def _open_log_file(filename):
"""
Helper to open the log file (always in the log dir) and cache its
handle. Will create a new file in the log dir if one didn't
exist.
"""
global _LOG_FILE_HANDLES, _LOGDIR
if not _LOGDIR:
from django.conf import settings
_LOGDIR = settings.LOG_DIR
filename = os.path.join(_LOGDIR, filename)
if filename in _LOG_FILE_HANDLES:
# cache the handle
return _LOG_FILE_HANDLES[filename]
else:
try:
filehandle = open(filename, "a+") # append mode + reading
_LOG_FILE_HANDLES[filename] = filehandle
return filehandle
except IOError:
log_trace()
return None
def log_file(msg, filename="game.log"): def log_file(msg, filename="game.log"):
""" """
@ -164,17 +189,9 @@ def log_file(msg, filename="game.log"):
on new lines following datetime info. on new lines following datetime info.
""" """
global LOG_FILE_HANDLES, _LOGDIR, _TIMEZONE
if not _LOGDIR:
from django.conf import settings
_LOGDIR = settings.LOG_DIR
if not _TIMEZONE:
from django.utils import timezone as _TIMEZONE
def callback(filehandle, msg): def callback(filehandle, msg):
"Writing to file and flushing result" "Writing to file and flushing result"
msg = "\n%s [-] %s" % (_TIMEZONE.now(), msg.strip()) msg = "\n%s [-] %s" % (timeformat(), msg.strip())
filehandle.write(msg) filehandle.write(msg)
# since we don't close the handle, we need to flush # since we don't close the handle, we need to flush
# manually or log file won't be written to until the # manually or log file won't be written to until the
@ -186,15 +203,47 @@ def log_file(msg, filename="game.log"):
log_trace() log_trace()
# save to server/logs/ directory # save to server/logs/ directory
filename = os.path.join(_LOGDIR, filename) filehandle = _open_log_file(filename)
if filehandle:
deferToThread(callback, filehandle, msg).addErrback(errback)
def tail_log_file(filename, offset, nlines):
"""
Return the tail of the log file.
Args:
filename (str): The name of the log file, presumed to be in
the Evennia log dir.
offset (int): The line offset *from the end of the file* to start
reading from. 0 means to start at the latest entry.
nlines (int): How many lines to return, counting backwards
from the offset. If file is shorter, will get all lines.
Returns:
lines (list): The nline entries from the end of the file, or
all if the file is shorter than nlines.
"""
lines_found = []
filehandle = _open_log_file(filename)
if filehandle:
# step backwards in chunks and stop only when we have enough lines
buffer_size = 4098
block_count = -1
while len(lines_found) < (offset + nlines):
try:
# scan backwards in file, starting from the end
filehandle.seek(block_count * buffer_size, os.SEEK_END)
except IOError:
# file too small for this seek, take what we've got
filehandle.seek(0)
lines_found = filehandle.readlines()
break
lines_found = filehandle.readlines()
block_count -= 1
# return the right number of lines
return lines_found[-nlines-offset:-offset if offset else None]
if filename in LOG_FILE_HANDLES:
filehandle = LOG_FILE_HANDLES[filename]
else:
try:
filehandle = open(filename, "a")
LOG_FILE_HANDLES[filename] = filehandle
except IOError:
log_trace()
return
deferToThread(callback, filehandle, msg).addErrback(errback)