summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRich Megginson <rmeggins@redhat.com>2009-11-24 12:16:23 -0700
committerRich Megginson <rmeggins@redhat.com>2009-11-25 11:10:10 -0700
commit16e255be98bbb8b3bcfb080c632add7f048cfd44 (patch)
tree5c28a326b327c95ffb38ca2f9fcce19b5c684dea
parentb2e2a3f5294707e1ccf2b25fd281ce3653dac819 (diff)
downloadds-16e255be98bbb8b3bcfb080c632add7f048cfd44.tar.gz
ds-16e255be98bbb8b3bcfb080c632add7f048cfd44.tar.xz
ds-16e255be98bbb8b3bcfb080c632add7f048cfd44.zip
Add Named Pipe Log Script, plugins, man page
The Named Pipe Log Script allows you to replace a log file with a named pipe attached to a script. The server can then send the log output to a script instead of to a log file. This allows you to do many different things such as: * log only certain events e.g. failed binds, connections from certain ip addresses, etc. * log only lines that match a certain pattern * log only the last N lines - useful for enabling full error log debug levels in production environments * send an email or other notification when a certain event is detected The script is written in python, and allows plugins. By default, the script will log the last N lines (default 1000). There are two plugins provided - one to log only failed bind attempts, and one that will log only lines that match given regular expressions. Reviewed by: nkinder (Thanks!) - found a bug in a comment Resolves: bug 486171 Bug Description: [RFE] Access log - Failed binds
-rw-r--r--Makefile.am8
-rw-r--r--Makefile.in8
-rw-r--r--ldap/admin/src/scripts/ds-logpipe.py313
-rw-r--r--ldap/admin/src/scripts/failedbinds.py169
-rw-r--r--ldap/admin/src/scripts/logregex.py36
-rw-r--r--man/man1/ds-logpipe.py.1100
6 files changed, 630 insertions, 4 deletions
diff --git a/Makefile.am b/Makefile.am
index 95eec142..dbab95ed 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -199,7 +199,9 @@ config_DATA = $(srcdir)/lib/ldaputil/certmap.conf \
# with the default schema e.g. there is
# considerable overlap of 60changelog.ldif and 01common.ldif
# and 60inetmail.ldif and 50ns-mail.ldif among others
-sampledata_DATA = $(srcdir)/ldap/ldif/Ace.ldif \
+sampledata_DATA = ldap/admin/src/scripts/failedbinds.py \
+ ldap/admin/src/scripts/logregex.py \
+ $(srcdir)/ldap/ldif/Ace.ldif \
$(srcdir)/ldap/ldif/European.ldif \
$(srcdir)/ldap/ldif/Eurosuffix.ldif \
$(srcdir)/ldap/ldif/Example.ldif \
@@ -291,7 +293,8 @@ bin_SCRIPTS = ldap/servers/slapd/tools/rsearch/scripts/dbgen.pl \
wrappers/cl-dump \
ldap/admin/src/scripts/cl-dump.pl \
wrappers/repl-monitor \
- ldap/admin/src/scripts/repl-monitor.pl
+ ldap/admin/src/scripts/repl-monitor.pl \
+ ldap/admin/src/scripts/ds-logpipe.py
# SCRIPTS makes them executables - these are perl modules
# and should not be marked as executable - so use DATA
@@ -369,6 +372,7 @@ mib_DATA = ldap/servers/snmp/RFC-1215.txt \
dist_man_MANS = man/man1/dbscan.1 \
man/man1/cl-dump.1 \
man/man1/dbgen.pl.1 \
+ man/man1/ds-logpipe.py.1 \
man/man1/dsktune.1 \
man/man1/infadd.1 \
man/man1/ldap-agent.1 \
diff --git a/Makefile.in b/Makefile.in
index c2d51822..c00fd8f2 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -1247,7 +1247,9 @@ config_DATA = $(srcdir)/lib/ldaputil/certmap.conf \
# with the default schema e.g. there is
# considerable overlap of 60changelog.ldif and 01common.ldif
# and 60inetmail.ldif and 50ns-mail.ldif among others
-sampledata_DATA = $(srcdir)/ldap/ldif/Ace.ldif \
+sampledata_DATA = ldap/admin/src/scripts/failedbinds.py \
+ ldap/admin/src/scripts/logregex.py \
+ $(srcdir)/ldap/ldif/Ace.ldif \
$(srcdir)/ldap/ldif/European.ldif \
$(srcdir)/ldap/ldif/Eurosuffix.ldif \
$(srcdir)/ldap/ldif/Example.ldif \
@@ -1339,7 +1341,8 @@ bin_SCRIPTS = ldap/servers/slapd/tools/rsearch/scripts/dbgen.pl \
wrappers/cl-dump \
ldap/admin/src/scripts/cl-dump.pl \
wrappers/repl-monitor \
- ldap/admin/src/scripts/repl-monitor.pl
+ ldap/admin/src/scripts/repl-monitor.pl \
+ ldap/admin/src/scripts/ds-logpipe.py
# SCRIPTS makes them executables - these are perl modules
@@ -1418,6 +1421,7 @@ mib_DATA = ldap/servers/snmp/RFC-1215.txt \
dist_man_MANS = man/man1/dbscan.1 \
man/man1/cl-dump.1 \
man/man1/dbgen.pl.1 \
+ man/man1/ds-logpipe.py.1 \
man/man1/dsktune.1 \
man/man1/infadd.1 \
man/man1/ldap-agent.1 \
diff --git a/ldap/admin/src/scripts/ds-logpipe.py b/ldap/admin/src/scripts/ds-logpipe.py
new file mode 100644
index 00000000..5c605570
--- /dev/null
+++ b/ldap/admin/src/scripts/ds-logpipe.py
@@ -0,0 +1,313 @@
+#!/usr/bin/env python
+
+import sys
+import os, os.path
+import errno
+import signal
+import pprint
+import types
+import time
+import fcntl
+import pwd
+
+maxlines = 1000 # set on command line
+S_IFIFO = 0010000
+
+buffer = [] # default circular buffer used by default plugin
+totallines = 0
+logfname = "" # name of log pipe
+
+# default plugin just keeps a circular buffer
+def defaultplugin(line):
+ global totallines
+ buffer.append(line)
+ totallines = totallines + 1
+ if len(buffer) > maxlines:
+ del buffer[0]
+ return True
+
+def printbuffer():
+ sys.stdout.writelines(buffer)
+ print "Read %d total lines" % totallines
+ print logfname, "=" * 60
+
+def defaultpost(): printbuffer()
+
+plgfuncs = [] # list of plugin functions
+plgpostfuncs = [] # list of post plugin funcs
+
+def finish():
+ for postfunc in plgpostfuncs: postfunc()
+ sys.exit(0)
+
+def sighandler(signum, frame):
+ if signum != signal.SIGHUP: finish()
+ else: printbuffer()
+
+def isvalidpluginfile(plg):
+ return os.path.isfile(plg)
+
+def my_import(plgfile):
+ '''import plgfile as a python module and return
+ an error string if error - also return the prefunc if any'''
+ if not isvalidpluginfile(plgfile):
+ return ("%s is not a valid plugin filename" % plgfile, None, None)
+ # __import__ searches for the file in sys.path - we cannot
+ # __import__ a file by the full path
+ # __import__('basename') looks for basename.py in sys.path
+ (dir, fname) = os.path.split(plgfile)
+ base = os.path.splitext(fname)[0]
+ if not dir: dir = "."
+ sys.path.insert(0, dir) # put our path first so it will find our file
+ mod = __import__(base) # will throw exception if problem with python file
+ sys.path.pop(0) # remove our path
+
+ # check for the plugin functions
+ plgfunc = getattr(mod, 'plugin', None)
+ if not plgfunc:
+ return ('%s does not specify a plugin function' % plgfile, None, base)
+ if not isinstance(plgfunc, types.FunctionType):
+ return ('the symbol "plugin" in %s is not a function' % plgfile, None, base)
+ plgfuncs.append(plgfunc) # add to list in cmd line order
+
+ # check for 'post' func
+ plgpostfunc = getattr(mod, 'post', None)
+ if plgpostfunc:
+ if not isinstance(plgpostfunc, types.FunctionType):
+ return ('the symbol "post" in %s is not a function' % plgfile, None, base)
+ else:
+ plgpostfuncs.append(plgpostfunc) # add to list in cmd line order
+
+ prefunc = getattr(mod, 'pre', None)
+ # check for 'pre' func
+ if prefunc and not isinstance(prefunc, types.FunctionType):
+ return ('the symbol "pre" in %s is not a function' % plgfile, None, base)
+
+ return ('', prefunc, base)
+
+def parse_plugins(parser, options, args):
+ '''Each plugin in the plugins list may have additional
+ arguments, specified on the command line like this:
+ --plugin=foo.py foo.bar=1 foo.baz=2 ...
+ that is, each argument to plugin X will be specified as X.arg=value'''
+ if not options.plugins: return args
+
+ for plgfile in options.plugins:
+ (errstr, prefunc, base) = my_import(plgfile)
+ if errstr:
+ parser.error(errstr)
+ return args
+
+ # parse the arguments to the plugin given on the command line
+ bvals = {} # holds plugin args and values, if any
+ newargs = []
+ for arg in args:
+ if arg.startswith(base + '.'):
+ argval = arg.replace(base + '.', '')
+ (plgarg, plgval) = argval.split('=', 1) # split at first =
+ if not plgarg in bvals:
+ bvals[plgarg] = plgval
+ elif isinstance(bvals[plgarg],list):
+ bvals[plgarg].append(plgval)
+ else: # convert to list
+ bvals[plgarg] = [bvals[plgarg], plgval]
+ else:
+ newargs.append(arg)
+ if prefunc:
+ print 'Calling "pre" function in', plgfile
+ if not prefunc(bvals):
+ parser.error('the "pre" function in %s returned an error' % plgfile)
+ args = newargs
+
+ return args
+
+def open_pipe(logfname):
+ opencompleted = False
+ logf = None
+ while not opencompleted:
+ try:
+ logf = open(logfname, 'r') # blocks until there is some input
+ # set pipe to non-blocking
+# oldflags = fcntl.fcntl(logf, fcntl.F_GETFL)
+# fcntl.fcntl(logf, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
+ opencompleted = True
+ except IOError, e:
+ if e.errno == errno.EINTR:
+ continue # open was interrupted, try again
+ else: # hard error
+ raise Exception, "%s [%d]" % (e.strerror, e.errno)
+ return logf
+
+def get_server_pid(serverpidfile, servertimeout):
+ endtime = int(time.time()) + servertimeout
+ serverpid = 0
+ if serverpidfile:
+ line = None
+ try:
+ spfd = open(serverpidfile, 'r')
+ line = spfd.readline()
+ spfd.close()
+ except IOError, e:
+ if e.errno != errno.ENOENT: # may not exist yet - that's ok
+ raise Exception, "%s [%d]" % (e.strerror, e.errno)
+ if line:
+ serverpid = int(line)
+ return serverpid
+
+def is_server_alive(serverpid):
+ retval = False
+ try:
+ os.kill(serverpid, 0) # sig 0 is a "ping"
+ retval = True
+ except OSError, e:
+ pass # no such process, or EPERM/EACCES
+ return retval
+
+def read_and_process_line(logf, plgfuncs):
+ line = None
+ done = False
+ readcompleted = False
+ while not readcompleted:
+ try:
+ line = logf.readline()
+ readcompleted = True # read completed
+ except IOError, e:
+ if e.errno == errno.EINTR:
+ continue # read was interrupted, try again
+ else: # hard error
+ raise Exception, "%s [%d]" % (e.strerror, e.errno)
+ if line: # read something
+ for plgfunc in plgfuncs:
+ if not plgfunc(line):
+ print "Aborting processing due to function %s" % str(plgfunc)
+ finish()
+ done = True
+ break
+ else: # EOF
+ done = True
+ return done
+
+def parse_options():
+ from optparse import OptionParser
+ usage = "%prog <name of pipe> [options]"
+ parser = OptionParser(usage)
+ parser.add_option("-m", "--maxlines", dest="maxlines", type='int',
+ help="maximum number of lines to keep in the buffer", default=1000)
+ parser.add_option("-d", "--debug", dest="debug", action="store_true",
+ default=False, help="gather extra debugging information")
+ parser.add_option("-p", "--plugin", type='string', dest='plugins', action='append',
+ help='filename of a plugin to use with this log')
+ parser.add_option("-s", "--serverpidfile", type='string', dest='serverpidfile',
+ help='name of file containing the pid of the server to monitor')
+ parser.add_option("-t", "--servertimeout", dest="servertimeout", type='int',
+ help="timeout in seconds to wait for the serverpid to be alive", default=60)
+ parser.add_option("--serverpid", dest="serverpid", type='int',
+ help="process id of server to monitor", default=0)
+ parser.add_option("-u", "--user", type='string', dest='user',
+ help='name of user to set effective uid to')
+
+ options, args = parser.parse_args()
+
+ args = parse_plugins(parser, options, args)
+
+ if len(args) < 1:
+ parser.error("You must specify the name of the pipe to use")
+ if len(args) > 1:
+ parser.error("error - unhandled command line arguments: %s" % args.join(' '))
+
+ return options, args[0]
+
+options, logfname = parse_options()
+
+if len(plgfuncs) == 0:
+ plgfuncs.append(defaultplugin)
+if len(plgpostfuncs) == 0:
+ plgpostfuncs.append(defaultpost)
+
+if options.user:
+ try: userid = int(options.user)
+ except ValueError: # not a numeric userid - look it up
+ userid = pwd.getpwnam(options.user)[2]
+ os.seteuid(userid)
+
+serverpid = options.serverpid
+if serverpid:
+ if not is_server_alive(serverpid):
+ print "Server pid [%d] is not alive - exiting" % serverpid
+ sys.exit(1)
+
+try:
+ if os.stat(logfname).st_mode & S_IFIFO:
+ print "Using existing log pipe", logfname
+ else:
+ print "Error:", logfname, "exists and is not a log pipe"
+ print "use a filename other than", logfname
+ sys.exit(1)
+except OSError, e:
+ if e.errno == errno.ENOENT:
+ print "Creating log pipe", logfname
+ os.mkfifo(logfname)
+ os.chmod(logfname, 0600)
+ else:
+ raise Exception, "%s [%d]" % (e.strerror, e.errno)
+
+print "Listening to log pipe", logfname, "number of lines", maxlines
+
+# set up our signal handlers
+signal.signal(signal.SIGHUP, sighandler)
+signal.signal(signal.SIGINT, sighandler)
+#signal.signal(signal.SIGPIPE, sighandler)
+signal.signal(signal.SIGTERM, sighandler)
+signal.signal(signal.SIGALRM, sighandler)
+
+if options.serverpidfile:
+ # start the timer to wait for the pid file to be available
+ signal.alarm(options.servertimeout)
+
+done = False
+while not done:
+ # open the pipe - will hang until
+ # 1. something opens the other end
+ # 2. alarm goes off
+ logf = open_pipe(logfname)
+
+ if serverpid:
+ if not is_server_alive(serverpid):
+ print "Server pid [%d] is not alive - exiting" % serverpid
+ sys.exit(1)
+ else: # cancel the timer
+ signal.alarm(0) # cancel timer - got pid
+
+ innerdone = False
+ while not innerdone and not done:
+ # read and process the next line in the pipe
+ # if server exits while we are reading, we will get
+ # EOF and innerdone will be True
+ # we may have to read some number of lines until
+ # we can get the pid file
+ innerdone = read_and_process_line(logf, plgfuncs)
+ if not serverpid and options.serverpidfile:
+ serverpid = get_server_pid(options.serverpidfile, options.servertimeout)
+ if serverpid:
+ signal.alarm(0) # cancel timer - got pid
+
+ if logf:
+ logf.close()
+ logf = None
+
+ if not done:
+ if serverpid:
+ if not is_server_alive(serverpid):
+ print "Server pid [%d] is not alive - exiting" % serverpid
+ sys.exit(1)
+ else: # the server will sometimes close the log and reopen it
+ # however, at shutdown, the server will close the log before
+ # the process has exited - so is_server_alive will return
+ # true for a short time - if we then attempt to open the
+ # pipe, the open will hang forever - to avoid this situation
+ # we set the alarm again to wake up the open
+ signal.alarm(options.servertimeout)
+
+ print "log pipe", logfname, "closed - reopening"
+
+finish()
diff --git a/ldap/admin/src/scripts/failedbinds.py b/ldap/admin/src/scripts/failedbinds.py
new file mode 100644
index 00000000..8afe0ff1
--- /dev/null
+++ b/ldap/admin/src/scripts/failedbinds.py
@@ -0,0 +1,169 @@
+import re
+import os, os.path
+
+# regex that matches a BIND request line
+regex_num = r'[-]?\d+' # matches numbers including negative
+regex_new_conn = re.compile(r'^(\[.+\]) (conn=%s) (fd=%s) (slot=%s) (?:SSL )?connection from (\S+)' % (regex_num, regex_num, regex_num))
+regex_sslinfo = re.compile(r'^\[.+\] (conn=%s) SSL (.+)$' % regex_num)
+regex_bind_req = re.compile(r'^(\[.+\]) (conn=%s) (op=%s) BIND dn=(.+) method=(\S+) version=\d ?(?:mech=(\S+))?' % (regex_num, regex_num))
+regex_bind_res = re.compile(r'^(\[.+\]) (conn=%s) (op=%s) RESULT err=(%s) tag=97 ' % (regex_num, regex_num, regex_num))
+regex_unbind = re.compile(r'^\[.+\] (conn=%s) op=%s UNBIND' % (regex_num, regex_num))
+regex_closed = re.compile(r'^(\[.+\]) (conn=%s) (op=%s) fd=%s closed' % (regex_num, regex_num, regex_num))
+regex_ssl_map_fail = re.compile(r'^\[.+\] (conn=%s) (SSL failed to map client certificate.*)$' % regex_num)
+
+# bind errors we can ignore
+ignore_errors = {'0': 'Success',
+ '10': 'Referral',
+ '14': 'SASL Bind In Progress'
+ }
+
+REQ = 0
+RES = 1
+
+class Conn:
+ def __init__(self, timestamp, conn, fd, slot, ip):
+ self.conn = conn
+ self.fd = fd
+ self.slot = slot
+ self.ip = ip
+ self.timestamp = timestamp
+ self.ops = {}
+ self.sslinfo = ''
+
+ def addssl(self, sslinfo):
+ if self.sslinfo and sslinfo:
+ self.sslinfo += ' '
+ self.sslinfo += sslinfo
+
+ def addreq(self, timestamp, opnum, dn, method, mech='SIMPLE'):
+ retval = None
+ if opnum in self.ops: # result came before request?
+ op = self.ops.pop(opnum) # grab the op and remove from list
+ if op[RES]['errnum'] in ignore_errors: # don't care about this op
+ return retval
+ if not mech: mech = "SIMPLE"
+ op[REQ] = {'dn': dn, 'method': method, 'timestamp': timestamp,
+ 'mech': mech}
+ retval = self.logstr(opnum, op)
+ else: # store request until we get the result
+ op = [None, None] # new empty list
+ if not mech: mech = "SIMPLE"
+ op[REQ] = {'dn': dn, 'method': method, 'timestamp': timestamp,
+ 'mech': mech}
+ self.ops[opnum] = op
+ return retval
+
+ def addres(self, timestamp, opnum, errnum):
+ retval = None
+ if opnum in self.ops:
+ op = self.ops.pop(opnum) # grab the op and remove from list
+ if errnum in ignore_errors: # don't care about this op
+ return retval
+ op[RES] = {'errnum': errnum, 'timestamp': timestamp}
+ retval = self.logstr(opnum, op)
+ else: # result came before request in access log - store until we find request
+ op = [None, None] # new empty list
+ op[RES] = {'errnum': errnum, 'timestamp': timestamp}
+ self.ops[opnum] = op
+ return retval
+
+ def logstr(self, opnum, op):
+ # timestamp connnum opnum err=X request timestamp dn=Y method=Z mech=W timestamp ip=ip
+ logstr = '%s %s %s err=%s REQUEST %s dn=%s method=%s mech=%s %s ip=%s extra=%s' % (
+ op[RES]['timestamp'], self.conn, opnum, op[RES]['errnum'],
+ op[REQ]['timestamp'], op[REQ]['dn'], op[REQ]['method'], op[REQ]['mech'],
+ self.timestamp, self.ip, self.sslinfo
+ )
+ return logstr
+
+# key is conn=X
+# val is ops hash
+# key is op=Y
+# value is list
+# list[0] is BIND request
+# list[1] is RESULT
+conns = {}
+
+# file to log failed binds to
+logf = None
+
+def pre(plgargs):
+ global logf
+ logfile = plgargs.get('logfile', None)
+ if not logfile:
+ print "Error: missing required argument failedbinds.logfile"
+ return False
+ needchmod = False
+ if not os.path.isfile(logfile): needchmod = True
+ logf = open(logfile, 'a', 0) # 0 for unbuffered output
+ if needchmod: os.chmod(logfile, 0600)
+ return True
+
+def post():
+ global logf
+ logf.close()
+ logf = None
+
+def plugin(line):
+ # is this a new conn line?
+ match = regex_new_conn.match(line)
+ if match:
+ (timestamp, connid, fdid, slotid, ip) = match.groups()
+ if connid in conns: conns.pop(connid) # remove old one, if any
+ conn = Conn(timestamp, connid, fdid, slotid, ip)
+ conns[connid] = conn
+ return True
+
+ # is this an UNBIND line?
+ match = regex_unbind.match(line)
+ if match:
+ connid = match.group(1)
+ if connid in conns: conns.pop(connid) # remove it
+ return True
+
+ # is this a closed line?
+ match = regex_closed.match(line)
+ if match:
+ (timestamp, connid, opid) = match.groups()
+ if connid in conns: conns.pop(connid) # remove it
+ return True
+
+ # is this an SSL info line?
+ match = regex_sslinfo.match(line)
+ if match:
+ (connid, sslinfo) = match.groups()
+ if connid in conns:
+ conns[connid].addssl(sslinfo)
+ return True
+
+ # is this a line with extra SSL mapping info?
+ match = regex_ssl_map_fail.match(line)
+ if match:
+ (connid, sslinfo) = match.groups()
+ if connid in conns:
+ conns[connid].addssl(sslinfo)
+ return True
+
+ # is this a REQUEST line?
+ match = regex_bind_req.match(line)
+ if match:
+ (timestamp, connid, opnum, dn, method, mech) = match.groups()
+ # should have seen new conn line - if not, have to create a dummy one
+ conn = conns.get(connid, Conn('unknown', connid, '', '', 'unknown'))
+ logmsg = conn.addreq(timestamp, opnum, dn, method, mech)
+ if logmsg:
+ logf.write(logmsg + "\n")
+ return True
+
+ # is this a RESULT line?
+ match = regex_bind_res.match(line)
+ if match:
+ (timestamp, connid, opnum, errnum) = match.groups()
+ # should have seen new conn line - if not, have to create a dummy one
+ conn = conns.get(connid, Conn('unknown', connid, '', '', 'unknown'))
+ logmsg = conn.addres(timestamp, opnum, errnum)
+ if logmsg:
+ logf.write(logmsg + "\n")
+ return True
+
+ return True # no match
diff --git a/ldap/admin/src/scripts/logregex.py b/ldap/admin/src/scripts/logregex.py
new file mode 100644
index 00000000..ac329227
--- /dev/null
+++ b/ldap/admin/src/scripts/logregex.py
@@ -0,0 +1,36 @@
+import sys
+import re
+import __main__ # to use globals
+
+# supports more than one regex - multiple regex are combined using AND logic
+# OR logic is easily supported with the '|' regex modifier
+regex_regex_ary = []
+buffer = []
+
+def pre(plgargs):
+ global regex_regex_ary
+ regexary = plgargs.get('regex', None)
+ if not regexary:
+ print "Error: missing required argument logregex.regex"
+ return False
+ if isinstance(regexary,list):
+ regex_regex_ary = [re.compile(xx) for xx in regexary]
+ else:
+ regex_regex_ary.append(re.compile(regexary))
+ return True
+
+def post():
+ global buffer
+ sys.stdout.writelines(buffer)
+ buffer = []
+
+def plugin(line):
+ global buffer
+ for rx in regex_regex_ary:
+ if not rx.search(line):
+ break # must match all regex
+ else: # all regexes matched
+ buffer.append(line)
+ if len(buffer) > __main__.maxlines:
+ del buffer[0]
+ return True
diff --git a/man/man1/ds-logpipe.py.1 b/man/man1/ds-logpipe.py.1
new file mode 100644
index 00000000..53093b2d
--- /dev/null
+++ b/man/man1/ds-logpipe.py.1
@@ -0,0 +1,100 @@
+.\" Hey, EMACS: -*- nroff -*-
+.\" First parameter, NAME, should be all caps
+.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
+.\" other parameters are allowed: see man(7), man(1)
+.TH DS-LOGPIPE.PY 1 "November 24, 2009"
+.\" Please adjust this date whenever revising the manpage.
+.\"
+.\" Some roff macros, for reference:
+.\" .nh disable hyphenation
+.\" .hy enable hyphenation
+.\" .ad l left justify
+.\" .ad b justify to both left and right margins
+.\" .nf disable filling
+.\" .fi enable filling
+.\" .br insert line break
+.\" .sp <n> insert n+1 empty lines
+.\" for manpage-specific macros, see man(7)
+.SH NAME
+ds-logpipe.py \- Create and read from a named pipe instead of a log file
+.SH SYNOPSIS
+.B ds\-logpipe.py
+/full/path/to/namedpipe
+ [\fI-m maxlinestobuffer\fR] [\fI-u userid\fR] [\fI-s serverpidfile\fR] [\fI-t servertimeout\fR] [\fI--plugin=/path/to/pluginfile.py\fR] [\fIpluginfile.arg=value\fR]
+
+.PP
+.SH DESCRIPTION
+The Named Pipe Log Script allows you to replace a log file with a named pipe attached to a script. The server can then send the log output to a script instead of to a log file. This allows you to do many different things such as:
+
+ * log only certain events e.g. failed binds, connections from certain ip addresses, etc.
+ * log only lines that match a certain pattern
+ * log only the last N lines - useful for enabling full error log debug levels in production environments
+ * send an email or other notification when a certain event is detected
+
+The script is written in python, and allows plugins. By default, the script will log the last N lines (default 1000). There are two plugins provided - one to log only failed bind attempts, and one that will log only lines that match given regular expressions.
+.PP
+.\" TeX users may be more comfortable with the \fB<whatever>\fP and
+.\" \fI<whatever>\fP escape sequences to invode bold face and italics,
+.\" respectively.
+.SH OPTIONS
+A summary of options is included below.
+.TP
+.B /full/path/to/namedpipe
+Required - full path and file name of the named pipe. If this does not exist, it will be created. If it exists and is a named pipe, the script will use it. If it exists and is not a pipe, the script will abort. The ownership will be the same as the user running the script (or see the -u option below).
+.TP
+.B \-m|\-\-maxlines=N
+Number of lines to buffer - default is 1000
+.TP
+.B \-u|\-\-userid=user
+The pipe and any other files created by the script will be chown()'d to this userid. This may be a string userid name or a numeric userid value.
+.TP
+.B \-s|\-\-serverpidfile=/path/to/servername.pid
+If you want the script to exit when a particular directory server exists, specify the full path to the file containing the server pid. The default is usually something like /var/run/dirsrv/slapd-<instancename>.pid where <instancename> is usually the hostname
+.TP
+.B \-t|\-\-servertimeout=N
+Since the serverpidfile may not exist yet when the script is run, the script will wait by default 60 seconds for the pid file to exist and the server to be started. Use this option to specify a different timeout.
+.TP
+.B \-\-serverpid=P
+IF the server you want to track is already running, you can specify it using this argument. If the specified pid is not valid, the script will abort.
+.TP
+.B \-p|\-\-plugin=/full/path/to/pluginname.py
+Specify a plugin to use. The plugin must be a python file and must end in \fI.py\fR. It must specify a function called \fIplugin\fR and may specify functions called \fIpre\fR and \fIpost\fR.
+.TP
+.B pluginname.arg1=value ... pluginname.argN=value
+You can specify arguments to plugins on the command line. If there is a plugin specified as --plugin=/full/path/to/pluginname.py, the arguments for that plugin are specified as \fIpluginname.argname=value\fR. The script parses these arguments and passes them to the plugin \fIpre\fR function as a python dict. IF there is more than one argument named \fIpluginname.argname\fR the values are passed as a python list.
+.SH DIRECTORY SERVER NOTES
+The directory server will usually need to be configured to log to the named pipe instead of the usual log file. For example, use the following LDIF to tell the server to use the file \fIaccess.pipe\fR for the access log:
+ dn: cn=config
+ changetype: modify
+ replace: nsslapd-accesslog-maxlogsperdir
+ nsslapd-accesslog-maxlogsperdir: 1
+ -
+ replace: nsslapd-accesslog-logexpirationtime
+ nsslapd-accesslog-logexpirationtime: -1
+ -
+ replace: nsslapd-accesslog-logrotationtime
+ nsslapd-accesslog-logrotationtime: -1
+ -
+ replace: nsslapd-accesslog
+ nsslapd-accesslog: /var/log/dirsrv/slapd-localhost/access.pipe
+ -
+ replace: nsslapd-accesslog-logbuffering
+ nsslapd-accesslog-logbuffering: off
+
+NOTE: Before doing this, you should save your current configuration so you can restore it later.
+ ldapsearch ... -s base -b "cn=config" nsslapd-accesslog-maxlogsperdir nsslapd-accesslog-logexpirationtime \
+ nsslapd-accesslog-logrotationtime nsslapd-accesslog nsslapd-accesslog > savedaccesslog.ldif
+
+The error log and audit log have similarly named configuration attributes e.g. nsslapd-errorlog, nsslapd-auditlog. Note that the audit log is disabled by default - use nsslapd-auditlog-logging-enabled: on to enable it.
+.br
+.SH AUTHOR
+ds-logpipe.py was written by the 389 Project.
+.SH "REPORTING BUGS"
+Report bugs to http://bugzilla.redhat.com.
+.SH COPYRIGHT
+Copyright \(co 2009 Red Hat, Inc.
+.br
+This is free software. You may redistribute copies of it under the terms of
+the Directory Server license found in the LICENSE file of this
+software distribution. This license is essentially the GNU General Public
+License version 2 with an exception for plug-in distribution.