├── LICENSE ├── README.md └── pg_metricus.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Avito Technology 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PG Metricus 2 | 3 | ### Info 4 | 5 | pg_metricus is a script written in Python for sending metrics in the socket (Brubeck aggregator, Graphite, etc.) from NOTIFY channel. 6 | 7 | It must be remembered, if a NOTIFY is executed inside a transaction, the notify events are not delivered until and unless the transaction is committed. This is appropriate, since if the transaction is aborted, all the commands within it have had no effect, including NOTIFY. But it can be disconcerting if one is expecting the notification events to be delivered immediately. 8 | 9 | If the same channel name is signaled multiple times from the same transaction with identical payload strings, the database server can decide to deliver a single notification only. On the other hand, notifications with distinct payload strings will always be delivered as distinct notifications. Similarly, notifications from different transactions will never get folded into one notification. Except for dropping later instances of duplicate notifications, NOTIFY guarantees that notifications from the same transaction get delivered in the order they were sent. It is also guaranteed that messages from different transactions are delivered in the order in which the transactions committed. 10 | 11 | ### Usage 12 | 13 | Optional arguments: 14 | ``` 15 | pg_metricus.py [-h] [-H PG_HOST] [-P PG_PORT] [-D DBNAME] [-U USERNAME] 16 | [-W PASSWORD] -A SOCKET_HOST -X SOCKET_PORT -C CHANNEL 17 | [-B] [-E] 18 | 19 | -H PG_HOST, --pg_host PG_HOST 20 | -P PG_PORT, --pg_port PG_PORT 21 | -D DBNAME, --dbname DBNAME 22 | -U USERNAME, --username USERNAME 23 | -W PASSWORD, --password PASSWORD 24 | -A SOCKET_HOST, --socket_host SOCKET_HOST 25 | -X SOCKET_PORT, --socket_port SOCKET_PORT 26 | -C CHANNEL, --channel CHANNEL 27 | -B, --start_listen 28 | -E, --stop_listen 29 | ``` 30 | 31 | Crontab 32 | ``` 33 | * * * * * pg_metricus.py -H 127.0.0.1 -P 5432 -D base -U user -W pass -A 10.9.5.164 -X 8124 -C test 34 | ``` 35 | 36 | ### Format 37 | 38 | For Brubeck aggregator: 39 | ```plpgsql 40 | select pg_notify('test', format(E'%s.%s:%s|%s\n', 41 | metric_path, 42 | metric_name, 43 | metric_value, 44 | metric_type 45 | )); 46 | ``` 47 | 48 | For Graphite: 49 | ```plpgsql 50 | select pg_notify('test', format(E'%s.%s %s %s \n', 51 | metric_path, 52 | metric_name, 53 | metric_value, 54 | extract(epoch from now())::integer 55 | )); 56 | ``` 57 | 58 | ### Example 59 | 60 | ```plpgsql 61 | do language plpgsql $$ 62 | declare 63 | x1 timestamp; 64 | x2 timestamp; 65 | v_val_hstore text; 66 | begin 67 | 68 | x1 = clock_timestamp(); 69 | 70 | v_val_hstore = get_val_hstore(); 71 | 72 | x2 = clock_timestamp(); 73 | 74 | perform pg_notify('test', format(E'%s.%s:%s|%s\n', 75 | 'db.sql.metric', 76 | 'get_val_hstore_duration', 77 | extract(millisecond from (x2 - x1))::bigint::text, 78 | 'ms' 79 | )); 80 | 81 | end 82 | $$; 83 | ``` 84 | 85 | ### Author 86 | 87 | Nikolay Vorobev (nvorobev@avito.ru) 88 | 89 | ### License 90 | 91 | MIT 92 | -------------------------------------------------------------------------------- /pg_metricus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # author: @nvorobev 4 | 5 | import psycopg2 6 | import select 7 | import os 8 | import sys 9 | import time 10 | import fcntl 11 | import argparse 12 | import getpass 13 | from socket import socket, AF_INET, SOCK_DGRAM 14 | 15 | def checkStop(): 16 | return os.path.isfile(STOP_FILE) 17 | 18 | if __name__ == '__main__': 19 | 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument('-H', '--pg_host', type=str, action="store", default='localhost') 22 | parser.add_argument('-P', '--pg_port', type=int, action="store", default=5432) 23 | parser.add_argument('-D', '--dbname', type=str, action="store", default='postgres') 24 | parser.add_argument('-U', '--username', type=str, action="store", default=os.getenv('PGUSER', getpass.getuser())) 25 | parser.add_argument('-W', '--password', type=str, action="store", default=os.getenv('PGPASSWORD')) 26 | parser.add_argument('-A', '--socket_host', type=str, action="store", required=True) 27 | parser.add_argument('-X', '--socket_port', type=int, action="store", required=True) 28 | parser.add_argument('-C', '--channel', type=str, action="store", required=True) 29 | parser.add_argument('-B', '--start_listen', action = 'store_true') 30 | parser.add_argument('-E', '--stop_listen', action = 'store_true') 31 | args = parser.parse_args() 32 | 33 | STOP_FILE = '/var/tmp/pg_metricus.%s.stop' % args.channel 34 | LOCK_FILE = '/var/tmp/pg_metricus.%s.lock' % args.channel 35 | 36 | if args.start_listen and args.stop_listen: 37 | sys.exit(0) 38 | 39 | if args.start_listen: 40 | if os.path.isfile(STOP_FILE): 41 | os.unlink(STOP_FILE) 42 | sys.exit(0) 43 | 44 | if args.stop_listen: 45 | open(STOP_FILE, 'w').close() 46 | sys.exit(0) 47 | 48 | if checkStop(): 49 | sys.exit(0) 50 | 51 | lock_fd = os.open(LOCK_FILE, os.O_WRONLY | os.O_CREAT, 0644) 52 | st = fcntl.fcntl(lock_fd, fcntl.F_GETFD) 53 | fcntl.fcntl(lock_fd, fcntl.F_SETFD, st | fcntl.FD_CLOEXEC) 54 | 55 | try: 56 | fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 57 | except IOError as e: 58 | if e.errno != os.errno.EAGAIN: 59 | raise 60 | sys.exit(1) 61 | 62 | os.ftruncate(lock_fd, 0) 63 | os.write(lock_fd, "%s %s\n" % (os.getpid(), time.ctime())) 64 | os.fsync(lock_fd) 65 | 66 | connect_string = 'host={host} port={port} dbname={dbname} user={user} connect_timeout=5'.format( 67 | host=args.pg_host, port=args.pg_port, dbname=args.dbname, user=args.username 68 | ) 69 | 70 | try: 71 | conn = psycopg2.connect(connect_string + (' password=' + args.password if args.password else '')) 72 | conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) 73 | 74 | curs = conn.cursor() 75 | curs.execute("LISTEN {0};".format(args.channel)) 76 | 77 | addr = (args.socket_host, args.socket_port) 78 | sock = socket(AF_INET, SOCK_DGRAM) 79 | 80 | while not checkStop(): 81 | if select.select([conn],[],[],5) != ([],[],[]): 82 | conn.poll() 83 | while conn.notifies: 84 | notification = conn.notifies.pop(0) 85 | sock.sendto(notification.payload.encode('utf-8'), addr) 86 | except Exception, e: 87 | sys.stdout.write("\nerror: %s\n" % e.message) 88 | finally: 89 | sock.close() 90 | conn.close() 91 | --------------------------------------------------------------------------------