Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9bc710c
Initial commit and project setup
mathom Sep 30, 2014
3f974b0
Some organization and app identity
mathom Sep 30, 2014
28f15cf
Use internal loggers
mathom Sep 30, 2014
13736f1
Refactoring some things, using requests
mathom Oct 4, 2014
edcb532
Log request payload
mathom Oct 4, 2014
1a29e7a
Wrap extra data as a string
mathom Oct 4, 2014
1bae951
Fix gzip compression
mathom Oct 4, 2014
d6475a4
Some refactoring for the base json class
mathom Oct 4, 2014
ae2e19f
Adding basic tracebacks
mathom Oct 4, 2014
1d634a4
Fix gzip for python3
mathom Oct 4, 2014
c491d68
Adding handler base and logrecord stuff
mathom Oct 5, 2014
b356445
More work on handler and listener
mathom Oct 5, 2014
79f1ad0
Finishing up the handler threading
mathom Oct 11, 2014
c373221
Format later in the logger
mathom Oct 11, 2014
7e55979
Adding unittest setup
mathom Oct 11, 2014
77847b2
More app test
mathom Oct 11, 2014
bc631ba
More tests, fixing bugs exposed by tests
mathom Oct 12, 2014
75b83a7
More logging tests
mathom Oct 12, 2014
6403154
Default level should be WARNING
mathom Oct 12, 2014
dc0b289
Test handlers
mathom Oct 12, 2014
f3f2112
Base http tests
mathom Oct 12, 2014
b4abed6
Reenable the retry module and test it
mathom Oct 12, 2014
9c3af5e
Test identify function
mathom Oct 12, 2014
5063de0
Test POST args
mathom Oct 12, 2014
c8f69f9
GZIP through POST test
mathom Oct 12, 2014
223718f
Some patch streamlining and new test
mathom Oct 12, 2014
4fb93a1
Make setup executable
mathom Oct 12, 2014
67319d1
Fix patch and add more testing
mathom Oct 12, 2014
a0319d5
More test changes, format fix for logger
mathom Oct 12, 2014
7274070
Adding json object tests
mathom Oct 12, 2014
32d165a
Adding log tests
mathom Oct 12, 2014
be81190
Exception testing
mathom Oct 12, 2014
f55f28f
Adding basic readme
mathom Oct 12, 2014
c91ca00
More doc tweaks
mathom Oct 12, 2014
0f2af25
Fixing nose and adding a new test
mathom Oct 14, 2014
b86db74
Fix tests for python3
mathom Oct 14, 2014
d3b1eb2
Testing format and fixing format bug
mathom Oct 14, 2014
ad22f11
Some cleanup
mathom Oct 14, 2014
09c8ad5
Clean up internal logging
mathom Oct 14, 2014
25269bb
More docs
mathom Oct 14, 2014
27959ef
Some PEP8 cleanup
mathom Oct 15, 2014
dcba89d
Some docstring tweaks
mathom Oct 15, 2014
97ccaf5
Fixing setup for pypi submission
mathom Oct 15, 2014
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
32 changes: 32 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
*.py[co]

# Packages
*.egg
*.egg-info
dist
build
eggs
parts
var
sdist
develop-eggs
.installed.cfg

# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
.tox

#Translations
*.mo

#Mr Developer
.mr.developer.cfg

# virutalenvs
.venv

# debug stuff
test.py
1 change: 1 addition & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
TODO
101 changes: 98 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,99 @@
stackify-api-python
===================

Stackify API for Python
=======

[Stackify](https://stackify.com) support for Python programs.

```python
import stackify

logger = stackify.getLogger()

try:
"Make it so, #" + 1
except:
logger.exception("Can't add strings and numbers")
```

## Installation
stackify-python can be installed through pip:
```bash
$ pip install -U stackify
```

You can also check out the repository and install with setuptools:
```bash
$ ./setup.py install
```

## Configuration
Your Stackify setup information can be provided via environment variables. For example:
```bash
export STACKIFY_APPLICATION=MyApp
export STACKIFY_ENVIRONMENT=Dev
export STACKIFY_API_KEY=******
```

These options can also be provided in your code:
```python
import stackify

logger = stackify.getLogger(application="MyApp", environment="Dev", api_key=******)
logger.warning('Something happened')
```

## Usage

stackify-python handles uploads in batches of 100 messages at a time on another thread.
When your program exits, it will shut the thread down and upload the remaining messages.

Stackify can store extra data along with your log message:
```python
import stackify

logger = stackify.getLogger()

try:
user_string = raw_input("Enter a number: ")
print("You entered", int(user_string))
except ValueError:
logger.exception('Bad input', extra={'user entered': user_string})
```

You can also name your logger instead of using the automatically generated one:
```python
import stackify

logger = stackify.getLogger('mymodule.myfile')
```

## Internal Logger

This library has an internal logger it uses for debugging and messaging.
For example, if you want to enable debug messages:
```python
import logging

logging.getLogger('stackify').setLevel(logging.DEBUG)
```

By default, it will enable the default logging settings via `logging.basicConfig()`
and print `WARNING` level messages and above. If you wish to set everything up yourself,
just pass `basic_config=False` in `getLogger`:
```python
import stackify

logger = stackify.getLogger(basic_config=False)
```

## Testing
Run the test suite with setuptools:
```bash
$ ./setup.py test
```

You can obtain a coverage report with nose:
```bash
$ ./setup nosetests --with-coverage --cover-package=stackify
```
You might need to install the `nose` and `coverage` packages.

42 changes: 42 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env python
from setuptools import setup
import re
import ast

try:
from pypandoc import convert
read_md = lambda f: convert(f, 'rst')
except ImportError:
print('warning: pypandoc module not found, could not convert Markdown to RST')
read_md = lambda f: open(f).read()

version_re = re.compile(r'__version__\s+=\s+(.*)')

with open('stackify/__init__.py') as f:
f = f.read()
version = ast.literal_eval(version_re.search(f).group(1))

setup(
name = 'stackify',
version = version,
author = 'Matthew Thompson',
author_email = 'chameleonator@gmail.com',
packages = ['stackify'],
url = 'https://github.com/stackify/stackify-api-python',
license = open('LICENSE.txt').readline(),
description = 'Stackify API for Python',
long_description = read_md('README.md'),
download_url = 'https://github.com/stackify/stackify-api-python/tarball/0.0.1',
keywords = ['logging', 'stackify', 'exception'],
classifiers=["Programming Language :: Python"],
install_requires = [
'retrying>=1.2.3',
'requests>=2.4.1'
],
test_suite = 'tests',
tests_requires = [
'mock>=1.0.1',
'nose==1.3.4'
]
)

120 changes: 120 additions & 0 deletions stackify/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""
Stackify Python API
"""

__version__ = '0.0.1'


API_URL = 'https://api.stackify.com'

READ_TIMEOUT = 5000

MAX_BATCH = 100

QUEUE_SIZE = 1000

import logging
import inspect
import atexit

DEFAULT_LEVEL = logging.ERROR

LOGGING_LEVELS = {
logging.CRITICAL: 'CRITICAL',
logging.ERROR: 'ERROR',
logging.WARNING: 'WARNING',
logging.INFO: 'INFO',
logging.DEBUG: 'DEBUG',
logging.NOTSET: 'NOTSET'
}


class NullHandler(logging.Handler):
def emit(self, record):
pass

logging.getLogger(__name__).addHandler(NullHandler())


from stackify.application import ApiConfiguration
from stackify.http import HTTPClient

from stackify.handler import StackifyHandler


def getLogger(name=None, auto_shutdown=True, basic_config=True, **kwargs):
'''Get a logger and attach a StackifyHandler if needed.

You can pass this function keyword arguments for Stackify configuration.
If they are omitted you can specify them through environment variables:
* STACKIFY_API_KEY
* STACKIFY_APPLICATION
* STACKIFY_ENVIRONMENT
* STACKIFY_API_URL

Args:
name: The name of the logger (or None to automatically make one)
auto_shutdown: Register an atexit hook to shut down logging
basic_config: Set up with logging.basicConfig() for regular logging

Optional Args:
api_key: Your Stackify API key
application: The name of your Stackify application
environment: The Stackfiy environment to log to
api_url: An optional API url if required

Returns:
A logger instance with Stackify handler and listener attached.
'''
if basic_config:
logging.basicConfig()

if not name:
name = getCallerName(2)

logger = logging.getLogger(name)

if not [isinstance(x, StackifyHandler) for x in logger.handlers]:
internal_logger = logging.getLogger(__name__)
internal_logger.debug('Creating handler for logger %s', name)
handler = StackifyHandler(**kwargs)
logger.addHandler(handler)

if auto_shutdown:
internal_logger.debug('Registering atexit callback')
atexit.register(stopLogging, logger)

if logger.getEffectiveLevel() == logging.NOTSET:
logger.setLevel(DEFAULT_LEVEL)

handler.listener.start()

return logger


def stopLogging(logger):
'''Stop logging on the Stackify handler.

Shut down the StackifyHandler on a given logger. This will block
and wait for the queue to finish uploading.
'''
internal_logger = logging.getLogger(__name__)
internal_logger.debug('Shutting down all handlers')
for handler in getHandlers(logger):
handler.listener.stop()


def getCallerName(levels=1):
'''Gets the name of the module calling this function'''
try:
frame = inspect.stack()[levels]
module = inspect.getmodule(frame[0])
name = module.__name__
except IndexError:
name = 'stackify-python-unknown'
return name


def getHandlers(logger):
'''Return the StackifyHandlers on a given logger'''
return [x for x in logger.handlers if isinstance(x, StackifyHandler)]
44 changes: 44 additions & 0 deletions stackify/application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import socket
import os

from stackify import API_URL
from stackify.formats import JSONObject


class EnvironmentDetail(JSONObject):
def __init__(self, api_config):
self.deviceName = socket.gethostname()
self.appLocation = os.getcwd()
self.configuredAppName = api_config.application
self.configuredEnvironmentName = api_config.environment


class ApiConfiguration:
def __init__(self, api_key, application, environment, api_url=API_URL):
self.api_key = api_key
self.api_url = api_url
self.application = application
self.environment = environment


def arg_or_env(name, args, default=None):
env_name = 'STACKIFY_{0}'.format(name.upper())
try:
value = args.get(name)
if not value:
value = os.environ[env_name]
return value
except KeyError:
if default:
return default
else:
raise NameError('You must specify the keyword argument {0} or '
'environment variable {1}'.format(name, env_name))


def get_configuration(**kwargs):
return ApiConfiguration(
application=arg_or_env('application', kwargs),
environment=arg_or_env('environment', kwargs),
api_key=arg_or_env('api_key', kwargs),
api_url=arg_or_env('api_url', kwargs, API_URL))
Loading