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:
parent
d00ff9ae32
commit
5583a8d758
3 changed files with 124 additions and 54 deletions
|
|
@ -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,6 +97,12 @@ 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
|
||||||
|
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)
|
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
|
||||||
|
|
|
||||||
|
|
@ -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,27 +181,35 @@ 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:
|
||||||
|
msgobj (Msg or TempMsg): Message to distribute.
|
||||||
online (bool): Only send to receivers who are actually online
|
online (bool): Only send to receivers who are actually online
|
||||||
(not currently used):
|
(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
|
||||||
for entity in self.subscriptions.all():
|
for entity in self.subscriptions.all():
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
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)
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue