# Author: Nic Wolfe <nic@wolfeden.ca>
# URL: http://code.google.com/p/sickbeard/
#
# This file is part of SickRage.
#
# SickRage is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickRage is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickRage. If not, see <http://www.gnu.org/licenses/>.
from __future__ import with_statement
import os
import re
import sys
import logging
import logging.handlers
import threading
import platform
import locale
import sickbeard
from sickbeard import classes, encodingKludge as ek
from github import Github, InputFileContent
import codecs
# log levels
ERROR = logging.ERROR
WARNING = logging.WARNING
INFO = logging.INFO
DEBUG = logging.DEBUG
DB = 5
reverseNames = {u'ERROR': ERROR,
u'WARNING': WARNING,
u'INFO': INFO,
u'DEBUG': DEBUG,
u'DB': DB}
censoredItems = {}
class NullHandler(logging.Handler):
def emit(self, record):
pass
class CensoredFormatter(logging.Formatter, object):
def __init__(self, *args, **kwargs):
super(CensoredFormatter, self).__init__(*args, **kwargs)
def format(self, record):
msg = super(CensoredFormatter, self).format(record)
for k, v in censoredItems.iteritems():
if v and len(v) > 0 and v in msg:
msg = msg.replace(v, len(v) * '*')
# Needed because Newznab apikey isn't stored as key=value in a section.
msg = re.sub(r'([&?]r|[&?]apikey|[&?]api_key)=[^&]*([&\w]?)',r'\1=**********\2', msg)
return msg
class Logger(object):
def __init__(self):
self.logger = logging.getLogger('sickrage')
self.loggers = [
logging.getLogger('sickrage'),
logging.getLogger('tornado.general'),
logging.getLogger('tornado.application'),
# logging.getLogger('tornado.access'),
]
self.consoleLogging = False
self.fileLogging = False
self.debugLogging = False
self.logFile = None
self.submitter_running = False
def initLogging(self, consoleLogging=False, fileLogging=False, debugLogging=False):
self.logFile = self.logFile or os.path.join(sickbeard.LOG_DIR, 'sickrage.log')
self.debugLogging = debugLogging
self.consoleLogging = consoleLogging
self.fileLogging = fileLogging
# add a new logging level DB
logging.addLevelName(DB, 'DB')
# nullify root logger
logging.getLogger().addHandler(NullHandler())
# set custom root logger
for logger in self.loggers:
if logger is not self.logger:
logger.root = self.logger
logger.parent = self.logger
# set minimum logging level allowed for loggers
for logger in self.loggers:
logger.setLevel(DB)
# console log handler
if self.consoleLogging:
console = logging.StreamHandler()
console.setFormatter(CensoredFormatter(u'%(asctime)s %(levelname)s::%(message)s', '%H:%M:%S'))
console.setLevel(INFO if not self.debugLogging else DEBUG)
for logger in self.loggers:
logger.addHandler(console)
# rotating log file handler
if self.fileLogging:
rfh = logging.handlers.RotatingFileHandler(self.logFile, maxBytes=sickbeard.LOG_SIZE, backupCount=sickbeard.LOG_NR, encoding='utf-8')
rfh.setFormatter(CensoredFormatter(u'%(asctime)s %(levelname)-8s %(message)s', '%Y-%m-%d %H:%M:%S'))
rfh.setLevel(DEBUG)
for logger in self.loggers:
logger.addHandler(rfh)
def shutdown(self):
logging.shutdown()
def log(self, msg, level=INFO, *args, **kwargs):
meThread = threading.currentThread().getName()
message = meThread + u" :: " + msg
# pass exception information if debugging enabled
if level == ERROR:
#Replace the SSL error with a link to information about how to fix it.
message = re.sub(r'error \[Errno 1\] _ssl.c:\d{3}: error:\d{8}:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert internal error', r'See: http://git.io/vJrkM', message)
self.logger.exception(message, *args, **kwargs)
classes.ErrorViewer.add(classes.UIError(message))
# if sickbeard.GIT_AUTOISSUES:
# self.submit_errors()
else:
self.logger.log(level, message, *args, **kwargs)
def log_error_and_exit(self, error_msg, *args, **kwargs):
self.log(error_msg, ERROR, *args, **kwargs)
if not self.consoleLogging:
sys.exit(error_msg.encode(sickbeard.SYS_ENCODING, 'xmlcharrefreplace'))
else:
sys.exit(1)
def submit_errors(self):
if not (sickbeard.GIT_USERNAME and sickbeard.GIT_PASSWORD and len(classes.ErrorViewer.errors) > 0):
self.log('Please set your GitHub username and password in the config, unable to submit issue ticket to GitHub!')
return
if self.submitter_running:
return 'RUNNING'
self.submitter_running = True
gh_org = sickbeard.GIT_ORG or 'SiCKRAGETV'
gh_repo = 'sickrage-issues'
gh = Github(login_or_token=sickbeard.GIT_USERNAME, password=sickbeard.GIT_PASSWORD, user_agent="SiCKRAGE")
try:
# read log file
log_data = None
if os.path.isfile(self.logFile):
with ek.ek(codecs.open, *[self.logFile, 'r', 'utf-8']) as f:
log_data = f.readlines()
for i in range (1 , int(sickbeard.LOG_NR)):
if os.path.isfile(self.logFile + "." + str(i)) and (len(log_data) <= 500):
with ek.ek(codecs.open, *[self.logFile + "." + str(i), 'r', 'utf-8']) as f:
log_data += f.readlines()
log_data = [line for line in reversed(log_data)]
# parse and submit errors to issue tracker
for curError in sorted(classes.ErrorViewer.errors, key=lambda error: error.time, reverse=True)[:500]:
#Skip SSL Error, we pointed them to a URL.
if re.search('http://git.io/vJrkM', curError.message):
classes.ErrorViewer.errors.remove(curError)
continue
try:
title_Error = str(curError.title)
if not len(title_Error) or title_Error == 'None':
title_Error = re.match("^[A-Z0-9\-\[\] :]+::\s*(.*)$", ek.ss(str(curError.message))).group(1)
if len(title_Error) > 1024:
title_Error = title_Error[0:1024]
except Exception as e:
self.log("Unable to get error title : " + sickbeard.exceptions.ex(e), ERROR)
gist = None
regex = "^(%s)\s+([A-Z]+)\s+([0-9A-Z\-]+)\s*(.*)$" % curError.time
for i, x in enumerate(log_data):
x = ek.ss(x)
match = re.match(regex, x)
if match:
level = match.group(2)
if reverseNames[level] == ERROR:
paste_data = "".join(log_data[i:i+50])
if paste_data:
gist = gh.get_user().create_gist(True, {"sickrage.log": InputFileContent(paste_data)})
break
else:
gist = 'No ERROR found'
message = u"### INFO\n"
message += u"Python Version: **" + sys.version[:120].replace('\n','') + "**\n"
message += u"Operating System: **" + platform.platform() + "**\n"
if not 'Windows' in platform.platform():
try:
message += u"Locale: " + locale.getdefaultlocale()[1] + "\n"
except:
message += u"Locale: unknown" + "\n"
message += u"Branch: **" + sickbeard.BRANCH + "**\n"
message += u"Commit: SiCKRAGETV/SickRage@" + sickbeard.CUR_COMMIT_HASH + "\n"
if gist and gist != 'No ERROR found':
message += u"Link to Log: " + gist.html_url + "\n"
else:
message += u"No Log available with ERRORS: " + "\n"
message += u"### ERROR\n"
message += u"```\n"
message += curError.message + "\n"
message += u"```\n"
message += u"---\n"
message += u"_STAFF NOTIFIED_: @SiCKRAGETV/owners @SiCKRAGETV/moderators"
title_Error = u"[APP SUBMITTED]: " + title_Error
reports = gh.get_organization(gh_org).get_repo(gh_repo).get_issues()
issue_found = False
issue_id = 0
for report in reports:
if title_Error == report.title:
comment = report.create_comment(message)
if comment:
issue_id = report.number
self.log('Commented on existing issue #%s successfully!' % issue_id )
issue_found = True
break
if not issue_found:
issue = gh.get_organization(gh_org).get_repo(gh_repo).create_issue(title_Error, message)
if issue:
issue_id = issue.number
self.log('Your issue ticket #%s was submitted successfully!' % issue_id )
# clear error from error list
classes.ErrorViewer.errors.remove(curError)
self.submitter_running = False
return issue_id
except Exception as e:
self.log(sickbeard.exceptions.ex(e), ERROR)
self.submitter_running = False
class Wrapper(object):
instance = Logger()
def __init__(self, wrapped):
self.wrapped = wrapped
def __getattr__(self, name):
try:
return getattr(self.wrapped, name)
except AttributeError:
return getattr(self.instance, name)
_globals = sys.modules[__name__] = Wrapper(sys.modules[__name__])