logtopg 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- logtopg-1.0.0/MANIFEST.in +1 -0
- logtopg-1.0.0/PKG-INFO +12 -0
- logtopg-1.0.0/README.rst +129 -0
- logtopg-1.0.0/logtopg/__init__.py +265 -0
- logtopg-1.0.0/logtopg/createtable.sql +40 -0
- logtopg-1.0.0/logtopg/insertrow.sql +36 -0
- logtopg-1.0.0/logtopg/tests/__init__.py +0 -0
- logtopg-1.0.0/logtopg/tests/test_logtopg.py +278 -0
- logtopg-1.0.0/logtopg/version.py +25 -0
- logtopg-1.0.0/logtopg.egg-info/PKG-INFO +12 -0
- logtopg-1.0.0/logtopg.egg-info/SOURCES.txt +14 -0
- logtopg-1.0.0/logtopg.egg-info/dependency_links.txt +1 -0
- logtopg-1.0.0/logtopg.egg-info/requires.txt +1 -0
- logtopg-1.0.0/logtopg.egg-info/top_level.txt +1 -0
- logtopg-1.0.0/setup.cfg +4 -0
- logtopg-1.0.0/setup.py +33 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include logtopg *.sql
|
logtopg-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: logtopg
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python logging handler that stores logs in postgresql
|
|
5
|
+
Home-page: https://github.com/216software/logtopg/
|
|
6
|
+
Author: 216 Software, LLC
|
|
7
|
+
Author-email: info@216software.com
|
|
8
|
+
License: BSD License
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
|
|
11
|
+
UNKNOWN
|
|
12
|
+
|
logtopg-1.0.0/README.rst
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
+++++++++++++++++
|
|
2
|
+
Log to PostgreSQL
|
|
3
|
+
+++++++++++++++++
|
|
4
|
+
|
|
5
|
+
.. image:: https://travis-ci.org/216software/logtopg.svg?branch=master
|
|
6
|
+
:target: https://travis-ci.org/216software/logtopg
|
|
7
|
+
|
|
8
|
+
.. image:: https://circleci.com/gh/216software/logtopg.png?circle-token=389fee16249541b4b1df6e8a7f8edb1401be66de
|
|
9
|
+
:target:https://circleci.com/gh/216software/logtopg
|
|
10
|
+
|
|
11
|
+
Install
|
|
12
|
+
=======
|
|
13
|
+
|
|
14
|
+
Grab the code with pip::
|
|
15
|
+
|
|
16
|
+
$ pip install logtopg
|
|
17
|
+
|
|
18
|
+
But you also have to install the ltree contrib module into your
|
|
19
|
+
database::
|
|
20
|
+
|
|
21
|
+
$ sudo -u postgres psql -c "create extension ltree;"
|
|
22
|
+
|
|
23
|
+
Try it out
|
|
24
|
+
==========
|
|
25
|
+
|
|
26
|
+
The code in `docs/example.py`_ shows how to set up your logging configs
|
|
27
|
+
with this handler.
|
|
28
|
+
|
|
29
|
+
.. _`docs/example.py`: https://github.com/216software/logtopg/blob/master/docs/example.py
|
|
30
|
+
|
|
31
|
+
.. include:: docs/example.py
|
|
32
|
+
:number-lines:
|
|
33
|
+
:code: python
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
Contribute to logtopg
|
|
37
|
+
=====================
|
|
38
|
+
|
|
39
|
+
Get a copy of the code::
|
|
40
|
+
|
|
41
|
+
$ git clone --origin github https://github.com/216software/logtopg.git
|
|
42
|
+
|
|
43
|
+
Install it like this::
|
|
44
|
+
|
|
45
|
+
$ cd logtopg
|
|
46
|
+
$ pip install -e .
|
|
47
|
+
|
|
48
|
+
Create test user and test database::
|
|
49
|
+
|
|
50
|
+
$ sudo -u postgres createuser logtopg
|
|
51
|
+
$ sudo -u postgres createdb --owner logtopg logtopg_tests
|
|
52
|
+
$ sudo -u postgres psql -c "create extension ltree;" -d logtopg_tests
|
|
53
|
+
|
|
54
|
+
Then run the tests like this::
|
|
55
|
+
|
|
56
|
+
$ python setup.py --quiet test
|
|
57
|
+
.....
|
|
58
|
+
----------------------------------------------------------------------
|
|
59
|
+
Ran 5 tests in 0.379s
|
|
60
|
+
|
|
61
|
+
OK
|
|
62
|
+
|
|
63
|
+
Hopefully it works!
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
Stuff to do
|
|
67
|
+
===========
|
|
68
|
+
|
|
69
|
+
* Fill out classifiers in setup.py.
|
|
70
|
+
|
|
71
|
+
* Somehow block updates to the table. Maybe a trigger is the right
|
|
72
|
+
way. Maybe there's a much simpler trick that I'm not aware of.
|
|
73
|
+
|
|
74
|
+
* Create a few views for typical queries.
|
|
75
|
+
|
|
76
|
+
* Test performance with many connected processes and tons of logging
|
|
77
|
+
messages. Make sure that logging doesn't compete with real
|
|
78
|
+
application work for database resources. Is there a way to say
|
|
79
|
+
something like
|
|
80
|
+
|
|
81
|
+
"Hey postgresql, take your time with this stuff, and deal with
|
|
82
|
+
other stuff first!"
|
|
83
|
+
|
|
84
|
+
In other words, a "nice" command for queries.
|
|
85
|
+
|
|
86
|
+
* Allow people to easily write their own SQL to create the logging
|
|
87
|
+
table and to insert records to it. The queries could be returned
|
|
88
|
+
from properties, so people would just need to subclass the PGHandler
|
|
89
|
+
and then redefine those properties.
|
|
90
|
+
|
|
91
|
+
* Write some documentation:
|
|
92
|
+
|
|
93
|
+
* installation
|
|
94
|
+
* typical queries
|
|
95
|
+
* tweak log table columns or indexes
|
|
96
|
+
* discuss performance issues
|
|
97
|
+
|
|
98
|
+
* Set up a readthedocs page for logtopg for that documentation.
|
|
99
|
+
|
|
100
|
+
* Experiment with what happens when the emit(...) function call takes
|
|
101
|
+
a long time. For example, say somebody is logging to a PG server
|
|
102
|
+
across the internet, will calls to log.debug(...) slow down the
|
|
103
|
+
local app? I imagine so.
|
|
104
|
+
|
|
105
|
+
* I just found out that the ltree column type (that I use for logger
|
|
106
|
+
names) can not handle logger names like "dazzle.insert-stuff". That
|
|
107
|
+
dash in there is invalid syntax.
|
|
108
|
+
|
|
109
|
+
I hope there is a way to raise an exception as soon as somebody uses
|
|
110
|
+
an invalid logger name.
|
|
111
|
+
|
|
112
|
+
Or, maybe I need to convert the invalid name to a valid name, by
|
|
113
|
+
maybe substituting any of a set of characters with something else.
|
|
114
|
+
|
|
115
|
+
* Set up table partitioning so that when there are millions or logs,
|
|
116
|
+
they are dealt with sanely.
|
|
117
|
+
|
|
118
|
+
This is a query that shows logs by day and log level::
|
|
119
|
+
|
|
120
|
+
select to_char(date_trunc('day', inserted), 'YYYY-MM-DD'),
|
|
121
|
+
log_level, count(*)
|
|
122
|
+
|
|
123
|
+
from dazzlelogs
|
|
124
|
+
|
|
125
|
+
group by 1, 2
|
|
126
|
+
|
|
127
|
+
order by 1, 2;
|
|
128
|
+
|
|
129
|
+
.. vim: set syntax=rst:
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# vim: set expandtab ts=4 sw=4 filetype=python fileencoding=utf8:
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import textwrap
|
|
8
|
+
import traceback
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
import psutil
|
|
12
|
+
|
|
13
|
+
import pkg_resources
|
|
14
|
+
import psycopg2
|
|
15
|
+
from psycopg2.extensions import adapt
|
|
16
|
+
|
|
17
|
+
from logtopg.version import __version__
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
class PGHandler(logging.Handler):
|
|
22
|
+
|
|
23
|
+
def __init__(self, log_table_name,
|
|
24
|
+
database,
|
|
25
|
+
user=None,
|
|
26
|
+
password=None,
|
|
27
|
+
host=None,
|
|
28
|
+
port=5432):
|
|
29
|
+
|
|
30
|
+
logging.Handler.__init__(self)
|
|
31
|
+
|
|
32
|
+
self.log_table_name = log_table_name
|
|
33
|
+
|
|
34
|
+
self.database = database
|
|
35
|
+
self.host = host
|
|
36
|
+
self.user = user
|
|
37
|
+
self.password = password
|
|
38
|
+
self.port = port
|
|
39
|
+
|
|
40
|
+
self.pgconn = None
|
|
41
|
+
self.create_table_sql = None
|
|
42
|
+
self.insert_row_sql = None
|
|
43
|
+
|
|
44
|
+
def check_if_log_table_exists(self):
|
|
45
|
+
|
|
46
|
+
while (True):
|
|
47
|
+
pgconn = self.get_pgconn()
|
|
48
|
+
|
|
49
|
+
cursor = pgconn.cursor()
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
cursor.execute("""
|
|
53
|
+
SELECT %s::regclass;
|
|
54
|
+
""", [self.log_table_name])
|
|
55
|
+
except psycopg2.ProgrammingError as e:
|
|
56
|
+
return False
|
|
57
|
+
except InterfaceError as ie:
|
|
58
|
+
self.pgconn = None
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
def maybe_create_table(self):
|
|
64
|
+
|
|
65
|
+
if not self.check_if_log_table_exists():
|
|
66
|
+
|
|
67
|
+
create_table_sql = self.get_create_table_sql()
|
|
68
|
+
|
|
69
|
+
out = run_sql_commands(create_table_sql, self.user, self.password,
|
|
70
|
+
self.host, self.port, self.database)
|
|
71
|
+
|
|
72
|
+
log.info("Created log table {0}.".format(self.log_table_name))
|
|
73
|
+
|
|
74
|
+
def get_pgconn(self):
|
|
75
|
+
|
|
76
|
+
if not self.pgconn:
|
|
77
|
+
self.make_pgconn()
|
|
78
|
+
|
|
79
|
+
return self.pgconn
|
|
80
|
+
|
|
81
|
+
def make_pgconn(self):
|
|
82
|
+
|
|
83
|
+
self.pgconn = psycopg2.connect(
|
|
84
|
+
database=self.database,
|
|
85
|
+
host=self.host,
|
|
86
|
+
user=self.user,
|
|
87
|
+
password=self.password,
|
|
88
|
+
port=self.port)
|
|
89
|
+
|
|
90
|
+
self.pgconn.autocommit = True
|
|
91
|
+
|
|
92
|
+
log.info("Just made an autocommitting database connection: {0}.".format(
|
|
93
|
+
self.pgconn))
|
|
94
|
+
|
|
95
|
+
def get_create_table_sql(self):
|
|
96
|
+
|
|
97
|
+
if not self.create_table_sql:
|
|
98
|
+
|
|
99
|
+
s = \
|
|
100
|
+
pkg_resources.resource_string(
|
|
101
|
+
"logtopg", "createtable.sql")\
|
|
102
|
+
.decode("utf-8")\
|
|
103
|
+
.format(self.log_table_name)
|
|
104
|
+
|
|
105
|
+
self.create_table_sql = s.encode("utf-8")
|
|
106
|
+
|
|
107
|
+
return self.create_table_sql
|
|
108
|
+
|
|
109
|
+
def get_insert_row_sql(self):
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
Cache the insert query (with placeholder parameters) in memory
|
|
113
|
+
so that every log.... call doesn't do file IO.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
if not self.insert_row_sql:
|
|
117
|
+
|
|
118
|
+
self.insert_row_sql = \
|
|
119
|
+
pkg_resources.resource_string(
|
|
120
|
+
"logtopg", "insertrow.sql")\
|
|
121
|
+
.decode("utf-8")\
|
|
122
|
+
.format(self.log_table_name)
|
|
123
|
+
|
|
124
|
+
return self.insert_row_sql
|
|
125
|
+
|
|
126
|
+
def build_d(self, record_dict):
|
|
127
|
+
|
|
128
|
+
d = record_dict
|
|
129
|
+
|
|
130
|
+
# Insert process info
|
|
131
|
+
d['cmd_line'] = " ".join(psutil.Process(os.getpid()).cmdline())
|
|
132
|
+
|
|
133
|
+
# Catch messages that can't be adapted as-is, and convert it to
|
|
134
|
+
# strings
|
|
135
|
+
try:
|
|
136
|
+
d["msg"] = adapt(record_dict["msg"])
|
|
137
|
+
|
|
138
|
+
except Exception as ex:
|
|
139
|
+
d["msg"] = str(record_dict["msg"])
|
|
140
|
+
|
|
141
|
+
return d
|
|
142
|
+
|
|
143
|
+
def emit(self, record):
|
|
144
|
+
|
|
145
|
+
self.format(record)
|
|
146
|
+
|
|
147
|
+
if record.exc_info:
|
|
148
|
+
record.exc_text = logging._defaultFormatter.formatException(record.exc_info)
|
|
149
|
+
|
|
150
|
+
else:
|
|
151
|
+
record.exc_text = ""
|
|
152
|
+
|
|
153
|
+
if isinstance(record.msg, Exception):
|
|
154
|
+
record.msg = str(record.msg)
|
|
155
|
+
|
|
156
|
+
pgconn = self.get_pgconn()
|
|
157
|
+
|
|
158
|
+
self.maybe_create_table()
|
|
159
|
+
|
|
160
|
+
cursor = pgconn.cursor()
|
|
161
|
+
|
|
162
|
+
cursor.execute(
|
|
163
|
+
self.get_insert_row_sql(),
|
|
164
|
+
self.build_d(record.__dict__))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
example_dict_config = dict({
|
|
168
|
+
|
|
169
|
+
"loggers": {
|
|
170
|
+
"logtopg": {
|
|
171
|
+
# "handlers": ["pg", "console"],
|
|
172
|
+
"handlers": ["console"],
|
|
173
|
+
"level": "DEBUG",
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
'handlers': {
|
|
178
|
+
'pg': {
|
|
179
|
+
'class': 'logtopg.PGHandler',
|
|
180
|
+
'level': 'DEBUG',
|
|
181
|
+
'log_table_name': 'logtopg_logs',
|
|
182
|
+
|
|
183
|
+
"database":"logtopg",
|
|
184
|
+
"host":"localhost",
|
|
185
|
+
"user":"logtopg",
|
|
186
|
+
"password":"l0gt0pg",
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
"console": {
|
|
190
|
+
"class": "logging.StreamHandler",
|
|
191
|
+
"level": "DEBUG",
|
|
192
|
+
"formatter": "consolefmt",
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
"formatters": {
|
|
198
|
+
"consolefmt":{
|
|
199
|
+
"format": '%(asctime)-22s [%(process)d] %(name)-30s %(lineno)-5d %(levelname)-8s %(message)s',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
# Any handlers attached to root get log messages from EVERYTHING,
|
|
204
|
+
# like third-party modules, etc.
|
|
205
|
+
'root': {
|
|
206
|
+
'handlers': ["pg"],
|
|
207
|
+
'level': 'DEBUG',
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
'version': 1,
|
|
211
|
+
|
|
212
|
+
# This is important! Without it, any log instances created before
|
|
213
|
+
# you run logging.config.dictConfig(...) will be disabled, which
|
|
214
|
+
# means all the global log objects in all the various imported files
|
|
215
|
+
# won't do anything.
|
|
216
|
+
'disable_existing_loggers': False,
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
def run_sql_commands(sql_text, user, password, host, port, database):
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
Run a whole bunch of SQL commands. This is nice when you have a
|
|
223
|
+
script with more than one statement in it.
|
|
224
|
+
|
|
225
|
+
Don't pass me the path to a SQL script file! Instead, give me the
|
|
226
|
+
sql text after you read it in from a file.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
env = os.environ.copy()
|
|
230
|
+
|
|
231
|
+
if password:
|
|
232
|
+
env['PGPASSWORD'] = password
|
|
233
|
+
|
|
234
|
+
# Feed the sql_text to psql's stdin.
|
|
235
|
+
# http://stackoverflow.com/questions/163542/python-how-do-i-pass-a-string-into-subprocess-popen-using-the-stdin-argument
|
|
236
|
+
|
|
237
|
+
stuff = [
|
|
238
|
+
"psql",
|
|
239
|
+
"--quiet",
|
|
240
|
+
"--no-psqlrc",
|
|
241
|
+
"-d",
|
|
242
|
+
database,
|
|
243
|
+
"--single-transaction",
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
if user:
|
|
247
|
+
stuff.append("-U")
|
|
248
|
+
stuff.append(user)
|
|
249
|
+
|
|
250
|
+
if host:
|
|
251
|
+
stuff.append("-h")
|
|
252
|
+
stuff.append(host)
|
|
253
|
+
|
|
254
|
+
if port:
|
|
255
|
+
stuff.append("-p")
|
|
256
|
+
stuff.append(str(port))
|
|
257
|
+
|
|
258
|
+
p = subprocess.Popen(
|
|
259
|
+
stuff,
|
|
260
|
+
stdin=subprocess.PIPE,
|
|
261
|
+
env=env)
|
|
262
|
+
|
|
263
|
+
out = p.communicate(input=sql_text)
|
|
264
|
+
|
|
265
|
+
return out
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
create table if not exists {0} (
|
|
2
|
+
|
|
3
|
+
log_id int generated
|
|
4
|
+
by default as identity primary key,
|
|
5
|
+
|
|
6
|
+
created timestamptz,
|
|
7
|
+
|
|
8
|
+
process_id int,
|
|
9
|
+
process_name text,
|
|
10
|
+
|
|
11
|
+
logger_name ltree,
|
|
12
|
+
|
|
13
|
+
path_name text,
|
|
14
|
+
module text,
|
|
15
|
+
file_name text,
|
|
16
|
+
|
|
17
|
+
function_name text,
|
|
18
|
+
|
|
19
|
+
line_number int,
|
|
20
|
+
|
|
21
|
+
log_level text,
|
|
22
|
+
log_level_number int,
|
|
23
|
+
|
|
24
|
+
cmd_line text,
|
|
25
|
+
|
|
26
|
+
message text,
|
|
27
|
+
|
|
28
|
+
exc_info text,
|
|
29
|
+
thread_id bigint,
|
|
30
|
+
thread_name text,
|
|
31
|
+
inserted timestamptz not null default now()
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
create index on {0} (created);
|
|
35
|
+
create index on {0} (inserted);
|
|
36
|
+
create index on {0} (logger_name);
|
|
37
|
+
create index on {0} (process_id);
|
|
38
|
+
|
|
39
|
+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
|
40
|
+
CREATE INDEX idx_logs_cmdline ON {0} USING GIN (cmd_line gin_trgm_ops);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
insert into {0} (
|
|
2
|
+
created,
|
|
3
|
+
process_id,
|
|
4
|
+
process_name,
|
|
5
|
+
logger_name,
|
|
6
|
+
path_name,
|
|
7
|
+
module,
|
|
8
|
+
file_name,
|
|
9
|
+
function_name,
|
|
10
|
+
line_number,
|
|
11
|
+
log_level,
|
|
12
|
+
log_level_number,
|
|
13
|
+
message,
|
|
14
|
+
exc_info,
|
|
15
|
+
cmd_line,
|
|
16
|
+
thread_id,
|
|
17
|
+
thread_name
|
|
18
|
+
) values (
|
|
19
|
+
to_timestamp(%(created)s),
|
|
20
|
+
%(process)s,
|
|
21
|
+
%(processName)s,
|
|
22
|
+
%(name)s,
|
|
23
|
+
%(pathname)s,
|
|
24
|
+
%(module)s,
|
|
25
|
+
%(filename)s,
|
|
26
|
+
%(funcName)s,
|
|
27
|
+
%(lineno)s,
|
|
28
|
+
%(levelname)s,
|
|
29
|
+
%(levelno)s,
|
|
30
|
+
%(message)s,
|
|
31
|
+
%(exc_text)s,
|
|
32
|
+
%(cmd_line)s,
|
|
33
|
+
%(thread)s,
|
|
34
|
+
%(threadName)s
|
|
35
|
+
);
|
|
36
|
+
|
|
File without changes
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# vim: set expandtab ts=4 sw=4 filetype=python fileencoding=utf8:
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import logging.config
|
|
5
|
+
import os
|
|
6
|
+
import unittest
|
|
7
|
+
|
|
8
|
+
import logtopg
|
|
9
|
+
import psycopg2
|
|
10
|
+
|
|
11
|
+
testing_dict_config = dict({
|
|
12
|
+
|
|
13
|
+
"loggers": {
|
|
14
|
+
"logtopg": {
|
|
15
|
+
"handlers": ["pg"],
|
|
16
|
+
"level": "DEBUG",
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
'handlers': {
|
|
21
|
+
'pg': {
|
|
22
|
+
'class': 'logtopg.PGHandler',
|
|
23
|
+
'level': 'DEBUG',
|
|
24
|
+
'log_table_name': 'logtopg_tests',
|
|
25
|
+
"database":"logtopg_tests",
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
"console": {
|
|
29
|
+
"class": "logging.StreamHandler",
|
|
30
|
+
"level": "DEBUG",
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
# 'root': {
|
|
36
|
+
# 'handlers': ["console"],
|
|
37
|
+
# 'level': 'DEBUG'},
|
|
38
|
+
|
|
39
|
+
'version': 1,
|
|
40
|
+
|
|
41
|
+
# This is important! Without it, any log instances created before
|
|
42
|
+
# you run logging.config.dictConfig(...) will be disabled.
|
|
43
|
+
'disable_existing_loggers': False,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
class Test1(unittest.TestCase):
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
This depends on a real postgresql database. I'll create a table and
|
|
50
|
+
then drop it.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
d = testing_dict_config
|
|
54
|
+
log_table_name = d["handlers"]["pg"]["log_table_name"]
|
|
55
|
+
database = d["handlers"]["pg"]["database"]
|
|
56
|
+
user = d["handlers"]["pg"].get("user")
|
|
57
|
+
password = d["handlers"]["pg"].get("password")
|
|
58
|
+
host = d["handlers"]["pg"].get("host")
|
|
59
|
+
|
|
60
|
+
db_credentials = dict(
|
|
61
|
+
user=user,
|
|
62
|
+
password=password,
|
|
63
|
+
host=host,
|
|
64
|
+
database=database,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def setUp(self):
|
|
68
|
+
|
|
69
|
+
logging.config.dictConfig(self.d)
|
|
70
|
+
|
|
71
|
+
self.log = logging.getLogger("logtopg.tests")
|
|
72
|
+
|
|
73
|
+
self.ltpg = logtopg.PGHandler(
|
|
74
|
+
self.log_table_name,
|
|
75
|
+
self.user,
|
|
76
|
+
self.password,
|
|
77
|
+
self.host,
|
|
78
|
+
self.database)
|
|
79
|
+
|
|
80
|
+
# Make a separate database connection to check results in
|
|
81
|
+
# database.
|
|
82
|
+
self.test_pgconn = psycopg2.connect(**self.db_credentials)
|
|
83
|
+
|
|
84
|
+
def test_1(self):
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
Verify we only read sql files once each.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
self.assertTrue(self.ltpg.create_table_sql is None)
|
|
91
|
+
|
|
92
|
+
s1 = self.ltpg.get_create_table_sql()
|
|
93
|
+
|
|
94
|
+
self.assertTrue(isinstance(self.ltpg.create_table_sql, bytes))
|
|
95
|
+
|
|
96
|
+
s2 = self.ltpg.get_create_table_sql()
|
|
97
|
+
|
|
98
|
+
self.assertTrue(s1 is s2)
|
|
99
|
+
|
|
100
|
+
self.ltpg.get_insert_row_sql()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_2(self):
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
Verify we make only one database connection in an instance.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
ltpg = logtopg.PGHandler(
|
|
110
|
+
self.log_table_name,
|
|
111
|
+
**self.db_credentials)
|
|
112
|
+
|
|
113
|
+
self.assertTrue(ltpg.pgconn is None)
|
|
114
|
+
|
|
115
|
+
conn1 = ltpg.get_pgconn()
|
|
116
|
+
|
|
117
|
+
self.assertTrue(ltpg.pgconn)
|
|
118
|
+
|
|
119
|
+
conn2 = ltpg.get_pgconn()
|
|
120
|
+
|
|
121
|
+
self.assertTrue(conn1 is conn2)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_3(self):
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
Verify we can create the log table.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
ltpg = logtopg.PGHandler(
|
|
131
|
+
self.log_table_name,
|
|
132
|
+
**self.db_credentials)
|
|
133
|
+
|
|
134
|
+
ltpg.maybe_create_table()
|
|
135
|
+
|
|
136
|
+
# Now, verify the table exists.
|
|
137
|
+
cursor = ltpg.pgconn.cursor()
|
|
138
|
+
|
|
139
|
+
cursor.execute("""
|
|
140
|
+
select exists(
|
|
141
|
+
select *
|
|
142
|
+
from information_schema.tables
|
|
143
|
+
where table_name = %s)
|
|
144
|
+
""", [self.log_table_name])
|
|
145
|
+
|
|
146
|
+
row = cursor.fetchone()
|
|
147
|
+
|
|
148
|
+
self.assertTrue(row[0], )
|
|
149
|
+
|
|
150
|
+
# Subsequent calls to maybe_create_table should be harmless and
|
|
151
|
+
# nearly instantaneous.
|
|
152
|
+
ltpg.maybe_create_table()
|
|
153
|
+
ltpg.maybe_create_table()
|
|
154
|
+
ltpg.maybe_create_table()
|
|
155
|
+
|
|
156
|
+
ltpg.pgconn.rollback()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_4(self):
|
|
160
|
+
|
|
161
|
+
"""
|
|
162
|
+
Verify log messages are stored in the database.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
logging.config.dictConfig(self.d)
|
|
166
|
+
|
|
167
|
+
log1 = logging.getLogger("logtopg.tests")
|
|
168
|
+
log2 = logging.getLogger("logtopg.tests")
|
|
169
|
+
log3 = logging.getLogger("logtopg.tests")
|
|
170
|
+
log4 = logging.getLogger("logtopg.tests")
|
|
171
|
+
|
|
172
|
+
log = logging.getLogger("logtopg.tests")
|
|
173
|
+
|
|
174
|
+
log.debug("debug!")
|
|
175
|
+
log.info("info!")
|
|
176
|
+
log.warning("warning!")
|
|
177
|
+
log.error("error!")
|
|
178
|
+
log.critical("critical!")
|
|
179
|
+
|
|
180
|
+
# Now check that those logs are actually in the database.
|
|
181
|
+
cursor = self.test_pgconn.cursor()
|
|
182
|
+
|
|
183
|
+
cursor.execute(
|
|
184
|
+
"""
|
|
185
|
+
select message
|
|
186
|
+
from {}
|
|
187
|
+
where process_id = %s
|
|
188
|
+
""".format(self.log_table_name), [os.getpid()])
|
|
189
|
+
|
|
190
|
+
counted_rows = cursor.rowcount
|
|
191
|
+
|
|
192
|
+
self.test_pgconn.rollback()
|
|
193
|
+
|
|
194
|
+
# There should be 7 logs in the database with this process's ID.
|
|
195
|
+
# Those 7 are the five above and the two connection logs.
|
|
196
|
+
self.assertEqual(counted_rows, 7)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_5(self):
|
|
200
|
+
|
|
201
|
+
"""
|
|
202
|
+
Verify different logger instances use a single database
|
|
203
|
+
connection.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
logging.config.dictConfig(self.d)
|
|
207
|
+
|
|
208
|
+
log1 = logging.getLogger("logtopg.tests.a")
|
|
209
|
+
log1.debug("trying this guy out")
|
|
210
|
+
|
|
211
|
+
log2 = logging.getLogger("logtopg.tests.b")
|
|
212
|
+
log2.debug("trying this guy out")
|
|
213
|
+
|
|
214
|
+
def test_6(self):
|
|
215
|
+
|
|
216
|
+
"""
|
|
217
|
+
Log an exception to the database.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
logging.config.dictConfig(self.d)
|
|
221
|
+
log = logging.getLogger("logtopg.tests.tests_6")
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
|
|
225
|
+
1/0
|
|
226
|
+
|
|
227
|
+
except Exception as ex:
|
|
228
|
+
|
|
229
|
+
log.exception(ex)
|
|
230
|
+
|
|
231
|
+
log.debug(AttributeError("This is a bogus exception"))
|
|
232
|
+
|
|
233
|
+
def test_7(self):
|
|
234
|
+
|
|
235
|
+
"""
|
|
236
|
+
Log something that can't be adapted to the database.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
logging.config.dictConfig(self.d)
|
|
240
|
+
log = logging.getLogger("logtopg.tests.tests_7")
|
|
241
|
+
|
|
242
|
+
class Unadaptable(object):
|
|
243
|
+
pass
|
|
244
|
+
|
|
245
|
+
u = Unadaptable()
|
|
246
|
+
|
|
247
|
+
log.debug("u is a {0}.".format(u))
|
|
248
|
+
log.debug(u)
|
|
249
|
+
log.debug(dict(u=u))
|
|
250
|
+
|
|
251
|
+
def tearDown(self):
|
|
252
|
+
|
|
253
|
+
self.test_pgconn.rollback()
|
|
254
|
+
|
|
255
|
+
cursor = self.test_pgconn.cursor()
|
|
256
|
+
|
|
257
|
+
cursor.execute(
|
|
258
|
+
"drop table if exists {0}".format(
|
|
259
|
+
Test1.log_table_name))
|
|
260
|
+
|
|
261
|
+
self.test_pgconn.commit()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def tearDownModule():
|
|
265
|
+
|
|
266
|
+
pgconn = psycopg2.connect(**Test1.db_credentials)
|
|
267
|
+
|
|
268
|
+
cursor = pgconn.cursor()
|
|
269
|
+
|
|
270
|
+
cursor.execute(
|
|
271
|
+
"drop table if exists {0}".format(
|
|
272
|
+
Test1.log_table_name))
|
|
273
|
+
|
|
274
|
+
pgconn.commit()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
unittest.main()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# vim: set expandtab ts=4 sw=4 filetype=python fileencoding=utf8:
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Do not do anything in this file except define the __version__ variable!
|
|
5
|
+
|
|
6
|
+
The setup.py script reads this version from here during install.
|
|
7
|
+
|
|
8
|
+
In the past, I've defined the version in the setup.py file (A), or
|
|
9
|
+
defined it in the top of the project, like in logtopg/__init__.py (B).
|
|
10
|
+
|
|
11
|
+
Choice A is bad because it isn't easy to fire up a python session and
|
|
12
|
+
then do::
|
|
13
|
+
|
|
14
|
+
>>> import logtopg
|
|
15
|
+
>>> logtopg.__version__ # doctest: +SKIP
|
|
16
|
+
|
|
17
|
+
to look up the version.
|
|
18
|
+
|
|
19
|
+
And choice B is bad because the logtopg/__init__.py file might blow up
|
|
20
|
+
during install because it tries to import a some third-party module that
|
|
21
|
+
hasn't been imported yet.
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: logtopg
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python logging handler that stores logs in postgresql
|
|
5
|
+
Home-page: https://github.com/216software/logtopg/
|
|
6
|
+
Author: 216 Software, LLC
|
|
7
|
+
Author-email: info@216software.com
|
|
8
|
+
License: BSD License
|
|
9
|
+
Platform: UNKNOWN
|
|
10
|
+
|
|
11
|
+
UNKNOWN
|
|
12
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.rst
|
|
3
|
+
setup.py
|
|
4
|
+
logtopg/__init__.py
|
|
5
|
+
logtopg/createtable.sql
|
|
6
|
+
logtopg/insertrow.sql
|
|
7
|
+
logtopg/version.py
|
|
8
|
+
logtopg.egg-info/PKG-INFO
|
|
9
|
+
logtopg.egg-info/SOURCES.txt
|
|
10
|
+
logtopg.egg-info/dependency_links.txt
|
|
11
|
+
logtopg.egg-info/requires.txt
|
|
12
|
+
logtopg.egg-info/top_level.txt
|
|
13
|
+
logtopg/tests/__init__.py
|
|
14
|
+
logtopg/tests/test_logtopg.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
psycopg2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
logtopg
|
logtopg-1.0.0/setup.cfg
ADDED
logtopg-1.0.0/setup.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# vim: set expandtab ts=4 sw=4 filetype=python fileencoding=utf8:
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
if sys.version_info < (2, 7):
|
|
6
|
+
raise Exception("sorry, this needs at least python 2.7!")
|
|
7
|
+
|
|
8
|
+
# Read __version__ from version.py
|
|
9
|
+
with open("logtopg/version.py") as f:
|
|
10
|
+
exec(f.read())
|
|
11
|
+
|
|
12
|
+
from setuptools import find_packages, setup
|
|
13
|
+
|
|
14
|
+
setup(
|
|
15
|
+
# name="LogToPG",
|
|
16
|
+
name="logtopg",
|
|
17
|
+
version=__version__,
|
|
18
|
+
description="Python logging handler that stores logs in postgresql",
|
|
19
|
+
url="https://github.com/216software/logtopg/",
|
|
20
|
+
packages=find_packages(),
|
|
21
|
+
|
|
22
|
+
author="216 Software, LLC",
|
|
23
|
+
author_email="info@216software.com",
|
|
24
|
+
license="BSD License",
|
|
25
|
+
include_package_data=True,
|
|
26
|
+
|
|
27
|
+
install_requires=[
|
|
28
|
+
'psycopg2',
|
|
29
|
+
],
|
|
30
|
+
|
|
31
|
+
test_suite="nose.collector",
|
|
32
|
+
use_2to3=True,
|
|
33
|
+
)
|