logtopg 1.0.2__py3-none-any.whl

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/__init__.py ADDED
@@ -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_{0}_cmdline ON {0} USING GIN (cmd_line gin_trgm_ops);
logtopg/insertrow.sql ADDED
@@ -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()
logtopg/version.py ADDED
@@ -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.2"
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: logtopg
3
+ Version: 1.0.2
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
+ Requires-Dist: psycopg2
10
+ Dynamic: author
11
+ Dynamic: author-email
12
+ Dynamic: home-page
13
+ Dynamic: license
14
+ Dynamic: requires-dist
15
+ Dynamic: summary
@@ -0,0 +1,10 @@
1
+ logtopg/__init__.py,sha256=wEvVz_YRJcR3wTvFGQM59BtWZdEXsV2pxp9IUwBv2So,6283
2
+ logtopg/createtable.sql,sha256=qilEAG8B371vqGacO9Tu1v5v-dhQZ_K5LNW1Tn-zywA,744
3
+ logtopg/insertrow.sql,sha256=mr-3kVSMxDNfMpfGj0k2utewK81zNQtmyIlvQslKW8M,577
4
+ logtopg/version.py,sha256=AwDmPU8h5HA6c_izak87tlXvUQi9Z3dT5g5N7bvBhF0,722
5
+ logtopg/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ logtopg/tests/test_logtopg.py,sha256=rVeM-o9Ggi9CpNb5v0ovhBBoCGj-pxwrW4fG8oCzi54,6259
7
+ logtopg-1.0.2.dist-info/METADATA,sha256=MYkXymE_KyckIpr8KzGun4xjyt5qtM2MR5XAlX4TyFc,385
8
+ logtopg-1.0.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
+ logtopg-1.0.2.dist-info/top_level.txt,sha256=56AgV79tf5I8ve-ns3cXFYYFbHtjh6IbSA-mfEhvPS4,8
10
+ logtopg-1.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ logtopg