Package rrlog :: Package server
[hide private]
[frames] | no frames]

Source Code for Package rrlog.server

  1  #Copyright (c) 2007 
  2  #        Ruben Reifenberg, Germany, 07381. 
  3  #    All rights reserved. 
  4  # 
  5  #Redistribution and use in source and binary forms, with or without 
  6  #modification, are permitted provided that the following conditions 
  7  #are met: 
  8  #1. Redistributions of source code must retain the above copyright 
  9  #   notice, this list of conditions and the following disclaimer as 
 10  #   the first lines of this file unmodified. 
 11  #2. Redistributions in binary form must reproduce the above copyright 
 12  #   notice, this list of conditions and the following disclaimer in the 
 13  #   documentation and/or other materials provided with the distribution. 
 14  # 
 15  # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 
 16  # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 17  # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 18  # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 
 19  # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
 20  # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 
 21  # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 22  # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
 23  # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 
 24  # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
 25  # SUCH DAMAGE. 
 26   
 27  """ 
 28  Log Server, 
 29  Part of the log which can (optionally) be in a separate process. 
 30  @author: Ruben Reifenberg 
 31  """ 
 32   
 33  from sys import stderr 
 34  from datetime import datetime 
 35  from rrlog.tool import mStrftime,ListRotator,traceToShortStr 
 36   
 37  EMPTYDICT = {} 
 38   
 39   
40 -class ColumnConfigurationMismatch(Exception):
41 """ 42 A configured column name is impossible because already reserved. 43 """ 44 pass
45
46 -class ObserverAlreadyAdded(Exception):
47 """ 48 Attempt to add an observer twice into the observer list 49 """ 50 pass
51 52 53 54
55 -class MsgJob(object):
56 """ 57 @ivar clientid: int, id of the client that caused the message. Unique in the server. 58 @ivar msgid: int, id of the message. Unique in the client. 59 @ivar msg: str, msg as created by the client. 60 @ivar ts: str, timestamp 61 @ivar special: dict with custom items (see "special" argument of the log method) 62 @ivar tblen: len of the client traceback when the log method was called 63 @ivar path: client traceback path as sequence of (filename, linenumber). [0] is the latest (where the log call happened) 64 """
65 - def __init__(self,pid,clientid,msgid,msg,ts,special,cat,path,tblen,cfunc,formatter):
66 """ 67 @param special: data for custom observers only 68 @param ts: timestamp, str 69 """ 70 self.pid = pid 71 self.clientid = clientid 72 self.msgid = msgid 73 self.msg = msg 74 self.ts = ts 75 if special is None: self.special = EMPTYDICT 76 else: self.special = special 77 self.cat = cat 78 self.path = path 79 self.tblen = tblen 80 self._formatter = formatter 81 self.cfunc = cfunc 82 self._iCfn = None # index of cfn in path (first non-None element) 83 for i,(cfn,cln) in enumerate(path): 84 if cfn is not None: 85 self._iCfn = i 86 break
87
88 - def cfn(self):
89 """ 90 @rtype: str 91 Callers File Name 92 """ 93 if self._iCfn is not None: 94 return self._formatter.format_fname(self.path[self._iCfn][0]) 95 else: 96 return None
97
98 - def cln(self):
99 """ 100 @rtype: int 101 Callers Line Number 102 """ 103 if self._iCfn is not None: 104 return self.path[self._iCfn][1] 105 else: 106 return -1
107
108 - def getFormattedDict(self):
109 """ 110 DEPRECATED: use format_dict of the DBLogWriter 111 @rtype: dict 112 @return: Dict with already formatted data 113 """ 114 import warnings 115 warnings.warn("use format_dict argument of DBLogWriter.__init__",DeprecationWarning) 116 # self.__dict__ wouldn't have the formatted stuff like cfn 117 res = dict( 118 pid=self.pid, 119 clientid = self.clientid, 120 msgid = self.msgid, 121 msg = self.msg, 122 cat = self.cat, 123 ts = self.ts, 124 cfunc=self.cfunc, 125 cfn = self.cfn(), 126 cln = self.cln(), 127 path = self.pathStr(0), # 0 since v0.1.5. Was 1 with <=v0.1.4 to omit the cfn/cln in the path string. 128 ) 129 res.update(self.special) 130 return res
131 132
133 - def pathStr(self, imin=0):
134 """ 135 @param imin: min.index of path to use. 136 Fo example, imin==1 will skip the first path item 137 (Note: the first item is also available separately as cfn,cln) 138 @rtype: str 139 @return: path as formatted str 140 """ 141 return self._formatter.pathAsStr(self.path, imin=imin)
142
143 - def copy_update(self, kwargs):
144 """ 145 @return: new instance with my init kwargs but updated with the given kwargs 146 """ 147 oldkwargs = { 148 "pid":self.pid, 149 "clientid":self.clientid, 150 "msgid":self.msgid, 151 "msg":self.msg, 152 "ts":self.ts, 153 "special":self.special, 154 "cat":self.cat, 155 "path":self.path, 156 "tblen":self.tblen, 157 "cfunc":self.cfunc, 158 "formatter":self._formatter, 159 } 160 oldkwargs.update(kwargs) 161 return self.__class__(**oldkwargs)
162 163 164 165 166
167 -class RotateWriterFactory(object):
168 - def __init__(self, configs, writerFactory):
169 assert len(configs)>0 170 self._configs = ListRotator(configs) 171 self._writerFactory = writerFactory
172 - def nextWriter(self, history):
173 """ 174 RENAMED from next(), otherwise Python3 conversion failure. 175 @param history: list of old writers to be maintained, oldest at [0] 176 Invalid (expired) writers are removed! 177 """ 178 if len(history) >= self._configs.len(): 179 history.pop(0) 180 return self._writerFactory(self._configs.next())
181 182
183 -class RotateLogWriter(object):
184 """ 185 Assigned to a list of LogWriters, 186 rotates by creating a new one each time 187 when a line count is exceeded. 188 Maintains a history of old writers. 189 @ivar writers: History of writers, current at [-1], oldest at [0]. 190 Only writers with an existing (i.e.not already overwritten) table/file are available. 191 (Migration note: This is analogous to the getWriteHistory() of version 0.1.1 but the order is ascending.) 192 """
193 - def __init__(self, getNextWriter, rotateLineMin):
194 """ 195 @param getNextWriter: Callable that takes a list (history) of previous writers, and returns the next logWriter to use 196 @param rotateLineMin: rotate when ~ lines are written 197 """ 198 self._rotateLineMin = rotateLineMin 199 self._nextWriter = getNextWriter, 200 self.writers = [] 201 self._rotate()
202 203
204 - def _rotate(self):
205 """ 206 Not threadsafe. 207 Maintains the history (self.writers). 208 A new writer is appended.. 209 removes the oldest writer [0] if self._writer count is longer than self._configs. 210 """ 211 writer = self._nextWriter[0](history=self.writers) 212 self.writers.append(writer)
213 214 215
216 - def writeNow(self, job):
217 """ 218 Write without buffering, return when written 219 """ 220 if (self._rotateLineMin is not None) and (self.writers[-1].estimateLineCount() >= self._rotateLineMin): 221 self._rotate() 222 self.writers[-1].writeNow(job)
223 224 225 226 227 228 229
230 -class LogServer(object):
231 """ 232 This can (but must not be) in the application process. 233 @cvar CAT_INTERNALERROR: Default "I". This is used as "cat" when I write a log message about a logging failure (if possible) 234 """ 235 CLIENTID_MAX = 32767 236 CFN_SHORT = 1 # Caller File Names Minimal-Length 237 CFN_FULL = 2 # Caller File Names with Full path 238 CAT_INTERNALERROR = "I" 239
240 - def __init__(self, 241 writer, 242 filters = None, 243 observers = None, 244 cfnMode=1, 245 tsFormat=None, 246 initMsg=False, 247 jobhistSize=100, 248 oie=True, 249 ):
250 """ 251 @param cfnMode: One of the MODE...constants. 252 CFN_SHORT: Caller File Names minimal,i.e.file name without directory path(=Default) 253 CFN_FULL: Caller File Names with full file path) 254 Default is CFN_SHORT 255 @param tsFormat: strftime-format string with an extension: use %3N for milliseconds. 256 [Year..second are: %Y/%y,%m,%d,%H,%M,%S] 257 None for no timestamps. 258 For convenience: 259 The String "std1" is interpreted as "%H:%M.%S;%3N". 260 The String "std2" is interpreted as "%m/%d %H:%M.%S". 261 @attention: The logged time stamps can be significantly later than 262 the log() call of your application, because time is taken by the LogServer 263 which may be remote connected. 264 @param observers: list of callables, called with args: jobhist, writer. 265 Called each time after(!) a message was written. 266 writer is the specific writer (depends on DB/file/Stdout modus.) 267 jobhist are the N last message-jobs, with the latests at [-1]. The size N of the jobhist 268 is limited by jobhistSize. Increase jobhistSize if your observer needs to see a large job history. 269 The observers are processed in the given order. 270 You can modify job content for following observers, without affecting the log (since the observers are called after 271 the line is written.) 272 For compatibility: An observer can also have an observe() method. If available, this is used (instead of __call__) 273 @param filters: list of callable objects, analogous to observers. 274 But filters are called before logging. When they modify the message, the change gets visible in the log. 275 @param initMsg: if True, write a short and witless initial message to test the LogWriter 276 @param jobhistSize: The count of recent messages that are available as a list (these may be read by observers). Default=100 277 @raise AssertionError: if an observer is >1 times in the list 278 @raise AssertionError: if a filter is >1 times in the list 279 @param oie: Observe Internal Error; defines how to handle errors occurring in the observers and filters. 280 If True, in case of an internal error in an observer/filter, all observers/filters receive an additional message describing the error. 281 If False, the observers/filters are not notified about the internal error (but the internal error still appears in the main log.) 282 """ 283 if filters is None: 284 filters=[] 285 else: 286 assert hasattr(filters,"__iter__"),"filters must be a sequence, not %s"%(type(filters)) 287 filters = list(filters) # need the count method 288 289 if observers is None: 290 observers=[] 291 else: 292 assert hasattr(observers,"__iter__"),"observers must be a sequence, not %s"%(type(observers)) 293 observers = list(observers) # need count method and eventually want to append 294 295 296 # BEGIN compatibility with v <= 0.1.4: Accept observers with observe() instead __call__(), too: 297 self._observers = [] 298 for i,x in enumerate(observers): 299 if hasattr(x,"observe") and callable(x.observe): 300 self._observers.append(x.observe) 301 else: 302 assert callable(x),"Observer %s must be callable (or need to have the deprecated observe() method)."%(x) 303 self._observers.append(x) 304 # END compatibility with v <= 0.1.4 305 306 for x in observers: 307 assert observers.count(x)==1, "%s is >1 times in observers list"%(x) 308 309 for i,x in enumerate(filters): 310 assert callable(x),"Filter %s must be callable."%(x) 311 assert filters.count(x)==1, "%s is >1 times in filters list"%(x) 312 313 314 self._time0 = datetime.now() 315 self._observers = list(self._observers) # allow to append 316 self._filters = filters 317 self._tsFormat = { 318 "std1": "%H:%M.%S;%3N", 319 "std2": "%m/%d %H:%M.%S", 320 }.get(tsFormat, tsFormat) 321 self._cfnMode = cfnMode 322 self._writer = writer 323 self._jobhist = [] 324 assert jobhistSize>0, "need at least a history size of 1, not %s"%(jobhistSize) 325 self._jobhistSize = jobhistSize 326 self.oie = oie 327 if initMsg: 328 self._writer.writeNow(MsgJob( 329 clientid=0, 330 msgid=0, 331 cat="", 332 special=None, 333 msg="Generated by rrlog.Log", 334 path=(("LOG",-1),), 335 tblen=0, 336 ts = self._timeStr(self._time0), 337 formatter=self, 338 )) 339 self._lastClientId = 0
340
341 - def addObserver(self, observer):
342 """ 343 appends the observer at the end of the observers list 344 """ 345 if observer in self._observers: 346 raise ObserverAlreadyAdded("already added: %s"%(observer)) 347 348 if hasattr(observer,"observe") and callable(observer.observe): 349 self._observers.append(observer.observe) 350 else: 351 assert callable(observer),"Observer %s must be callable (or need to have the deprecated observe() method)."%(observer) 352 self._observers.append(observer)
353
354 - def format_fname(self,name): # 0.2.1: renamed from cfnFormatted
355 """ 356 file name in the configured format (cfnMode). 357 @return: String,men-Readable Callers File Name (None is name was None) 358 @param name:File file name incl. path, or None 359 """ 360 if name is None: return None 361 if (self._cfnMode == self.CFN_SHORT): 362 """configured to use minimum name:use file name without directory path """ 363 res = (name.split("/"))[-1] 364 #Cut away ".py" if possible, to shorten the name 365 if len(res) > 3: 366 if res[-3:]==".py": 367 res=res[:-3] 368 else: 369 assert self._cfnMode == self.CFN_FULL, "unknown value for cfn mode:%s"%(self._cfnMode) 370 """full name:use full path as name""" 371 res = name.replace("/","_") 372 res = res.replace(".","-") 373 return res
374 375
376 - def pathAsStr(self, path, imin=0):
377 """ 378 @param path: iterable 379 @param imin: min.index of path to use. 380 @return: call path, formatted as str, Empty str if path is empty 381 @rtype: str 382 """ 383 res = "" 384 lastWasOmitted = False 385 for i,(cfn,cln) in enumerate(path): 386 if i >= imin: 387 if i==imin: 388 res = "|" 389 separator = "" 390 else: 391 separator = "<-" 392 if cfn is not None: 393 # file name and line number are available 394 res += "%s%s(%s)"%(separator,self.format_fname(cfn),cln) 395 lastWasOmitted = False 396 else: 397 # a file name to omit 398 if not lastWasOmitted: # avoid multiple "<-..." 399 res += "<-..." 400 lastWasOmitted = True 401 return res
402 403 404
405 - def addClient(self):
406 """ 407 Not thread safe 408 @return: unique client id, >=1. Numbers start with 1 again if CLIENTID_MAX is exceeded. 409 """ 410 if self._lastClientId == self.CLIENTID_MAX: 411 self._lastClientId = 0 412 self._lastClientId +=1 413 return self._lastClientId
414
415 - def _timeStr(self, dt):
416 """ 417 @rtype: str 418 @return: readable time info, based on my _tsFormat 419 """ 420 if self._tsFormat is None: 421 return "" 422 else: 423 return mStrftime(dt,self._tsFormat)
424 425
426 - def log(self, pid, clientid, msgid, msg, special, cat, path, tblen, cfunc):
427 """ 428 @param msg: ASCII, the log message 429 @param cat: An application-specific category string of length 1. 430 Suggesting convention is "E" for error,"W" for Warning, "I" for internal error... 431 None for no category. 432 @param pid: clients os - process id 433 @param clientid: int, the id that the calling client obtained with registration. 434 @param msgid: int, recommended to be unique for this client (I do not check if unique.) 435 @param path: list, len>=0, each item = (int cfn,str cln) 436 @param cfunc: str,callers function name (additionally to cfn,cln) or None 437 """ 438 kwargs = {"pid":pid,"clientid":clientid,"msgid":msgid,"msg":msg,"special":special,"cat":cat,"path":path,"tblen":tblen,"cfunc":cfunc, 439 "formatter":self,"ts":self._timeStr(datetime.now())} 440 if len(self._jobhist) >= self._jobhistSize: 441 # gain a marginal relieving of the GC: 442 # re-use the popped job from history 443 job = self._jobhist.pop(0) 444 job.__init__(**kwargs) 445 else: 446 job = MsgJob(**kwargs) 447 self.logJob(job, self.oie)
448
449 - def logJob(self, job, oie):
450 # maintain jobhist queue with current job at [-1]: 451 self._jobhist.append(job) 452 453 for i,filter_ in enumerate(self._filters): 454 try: 455 filter_(jobhist=self._jobhist, writer=self._writer) 456 except Exception,e: 457 errorjob = job.copy_update({ 458 "clientid":job.clientid, 459 "msgid":-1, #mark as internal error 460 "msg": "[filter#%d(%s) failure.msgid:%d,raised:%s:::%s]"%(i,filter_,job.msgid,e,traceToShortStr() ), 461 "cat":self.CAT_INTERNALERROR, 462 "ts":"*"+job.ts, 463 }) 464 if oie: 465 # do both log the problem and let it be observed/filtered 466 self.logJob(errorjob,False) # oie False now ensures recursion will end 467 else: 468 try: 469 # log the problem but don't let it be observed/filtered 470 self._writer.writeNow(errorjob) 471 except Exception,e: 472 stderr.write("Error while reporting another error:%s,%s"%(e,traceToShortStr())) 473 474 475 self._writer.writeNow(job) 476 477 for i,observer in enumerate(self._observers): 478 try: 479 observer(jobhist=self._jobhist, writer=self._writer) 480 except Exception,e: 481 errorjob = job.copy_update({ 482 "clientid":job.clientid, 483 "msgid":-1, #mark as internal error 484 "msg": "[observer#%d(%s) failure.msgid:%d,raised:%s:::%s]"%(i,observer,job.msgid,e,traceToShortStr(8)), 485 "cat":self.CAT_INTERNALERROR, 486 "ts":"*"+job.ts, 487 }) 488 if oie: 489 # do both log the problem and let it be observed/filtered 490 self.logJob(errorjob,False) 491 else: 492 try: 493 # log the problem but don't let it be observed/filtered 494 self._writer.writeNow(errorjob) 495 except Exception,e: 496 stderr.write("Error while reporting another error:%s,%s"%(e,traceToShortStr(8)))
497