# -*- Mode: Python; tab-width: 4 -*-

import asynchat
import socket
import string
import time         # these three are for the rotating logger
import os           # |
import stat         # v

#
# three types of log:
# 1) file
#    with optional flushing.  Also, one that rotates the log.
# 2) socket
#    dump output directly to a socket connection. [how do we
#    keep it open?]
# 3) syslog
#    log to syslog via tcp.  this is a per-line protocol.
#

#
# The 'standard' interface to a logging object is simply
# log_object.log (message)
#

# a file-like object that captures output, and
# makes sure to flush it always...  this could
# be connected to:
#  o	stdio file
#  o	low-level file
#  o	socket channel
#  o	syslog output...

class file_logger:
			
	# pass this either a path or a file object.
	def __init__ (self, file, flush=1, mode='a'):
		if type(file) == type(''):
			if (file == '-'):
				import sys
				self.file = sys.stdout
			else:
				self.file = open (file, mode)
		else:
			self.file = file
		self.do_flush = flush

	def __repr__ (self):
		return '<file logger: %s>' % self.file

	def write (self, data):
		self.file.write (data)
		self.maybe_flush()
		
	def writeline (self, line):
		self.file.writeline (line)
		self.maybe_flush()
		
	def writelines (self, lines):
		self.file.writelines (lines)
		self.maybe_flush()

	def maybe_flush (self):
		if self.do_flush:
			self.file.flush()

	def flush (self):
		self.file.flush()

	def softspace (self, *args):
		pass

	def log (self, message):
		if message[-1] not in ('\r', '\n'):
			self.write (message + '\n')
		else:
			self.write (message)

# like a file_logger, but it must be attached to a filename.
# When the log gets too full, or a certain time has passed,
# it backs up the log and starts a new one.  Note that backing
# up the log is done via "mv" because anything else (cp, gzip)
# would take time, during which medusa would do nothing else.

class rotating_file_logger (file_logger):
			
	# If freq is non-None we back up "daily", "weekly", or "monthly".
	# Else if maxsize is non-None we back up whenever the log gets
	# to big.  If both are None we never back up.
	def __init__ (self, file, freq=None, maxsize=None, flush=1, mode='a'):
		self.filename = file
		self.mode = mode
		self.file = open (file, mode)
		self.freq = freq
		self.maxsize = maxsize
		self.rotate_when = self.next_backup(self.freq)
		self.do_flush = flush

	def __repr__ (self):
		return '<rotating-file logger: %s>' % self.file

	# We back up at midnight every 1) day, 2) monday, or 3) 1st of month
	def next_backup (self, freq):
		(yr, mo, day, hr, min, sec, wd, jday, dst) = time.localtime(time.time())
		if freq == 'daily':
			return time.mktime(yr,mo,day+1, 0,0,0, 0,0,-1)
		elif freq == 'weekly':
			return time.mktime(yr,mo,day-wd+7, 0,0,0, 0,0,-1)  # wd(monday)==0
		elif freq == 'monthly':
			return time.mktime(yr,mo+1,1, 0,0,0, 0,0,-1)
		else:
			return None                  # not a date-based backup

	def maybe_flush (self):              # rotate first if necessary
		self.maybe_rotate()
		if self.do_flush:                # from file_logger()
			self.file.flush()

	def maybe_rotate (self):
		if self.freq and time.time() > self.rotate_when:
			self.rotate()
			self.rotate_when = self.next_backup(self.freq)
		elif self.maxsize:               # rotate when we get too big
			try:
				if os.stat(self.filename)[stat.ST_SIZE] > self.maxsize:
					self.rotate()
 			except os.error:             # file not found, probably
				self.rotate()            # will create a new file

	def rotate (self):
		(yr, mo, day, hr, min, sec, wd, jday, dst) = time.localtime(time.time())
		try:
			self.file.close()
			newname = '%s.ends%04d%02d%02d' % (self.filename, yr, mo, day)
			try:
				open(newname, "r").close()      # check if file exists
				newname = newname + "-%02d%02d%02d" % (hr, min, sec)
			except:                             # YEARMODY is unique
				pass
			os.rename(self.filename, newname)
			self.file = open(self.filename, self.mode)
		except:
			pass

# syslog is a line-oriented log protocol - this class would be
# appropriate for FTP or HTTP logs, but not for dumping stderr to.

# TODO: a simple safety wrapper that will ensure that the line sent
# to syslog is reasonable.

# TODO: async version of syslog_client: now, log entries use blocking
# send()

import m_syslog
syslog_logger = m_syslog.syslog_client

class syslog_logger (m_syslog.syslog_client):
	def __init__ (self, address, facility='user'):
		m_syslog.syslog_client.__init__ (self, address)
		self.facility = m_syslog.facility_names[facility]
		self.address=address

	def __repr__ (self):
		return '<syslog logger address=%s>' % (repr(self.address))

	def log (self, message):
		m_syslog.syslog_client.log (
			self,
			message,
			facility=self.facility,
			priority=m_syslog.LOG_INFO
			)

# log to a stream socket, asynchronously

class socket_logger (asynchat.async_chat):

	def __init__ (self, address):

		if type(address) == type(''):
			self.create_socket (socket.AF_UNIX, socket.SOCK_STREAM)
		else:
			self.create_socket (socket.AF_INET, socket.SOCK_STREAM)

		self.connect (address)
		self.address = address
		
	def __repr__ (self):
		return '<socket logger: address=%s>' % (self.address)

	def log (self, message):
		if message[-2:] != '\r\n':
			self.socket.push (message + '\r\n')
		else:
			self.socket.push (message)

# log to multiple places
class multi_logger:
	def __init__ (self, loggers):
		self.loggers = loggers

	def __repr__ (self):
		return '<multi logger: %s>' % (repr(self.loggers))

	def log (self, message):
		for logger in self.loggers:
			logger.log (message)

class resolving_logger:
	"""Feed (ip, message) combinations into this logger to get a
	resolved hostname in front of the message.  The message will not
	be logged until the PTR request finishes (or fails)."""

	def __init__ (self, resolver, logger):
		self.resolver = resolver
		self.logger = logger

	class logger_thunk:
		def __init__ (self, message, logger):
			self.message = message
			self.logger = logger

		def __call__ (self, host, ttl, answer):
			if not answer:
				answer = host
			self.logger.log ('%s:%s' % (answer, self.message))

	def log (self, ip, message):
		self.resolver.resolve_ptr (
			ip,
			self.logger_thunk (
				message,
				self.logger
				)
			)

class unresolving_logger:
	"Just in case you don't want to resolve"
	def __init__ (self, logger):
		self.logger = logger

	def log (self, ip, message):
		self.logger.log ('%s:%s' % (ip, message))


def strip_eol (line):
	while line and line[-1] in '\r\n':
		line = line[:-1]
	return line

class tail_logger:
	"Keep track of the last <size> log messages"
	def __init__ (self, logger, size=500):
		self.size = size
		self.logger = logger
		self.messages = []

	def log (self, message):
		self.messages.append (strip_eol (message))
		if len (self.messages) > self.size:
			del self.messages[0]
		self.logger.log (message)
