Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge most recent version from my master branch #3

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea/
*.pyc
9 changes: 9 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,12 @@ Usage
raven-harakiri <uwsgi_log_string_with_harakiri> --dsn=<your_sentry_dsn>

First argument (or stdin from other process, if piped) should be text with python tracebacker log.

Here's an example of a typical usage of --watch for use under
[supervisord](http://supervisord.org/).

[program:raven-harakiri]
user=user
command=/path/to/a/virtual/env/bin/raven-harakiri --watch /tmp/uwsgi.log --thread-regex 'uWSGIWorker.*'
redirect_stderr=true
autorestart=true
125 changes: 96 additions & 29 deletions raven_harakiri.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
:license: BSD, see LICENSE for more details.
"""
from __future__ import print_function
import subprocess
import fileinput
import logging
import re
import sys
from optparse import OptionParser
from raven import get_version, os, Client
from raven.utils import json
from raven.utils.six import StringIO
from six import StringIO
from raven.utils.stacks import get_lines_from_file


Expand All @@ -28,7 +29,7 @@ def store_json(option, opt_str, value, parser):
setattr(parser.values, option.dest, value)


def convert_traceback(uwsgi_traceback):
def convert_traceback(uwsgi_traceback, thread_regex=None):
""" Convert uwsgi traceback string with following pattern to raven protocol traceback
thread_id = %s
filename = %s
Expand All @@ -39,7 +40,11 @@ def convert_traceback(uwsgi_traceback):
More info: http://sentry.readthedocs.org/en/latest/developer/client/#building-the-json-packet
http://uwsgi-docs.readthedocs.org/en/latest/Tracebacker.html

thread_id = MainThread filename = /home/belonika/.pythonz/pythons/CPython-2.6.8/lib/python2.6/socket.py lineno = 554 function = create_connection line = sock.connect(sa)
thread_id = MainThread
filename = /home/belonika/.pythonz/pythons/CPython-2.6.8/lib/python2.6/socket.py
lineno = 554
function = create_connection
line = sock.connect(sa)
"""
variables = ('thread_id', 'filename', 'lineno', 'function', 'line')
regexp = r' '.join(('%s = (?P<%s>.+?)' % (var, var) for var in variables))
Expand All @@ -48,6 +53,9 @@ def convert_traceback(uwsgi_traceback):
match = re.match(r'^%s$' % regexp, line)
values = match.groupdict() if match else None
if values:
if thread_regex and\
not re.match(thread_regex, values['thread_id']):
continue
frame_result = {
'abs_path': values['filename'],
'context_line': values['line'],
Expand All @@ -60,10 +68,12 @@ def convert_traceback(uwsgi_traceback):
'pre_context': [],
'vars': {}
}
pre_context, context_line, post_context = get_lines_from_file(frame_result['abs_path'], frame_result['lineno'], 5)
pre_context, context_line, post_context = get_lines_from_file(frame_result['abs_path'],
frame_result['lineno'] - 1, 5)
if context_line is not None:
frame_result.update({
'pre_context': pre_context,
'context_line': context_line,
'post_context': post_context,
})

Expand All @@ -72,32 +82,44 @@ def convert_traceback(uwsgi_traceback):
return traceback


def send_message(client, options, traceback):
print("Client configuration:")
for k in ('servers', 'project', 'public_key', 'secret_key'):
print(' %-15s: %s' % (k, getattr(client, k)))
print()
def extract_http(log):
for line in log.split('\n'):
match = re.match(
r'^[^-]+- HARAKIRI \[core (?P<core>.+)\] (?P<remote_addr>.+) - '
r'(?P<method>.+) (?P<url>.+) since (?P<begin_time>.+)$', line)
if match:
break
if match:
values = match.groupdict()
return values['method'], values['url'], values['remote_addr']
else:
return None

if not all([client.servers, client.project, client.public_key, client.secret_key]):
print("Error: All values must be set!")
sys.exit(1)

def send_message(client, options, log):
if not client.is_enabled():
print('Error: Client reports as being disabled!')
sys.exit(1)

http_info = extract_http(log)

data = {
'logger': 'uwsgi.harakiri',
'sentry.interfaces.Stacktrace': {
'frames': convert_traceback(traceback)
'stacktrace': {
'frames': convert_traceback(log, options.get('thread_regex'))
},
'sentry.interfaces.Http': {
'method': 'GET',
'url': 'http://example.com',
}
}

print('Sending a message...',)
if http_info:
method, url, remote_addr = http_info
data['request'] = {
'method': method,
'url': url,
'env': {
'REMOTE_ADDR': remote_addr
}
}

ident = client.get_ident(client.captureMessage(
message='uWSGI harakiri',
data=data,
Expand All @@ -107,11 +129,32 @@ def send_message(client, options, traceback):
))

if client.state.did_fail():
print('error!')
return False

print('success!')
print('Event ID was %r' % (ident,))
return ident


def group_log(client, options, to_follow, proc=None):
in_harakiri = False
harakiri_lines = []
harakiris_seen = 0
while 1:
line = to_follow.readline()
if not line:
if proc and proc.returncode:
print("Error: tail exited uncleanly -- you must use GNU tail")
os.exit(1)
break
if 'HARAKIRI ON WORKER' in line:
in_harakiri = True
if in_harakiri:
harakiri_lines.append(line)
harakiris_seen += 'HARAKIRI' in line
if harakiris_seen == 4:
send_message(client, options, log="".join(harakiri_lines))
in_harakiri = False
harakiri_lines = []
harakiris_seen = 0


def main():
Expand All @@ -123,9 +166,21 @@ def main():
parser.add_option("--tags", action="callback", callback=store_json, type="string", nargs=1, dest="tags")
parser.add_option("--verbose", action="store_true")
parser.add_option("--dsn")
parser.add_option("--tail", action="store_true")
parser.add_option("--watch", action="store_true")
parser.add_option("--thread-regex")
opts, args = parser.parse_args()

dsn = opts.dsn or os.environ.get('SENTRY_DSN')

if not dsn:
try:
from django.conf import settings
except ImportError:
pass
else:
dsn = settings.RAVEN_CONFIG['dsn']

if not dsn:
print("Error: No configuration detected!")
print("You must either pass a DSN to the command, or set the SENTRY_DSN environment variable.")
Expand All @@ -134,14 +189,26 @@ def main():
if not opts.verbose:
sys.stdout = StringIO()

print("Using DSN configuration:")
print(" ", dsn)
print()

traceback = ''.join([line for line in fileinput.input(args)])

client = Client(dsn, include_paths=['raven'], string_max_length=100000)
send_message(client, opts.__dict__, traceback=traceback)

if opts.watch:
if len(args) != 1:
print('Error: In --watch mode you must provide exactly one file '
'argument')
sys.exit(1)
tail = subprocess.Popen(['tail', '--follow=name', '--retry', args[0]], stdout=subprocess.PIPE)
group_log(client, opts.__dict__, tail.stdout, tail)

elif opts.tail:
if len(args) > 0:
print('Error: In --tail mode, no file arguments should be provided'
' -- pipe to STDIN instead')
sys.exit(1)
group_log(client, opts.__dict__, sys.stdin)

else:
log = ''.join([line for line in fileinput.input(args)])
send_message(client, opts.__dict__, log=log)


if __name__ == '__main__':
Expand Down
16 changes: 8 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import sys
from setuptools.command.test import test as TestCommand
import setuptools.command.test
from setuptools import setup


class PyTest(TestCommand):
class PyTest(setuptools.command.test.test):
def finalize_options(self):
TestCommand.finalize_options(self)
setuptools.command.test.test.finalize_options(self)
self.test_args = []
self.test_suite = True

Expand All @@ -17,20 +17,20 @@ def run_tests(self):

setup(
name='raven-harakiri',
version='0.1.2',
version='0.1.8',
py_modules=['raven_harakiri'],
url='http://github.com/futurecolors/raven-harakiri',
url='http://github.com/LexMachinaInc/raven-harakiri',
license='MIT',
author='Ilya Baryshev',
author_email='baryshev@gmail.com',
author='Sean Davis',
author_email='sdavis@lexmachina.com',
description='Send UWSGI harakiri logs to sentry',
long_description=open('README.rst').read(),
entry_points={
'console_scripts': [
'raven-harakiri = raven_harakiri:main',
],
},
install_requires=['raven>=3.4'],
install_requires=['raven>=3.4', 'six'],
tests_require=['pytest'],
cmdclass={'test': PyTest},
classifiers=[
Expand Down
16 changes: 15 additions & 1 deletion test_harakiri.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
/lib/libpthread.so.0(+0x68ca) [0x7f2c44b4a8ca]
/lib/libc.so.6(clone+0x6d) [0x7f2c43413b6d]"""

from raven_harakiri import convert_traceback
from raven_harakiri import convert_traceback, extract_http


def test_convert():
Expand All @@ -78,3 +78,17 @@ def test_convert():
'pre_context': [],
'vars': {}
}


example_http = \
""" Mon Jan 2 14:43:09 2017 - HARAKIRI !!! worker 1 status !!!
Mon Jan 2 14:43:09 2017 - HARAKIRI [core 0] 127.0.0.1 - GET /trigger_harakiri/ since 1483368158
Mon Jan 2 14:43:09 2017 - HARAKIRI !!! end of worker 1 status !!!
"""


def test_extract_http():
method, url, remote_addr = extract_http(example_http)
assert method == 'GET'
assert url == '/trigger_harakiri/'
assert remote_addr == '127.0.0.1'