2014-04-07 16:43:39 -04:00
|
|
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
|
|
|
|
|
|
# Copyright 2014 IBM Corporation
|
2015-01-19 14:35:22 -05:00
|
|
|
# Copyright 2015 Lenovo
|
2014-04-07 16:43:39 -04:00
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
2013-08-16 16:37:19 -04:00
|
|
|
|
|
|
|
# This module contains function to write out log type data.
|
|
|
|
# In this go around, log data is explicitly kept distinct from config data
|
|
|
|
# config data almost always retrieved by a particular key value and access
|
|
|
|
# pattern is random. For logs, the access tends to be sequential.
|
|
|
|
#
|
|
|
|
# Current thought is to have a plain-text file and a secondary binary index
|
|
|
|
# file. The index would track events and time intervals and the seek() value.
|
|
|
|
# Markers would be put into the plain text, allowing utility to rebuild
|
|
|
|
# index if something happens beyond the scope of this module's code.
|
|
|
|
#
|
|
|
|
# We can contemplate how to add value as an audit log. The following
|
|
|
|
# possibilities could be explored:
|
|
|
|
# - Forward Secure Sealing (like systemd). Examine the algorithm and decide
|
|
|
|
# if it is sufficient (their implementation, for example, seems hard
|
|
|
|
# to protect against tampering as at least a few moments into the past
|
|
|
|
# can always be manipulated....
|
|
|
|
# - TPM PCRs. Understand better what PCRs may be used/extended perhaps
|
|
|
|
# per-indexed event..
|
2014-02-09 10:43:26 -05:00
|
|
|
|
|
|
|
# On the plaintext half of a log:
|
|
|
|
# Strategy is that console log shall have just the payload logged, sans
|
|
|
|
# timestamp.
|
|
|
|
# Other events (e.g. SEL or other actions) will get timestamps
|
|
|
|
# preceding '[]' to denote them. Timestamps will be in local
|
|
|
|
# time in the text output
|
|
|
|
# If a log is set to not be primarily console type data, then '[]' are not
|
|
|
|
# used, timestamp still precedes record, and records get '\n' appended
|
|
|
|
# If binary data is really called for, base64 format shall be used to
|
|
|
|
# avoid messing up text reads.
|
|
|
|
|
|
|
|
# On the binary half of a log (.jnl):
|
|
|
|
# The specific format can be whatever we decide since there is a text format.
|
|
|
|
# The information to store:
|
|
|
|
# - leading bit reserved, 0 for now
|
|
|
|
# - length of metadata record 7 bits
|
2014-03-09 20:34:39 -04:00
|
|
|
# - type of data referenced by this entry (one byte), currently:
|
|
|
|
# 0=text event, 1=json, 2=console data
|
2014-02-09 10:43:26 -05:00
|
|
|
# - offset into the text log to begin (4 bytes)
|
|
|
|
# - length of data referenced by this entry (2 bytes)
|
|
|
|
# - UTC timestamp of this entry in seconds since epoch (unsigned 32 bit?)
|
|
|
|
# - CRC32 over the record
|
|
|
|
# (a future extended version might include suport for Forward Secure Sealing
|
|
|
|
# or other fields)
|
|
|
|
|
2014-03-10 13:15:31 -04:00
|
|
|
import collections
|
2015-07-31 17:07:40 -04:00
|
|
|
import confluentd.config.configmanager
|
2014-03-09 20:34:39 -04:00
|
|
|
import eventlet
|
2014-04-18 10:36:51 -04:00
|
|
|
import json
|
|
|
|
import os
|
2014-03-09 20:34:39 -04:00
|
|
|
import struct
|
|
|
|
import time
|
2015-01-19 14:35:22 -05:00
|
|
|
import traceback
|
2014-02-09 10:43:26 -05:00
|
|
|
|
2015-07-30 17:08:52 -04:00
|
|
|
try:
|
|
|
|
from fcntl import flock, LOCK_EX, LOCK_UN, LOCK_SH
|
|
|
|
except ImportError:
|
|
|
|
flock = lambda file, flag: True
|
|
|
|
LOCK_EX = None
|
|
|
|
LOCK_UN = None
|
|
|
|
LOCK_SH = None
|
|
|
|
|
|
|
|
|
2014-02-09 10:43:26 -05:00
|
|
|
# on conserving filehandles:
|
|
|
|
# upon write, if file not open, open it for append
|
|
|
|
# upon write, schedule/reschedule closing filehandle in 15 seconds
|
|
|
|
# this way, idle log files get closed, mitigating risk of running afoul
|
|
|
|
# of uname type limts, but the filehandle stays open under load and
|
|
|
|
# buffering and such work when things are busy
|
|
|
|
# perhaps test with very low ulimit and detect shortage and
|
|
|
|
# switch to aggressive handle reclaim, tanking performance
|
|
|
|
# if that happens, warn to have user increase ulimit for optimal
|
|
|
|
# performance
|
|
|
|
|
2014-04-18 10:36:51 -04:00
|
|
|
_loggers = {}
|
2014-03-09 20:34:39 -04:00
|
|
|
|
2014-04-18 15:52:29 -04:00
|
|
|
|
2014-03-09 20:34:39 -04:00
|
|
|
class Events(object):
|
2014-04-16 15:45:05 -04:00
|
|
|
(
|
|
|
|
undefined, clearscreen, clientconnect, clientdisconnect,
|
2014-04-18 10:36:51 -04:00
|
|
|
consoledisconnect, consoleconnect, stacktrace
|
|
|
|
) = range(7)
|
2014-03-09 20:34:39 -04:00
|
|
|
logstr = {
|
|
|
|
2: 'connection by ',
|
|
|
|
3: 'disconnection by ',
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class DataTypes(object):
|
|
|
|
text, dictionary, console, event = range(4)
|
|
|
|
|
2014-04-16 15:45:05 -04:00
|
|
|
|
2014-02-09 10:43:26 -05:00
|
|
|
class Logger(object):
|
2014-03-09 20:34:39 -04:00
|
|
|
"""
|
|
|
|
:param console: If true, [] will be used to denote non-text events. If
|
|
|
|
False, events will be formatted like syslog:
|
|
|
|
date: message<CR>
|
|
|
|
"""
|
2014-04-18 10:36:51 -04:00
|
|
|
def __new__(cls, logname, console=False, tenant=None):
|
|
|
|
global _loggers
|
|
|
|
if console:
|
|
|
|
relpath = 'consoles/' + logname
|
|
|
|
else:
|
|
|
|
relpath = logname
|
|
|
|
if relpath in _loggers:
|
|
|
|
return _loggers[relpath]
|
|
|
|
else:
|
|
|
|
return object.__new__(cls)
|
|
|
|
|
|
|
|
def __init__(self, logname, console=False, tenant=None):
|
|
|
|
if hasattr(self, 'initialized'):
|
|
|
|
# we are just a copy of the same object
|
|
|
|
return
|
|
|
|
self.initialized = True
|
2015-07-31 17:07:40 -04:00
|
|
|
self.filepath = confluentd.config.configmanager.get_global("logdirectory")
|
2014-03-09 20:34:39 -04:00
|
|
|
if self.filepath is None:
|
2015-07-31 17:07:40 -04:00
|
|
|
if os.name == 'nt':
|
|
|
|
self.filepath = os.path.join(
|
|
|
|
os.getenv('SystemDrive'), '\\ProgramData', 'confluent',
|
|
|
|
'logs')
|
|
|
|
else:
|
|
|
|
self.filepath = "/var/log/confluent/"
|
2014-03-09 20:34:39 -04:00
|
|
|
self.isconsole = console
|
|
|
|
if console:
|
|
|
|
self.filepath += "consoles/"
|
2014-04-18 10:36:51 -04:00
|
|
|
if not os.path.isdir(self.filepath):
|
|
|
|
os.makedirs(self.filepath, 448)
|
2014-03-09 20:34:39 -04:00
|
|
|
self.textpath = self.filepath + logname
|
|
|
|
self.binpath = self.filepath + logname + ".cbl"
|
|
|
|
self.writer = None
|
|
|
|
self.closer = None
|
|
|
|
self.textfile = None
|
|
|
|
self.binfile = None
|
2014-03-10 13:15:31 -04:00
|
|
|
self.logentries = collections.deque()
|
2014-03-09 20:34:39 -04:00
|
|
|
|
|
|
|
def writedata(self):
|
|
|
|
if self.textfile is None:
|
|
|
|
self.textfile = open(self.textpath, mode='ab')
|
|
|
|
if self.binfile is None:
|
|
|
|
self.binfile = open(self.binpath, mode='ab')
|
2014-03-10 13:15:31 -04:00
|
|
|
while self.logentries:
|
|
|
|
entry = self.logentries.popleft()
|
2014-03-09 20:34:39 -04:00
|
|
|
ltype = entry[0]
|
|
|
|
tstamp = entry[1]
|
|
|
|
data = entry[2]
|
|
|
|
evtdata = entry[3]
|
|
|
|
textdate = ''
|
|
|
|
if self.isconsole and ltype != 2:
|
|
|
|
textdate = time.strftime(
|
|
|
|
'[%m/%d %H:%M:%S ', time.localtime(tstamp))
|
|
|
|
if ltype == DataTypes.event and evtdata in Events.logstr:
|
|
|
|
textdate += Events.logstr[evtdata]
|
|
|
|
elif not self.isconsole:
|
|
|
|
textdate = time.strftime(
|
|
|
|
'%b %d %H:%M:%S ', time.localtime(tstamp))
|
2015-07-30 17:08:52 -04:00
|
|
|
flock(self.textfile, LOCK_EX)
|
2014-03-09 20:34:39 -04:00
|
|
|
offset = self.textfile.tell() + len(textdate)
|
|
|
|
datalen = len(data)
|
2014-03-12 16:16:31 -04:00
|
|
|
eventaux = entry[4]
|
|
|
|
if eventaux is None:
|
|
|
|
eventaux = 0
|
2014-03-09 20:34:39 -04:00
|
|
|
# metadata length is always 16 for this code at the moment
|
2014-04-16 15:45:05 -04:00
|
|
|
binrecord = struct.pack(
|
|
|
|
">BBIHIBBH", 16, ltype, offset, datalen, tstamp, evtdata,
|
|
|
|
eventaux, 0)
|
2014-03-09 20:34:39 -04:00
|
|
|
if self.isconsole:
|
|
|
|
if ltype == 2:
|
|
|
|
textrecord = data
|
|
|
|
else:
|
|
|
|
textrecord = textdate + data + ']'
|
|
|
|
else:
|
2014-04-23 10:38:11 -04:00
|
|
|
textrecord = textdate + data
|
|
|
|
if not textrecord.endswith('\n'):
|
|
|
|
textrecord += '\n'
|
2014-03-09 20:34:39 -04:00
|
|
|
self.textfile.write(textrecord)
|
2015-07-30 17:08:52 -04:00
|
|
|
flock(self.textfile, LOCK_UN)
|
|
|
|
flock(self.binfile, LOCK_EX)
|
2014-03-09 20:34:39 -04:00
|
|
|
self.binfile.write(binrecord)
|
2015-07-30 17:08:52 -04:00
|
|
|
flock(self.binfile, LOCK_UN)
|
2014-03-10 13:15:31 -04:00
|
|
|
self.textfile.flush()
|
|
|
|
self.binfile.flush()
|
2014-03-09 20:34:39 -04:00
|
|
|
if self.closer is None:
|
|
|
|
self.closer = eventlet.spawn_after(15, self.closelog)
|
|
|
|
self.writer = None
|
|
|
|
|
2014-03-12 17:12:01 -04:00
|
|
|
def read_recent_text(self, size):
|
|
|
|
try:
|
|
|
|
textfile = open(self.textpath, mode='r')
|
|
|
|
binfile = open(self.binpath, mode='r')
|
|
|
|
except IOError:
|
2014-10-28 16:45:55 -04:00
|
|
|
return '', 0, 0
|
2015-07-30 17:08:52 -04:00
|
|
|
flock(binfile, LOCK_SH)
|
2014-03-12 17:12:01 -04:00
|
|
|
binfile.seek(0, 2)
|
|
|
|
binidx = binfile.tell() - 16
|
|
|
|
currsize = 0
|
2014-03-12 19:50:42 -04:00
|
|
|
offsets = []
|
2014-03-12 17:14:00 -04:00
|
|
|
termstate = None
|
2014-10-06 15:12:16 -04:00
|
|
|
recenttimestamp = 0
|
2014-03-12 17:12:01 -04:00
|
|
|
while binidx > 0 and currsize < size:
|
|
|
|
binfile.seek(binidx, 0)
|
|
|
|
binidx -= 16
|
|
|
|
recbytes = binfile.read(16)
|
|
|
|
(_, ltype, offset, datalen, tstamp, evtdata, eventaux, _) = \
|
|
|
|
struct.unpack(">BBIHIBBH", recbytes)
|
|
|
|
if ltype != 2:
|
|
|
|
continue
|
2014-10-06 15:12:16 -04:00
|
|
|
if tstamp > recenttimestamp:
|
|
|
|
recenttimestamp = tstamp
|
2014-03-12 17:12:01 -04:00
|
|
|
currsize += datalen
|
|
|
|
offsets.append((offset, datalen))
|
2014-03-12 17:14:00 -04:00
|
|
|
if termstate is None:
|
2014-03-12 19:50:42 -04:00
|
|
|
termstate = eventaux
|
2015-07-30 17:08:52 -04:00
|
|
|
flock(binfile, LOCK_UN)
|
2014-03-12 17:12:01 -04:00
|
|
|
binfile.close()
|
|
|
|
textdata = ''
|
2015-07-30 17:08:52 -04:00
|
|
|
flock(textfile, LOCK_SH)
|
2014-03-12 17:12:01 -04:00
|
|
|
while offsets:
|
2014-04-18 15:52:29 -04:00
|
|
|
(offset, length) = offsets.pop()
|
2014-03-12 17:12:01 -04:00
|
|
|
textfile.seek(offset, 0)
|
2014-04-18 15:52:29 -04:00
|
|
|
textdata += textfile.read(length)
|
2015-07-30 17:08:52 -04:00
|
|
|
flock(textfile, LOCK_UN)
|
2014-03-12 17:12:01 -04:00
|
|
|
textfile.close()
|
2014-03-18 16:30:37 -04:00
|
|
|
if termstate is None:
|
|
|
|
termstate = 0
|
2014-10-06 15:12:16 -04:00
|
|
|
return textdata, termstate, recenttimestamp
|
2014-03-12 17:12:01 -04:00
|
|
|
|
2014-04-23 10:06:31 -04:00
|
|
|
def write(self, data):
|
|
|
|
"""Write plain text to log
|
|
|
|
|
|
|
|
This is intended so that a log object may be used to replace a
|
|
|
|
normal file object with a loss of capability. For example,
|
|
|
|
sys.stdout = logobject
|
|
|
|
|
|
|
|
:param data: data to log
|
|
|
|
"""
|
2014-04-23 10:38:11 -04:00
|
|
|
if data != '\n': # 'print' likes to do '\n' by itself, skip that
|
2015-01-19 14:35:22 -05:00
|
|
|
self.log(traceback.format_stack(limit=2)[0][:-1] + ": " + data)
|
2014-04-23 10:06:31 -04:00
|
|
|
|
|
|
|
def flush(self):
|
|
|
|
pass
|
|
|
|
|
2014-03-12 16:16:31 -04:00
|
|
|
def log(self, logdata=None, ltype=None, event=0, eventdata=None):
|
2014-03-09 20:34:39 -04:00
|
|
|
if type(logdata) not in (str, unicode, dict):
|
|
|
|
raise Exception("Unsupported logdata")
|
|
|
|
if ltype is None:
|
|
|
|
if type(logdata) == dict:
|
2014-04-18 10:36:51 -04:00
|
|
|
logdata = json.dumps(logdata)
|
2014-03-09 20:34:39 -04:00
|
|
|
ltype = 1
|
|
|
|
elif self.isconsole:
|
|
|
|
ltype = 2
|
|
|
|
else:
|
|
|
|
ltype = 0
|
|
|
|
if self.closer is not None:
|
|
|
|
self.closer.cancel()
|
|
|
|
self.closer = None
|
|
|
|
timestamp = int(time.time())
|
|
|
|
if (len(self.logentries) > 0 and ltype == 2 and
|
|
|
|
event == 0 and self.logentries[-1][0] == 2 and
|
|
|
|
self.logentries[-1][1] == timestamp):
|
|
|
|
self.logentries[-1][2] += logdata
|
2014-03-12 16:16:31 -04:00
|
|
|
if eventdata is not None:
|
|
|
|
self.logentries[-1][4] = eventdata
|
2014-03-09 20:34:39 -04:00
|
|
|
else:
|
2014-04-16 15:45:05 -04:00
|
|
|
self.logentries.append(
|
|
|
|
[ltype, timestamp, logdata, event, eventdata])
|
2014-03-09 20:34:39 -04:00
|
|
|
if self.writer is None:
|
|
|
|
self.writer = eventlet.spawn_after(2, self.writedata)
|
|
|
|
|
|
|
|
def closelog(self):
|
|
|
|
self.textfile.close()
|
|
|
|
self.binfile.close()
|
|
|
|
self.textfile = None
|
|
|
|
self.binfile = None
|
|
|
|
self.closer = None
|