Skip to content
This repository has been archived by the owner on Sep 18, 2024. It is now read-only.

Commit

Permalink
add the code
Browse files Browse the repository at this point in the history
  • Loading branch information
jlabath committed Dec 27, 2018
1 parent 6132bed commit 9cda170
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 0 deletions.
7 changes: 7 additions & 0 deletions datadoglog/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from .formatters import FormatFactory
from .handlers import start_logger

__all__ = [
'FormatFactory',
'start_logger',
]
52 changes: 52 additions & 0 deletions datadoglog/formatters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import os
import json
import logging
import datetime
import traceback


class FormatFactory:

app_key = None
service = None
source = None
env = None

def __init__(self, app_key: str, source: str, service: str, env: str) -> None:
self.app_key = app_key
self.service = service
self.source = source
self.env = env
self.host = os.uname().nodename

def my_format(self, record: logging.LogRecord) -> str:
data = {
"timestamp": datetime.datetime.fromtimestamp(
record.created, datetime.timezone.utc).strftime(
"%Y-%m-%dT%H:%M:%S.%fZ"),
"host": self.host,
"ddsource": self.source,
"syslog.env": self.env,
"service": self.service,
"message": record.getMessage(),
"level": record.levelname,
"logger.name": record.name,
"pathname": record.pathname,
"lineno": record.lineno,
}
# work with exception here
ex_info = record.exc_info
if ex_info is not None:
(extype, value, tb) = ex_info
data["error.message"] = str(value)
data["error.kind"] = str(extype)
data["error.stack"] = "".join(traceback.format_tb(tb))
return "{} {}".format(self.app_key, json.dumps(data))

def format(self, record: logging.LogRecord) -> str:
# yeah python logging expects formatting of a message to also adjust
# internal attributes of LogRecord - so sad
# on the other hand record.message is the only thing that will matter
# from here on
record.message = self.my_format(record)
return record.message
78 changes: 78 additions & 0 deletions datadoglog/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import logging
import socket
import ssl
import typing
from queue import Queue


def do_debug() -> bool:
return False

# reference to fuly grasp what is happening here
# https://github.com/python/cpython/blob/master/Lib/logging/handlers.py


class DogHandler(logging.handlers.SocketHandler):

def __init__(self):
# initialize parent with datadogs info
# hardcoding is fine given that should this change
# we likely need to fix this code anyway
# gives less chance for users to mess things up
# as well
super().__init__("intake.logs.datadoghq.com", 10516)

def makePickle(self, record):
"""prepares record for writing over wire"""
return "{}\n".format(record.message).encode("utf8")

def send(self, s):
"""sends serialized data s over wire"""
if do_debug():
print("DOGDEBUG send", s)
super().send(s)

def makeSocket(self, timeout=1):
"""
A factory method which allows subclasses to define the precise
type of socket they want.
Since our logging data is sensitive we most def want to send it
over SSL so we subclass and act accordingly
"""
context = ssl.create_default_context()

# self.address should contain the host port tuple
# that was initialized by the SocketHandler.__init__
# here we rely on inner workings of other peoples code
# people who encourage us to do so while having no idea
# what parts of their code we are depending on
# but hey this is official python code and docs
# surely they know what they are doing, right?
# personally if any of this breaks in future versions of python
# not least bit surprised
# written and tested for python 3.6
s = socket.create_connection(self.address, timeout=timeout)
conn = context.wrap_socket(s, server_hostname=self.host)
if do_debug():
print("DOGDEBUG makeSocket connected {} to {} with {}".format(
str(conn), str(self.address), conn.version()))
return conn


def start_logger(que: Queue) -> typing.Callable:
"""
this starts the QueueListener in separate thread
that will just consume and forward all the messages to datadog
so logging should never ever block
and can be used by any high performance apps
it returns a function which called will properly shut down the listener thread
and close tcp sockets
"""
if do_debug():
print('Starting logger for queue', que)
handler = DogHandler()
listener = logging.handlers.QueueListener(que, handler)
formatter = logging.Formatter("%(message)s")
handler.setFormatter(formatter)
listener.start()
return listener.stop
21 changes: 21 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import setuptools

with open("README.md", "r") as fh:
long_description = fh.read()

setuptools.setup(
name="datadoglog",
version="0.0.1",
author="Jakub Labath",
author_email="[email protected]",
description="Logging to datadog log",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/gadventures/datadoglog",
packages=setuptools.find_packages(),
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
],
)

0 comments on commit 9cda170

Please sign in to comment.