├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md └── pg_tail.c /.gitignore: -------------------------------------------------------------------------------- 1 | pg_tail 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.7 2 | 3 | ENV DIR /tmp/pg_tail/ 4 | ADD * $DIR 5 | RUN apk update \ 6 | && apk add postgresql-libs \ 7 | && apk add --virtual .build-deps curl build-base postgresql-dev \ 8 | && cd $DIR && make && make install \ 9 | && apk del .build-deps \ 10 | && rm -rf $DIR /var/cache/apk 11 | 12 | ENTRYPOINT ["pg_tail"] 13 | CMD ["--help"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2013 Andre Parmeggiani 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/sh 2 | 3 | pgc := $(shell which pg_config) 4 | inc := $(shell $(pgc) --includedir) 5 | lib := $(shell $(pgc) --libdir) 6 | 7 | INSTALL=/usr/local/bin 8 | INC=-I$(inc) 9 | LIB=-L$(lib) 10 | CFLAGS=-Wall -O2 11 | LDFLAGS=-lpq 12 | 13 | all: pg_tail 14 | 15 | clean: 16 | rm -f pg_tail 17 | 18 | pg_tail: pg_tail.c 19 | $(CC) $(CFLAGS) $(INC) $(LIB) -o pg_tail pg_tail.c $(LDFLAGS) 20 | 21 | install: pg_tail 22 | cp pg_tail $(INSTALL) 23 | 24 | uninstall: 25 | rm $(INSTALL)/pg_tail 26 | 27 | homebrew: pg_tail 28 | cp pg_tail $(PREFIX) 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## pg_tail 2 | 3 | Watch last lines of a PostgreSQL table like in a "tail -f". 4 | ( inspired by [mysqltail](http://www.vanheusden.com/mysqltail/) ) 5 | 6 | ## Install 7 | 8 | #### macOS 9 | ```console 10 | brew install aaparmeggiani/tap/pg_tail 11 | ``` 12 | 13 | #### *nix 14 | (requires PostgreSQL, pg_config, libpq) 15 | ```console 16 | git clone https://github.com/aaparmeggiani/pg_tail.git 17 | cd pg_tail 18 | make 19 | make install 20 | ``` 21 | 22 | ## Usage 23 | ```console 24 | % pg_tail [OPTIONS] -t TABLE -c uKEY[,COL1,..,COLn] 25 | 26 | Options: 27 | -d, --dbname=DBNAME database to connect to 28 | -h, --host=HOSTNAME database server host or socket directory 29 | -p, --port=PORT database server port number 30 | -U, --username=NAME connect as specified database user 31 | -W, --password prompts for the user password 32 | 33 | 34 | -t, --table=TABLE table to watch 35 | -c, --columns=COL1..COLn columns to watch, the first one must be an ordered primary key (sequence) 36 | -i, --interval=SECONDS database polling interval in seconds (default: 10) 37 | -s, --separator=CHAR sets a column delimiter (an no column alignment) 38 | -n NUM number of lines in the first polling (default: 5) 39 | -j, --json output as json 40 | -v, --version version info 41 | ``` 42 | 43 | Options can also be passed through the following `PG` / `PGTAIL` env variables: 44 | 45 | ``` 46 | PGDATABASE, PGHOST, PGPORT, PGUSER 47 | 48 | PGTAILTABLE, PGTAILKEY, PGTAILCOLUMNS, PGTAILSEPARATOR, PGTAILINTERVAL, PGTAILLINES, PGTAILALIGN, PGTAILJSON 49 | ``` 50 | 51 | ## Example 52 | 53 | ```console 54 | % pg_tail -d database -t users -c id,login,email 55 | id | login | email | 56 | 1 | system | system@example.com | 57 | 2 | global | global@example.com | 58 | 3 | teller | teller@example.com | 59 | ``` 60 | 61 | ## Dockerized 62 | [![Docker Automated Status](https://img.shields.io/docker/cloud/automated/aaparmeggiani/pg_tail.svg)]() 63 | [![Docker Build Status](https://img.shields.io/docker/cloud/build/aaparmeggiani/pg_tail.svg)]() 64 | ``` 65 | docker run -it --init --rm aaparmeggiani/pg_tail --help 66 | ``` 67 | (_don't forget **--init** or you might find yourself trapped without ctrl-c_ =) 68 | 69 | ## License 70 | MIT 71 | 72 | -------------------------------------------------------------------------------- /pg_tail.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define VERSION "0.10" 10 | #define INTERVAL 10 /* default polling interval in seconds*/ 11 | #define LINES 5 /* default number of lines in the first poll */ 12 | #define SEPARATOR " | " /* default column delimiter */ 13 | 14 | #define MAX(n1, n2) ((n1) > (n2) ? (n1) : (n2)) 15 | 16 | static void help(void) { 17 | printf("\npg_tail - watches the last records of a PostgreSQL table.\n\n"); 18 | printf("Usage:\n"); 19 | printf(" pg_tail [OPTIONS] -t TABLE -c uKEY[,COL1,..,COLn]\n"); 20 | 21 | printf("\nOptions:\n"); 22 | printf(" -d, --dbname=DBNAME database to connect to\n"); 23 | printf(" -h, --host=HOSTNAME database server host or socket directory\n"); 24 | printf(" -p, --port=PORT database server port number\n"); 25 | printf(" -U, --username=NAME connect as specified database user\n"); 26 | printf(" -W, --password prompts for the user password\n\n"); 27 | 28 | printf(" -t, --table=TABLE table to watch\n"); 29 | printf(" -c, --columns=COL1..COLn columns to watch, the first one must be an ordered primary key (sequence)\n"); 30 | printf(" -i, --interval=SECONDS database polling interval in seconds (default: %d)\n", INTERVAL); 31 | printf(" -s, --separator=CHAR sets a column delimiter (an no column alignment) \n"); 32 | printf(" -n NUM number of lines in the first poll (default: %d)\n", LINES); 33 | printf(" -j, --json output as json\n"); 34 | printf(" -v, --version version info\n\n"); 35 | } 36 | 37 | static void exit_nicely(PGconn *conn) { 38 | if(conn) { PQfinish(conn); } 39 | exit(1); 40 | } 41 | 42 | int main(int argc, char **argv) 43 | { 44 | PGconn *conn; 45 | PGresult *res; 46 | const char *op_dbname = getenv("PGDATABASE"); 47 | const char *op_pghost = getenv("PGHOST"); 48 | const char *op_pgport = getenv("PGPORT"); 49 | const char *op_username = getenv("PGUSER"); 50 | const char *op_table = getenv("PGTAILTABLE"); 51 | const char *op_key = getenv("PGTAILKEY"); 52 | const char *op_columns = getenv("PGTAILCOLUMNS"); 53 | const char *op_separator = getenv("PGTAILSEPARATOR") ? getenv("PGTAILSEPARATOR") : SEPARATOR; 54 | int op_interval = getenv("PGTAILINTERVAL") ? atoi(getenv("PGTAILINTERVAL")) : INTERVAL; 55 | int op_n = getenv("PGTAILLINES") ? atoi(getenv("PGTAILLINES")) : LINES; 56 | int op_align = getenv("PGTAILALIGN") ? atoi(getenv("PGTAILALIGN")) : 1; 57 | int op_json = getenv("PGTAILJSON") ? atoi(getenv("PGTAILJSON")) : 0; 58 | 59 | char *current_key = NULL; 60 | char query[2000] = {}; 61 | char *password = getenv("PGPASSWORD"); 62 | int col_length[500] = {}; 63 | int num_rows = 0; 64 | int num_fields = 0; 65 | int i,j,c; 66 | int optindex; 67 | 68 | static struct option long_options[] = { 69 | {"dbname", required_argument, NULL, 'd'}, 70 | {"host", required_argument, NULL, 'h'}, 71 | {"port", required_argument, NULL, 'p'}, 72 | {"username", required_argument, NULL, 'U'}, 73 | {"password", no_argument, NULL, 'W'}, 74 | {"table", required_argument, NULL, 't'}, 75 | {"columns", required_argument, NULL, 'c'}, 76 | {"separator", required_argument, NULL, 's'}, 77 | {"interval", required_argument, NULL, 'i'}, 78 | {"json", no_argument, NULL, 'j'}, 79 | {"version", no_argument, NULL, 'v'}, 80 | {NULL, 0, NULL, 0} 81 | }; 82 | 83 | if ( argc <= 1 ) { 84 | help(); 85 | exit_nicely(0); 86 | } 87 | else if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-?") == 0) { 88 | help(); 89 | exit_nicely(0); 90 | } 91 | 92 | while ((c = getopt_long(argc, argv, "d:h:p:t:c:i:s:n:U:Wjv", long_options, &optindex)) != -1) { 93 | 94 | switch (c) { 95 | 96 | case 'd': 97 | op_dbname = strdup(optarg); 98 | break; 99 | 100 | case 'h': 101 | op_pghost = strdup(optarg); 102 | break; 103 | 104 | case 'p': 105 | op_pgport = strdup(optarg); 106 | break; 107 | 108 | case 't': 109 | op_table = strdup(optarg); 110 | break; 111 | 112 | case 'c': 113 | op_columns = strdup(optarg); 114 | op_key = strtok(strdup(op_columns), ","); 115 | break; 116 | 117 | case 'i': 118 | op_interval = atoi(optarg); 119 | break; 120 | 121 | case 's': 122 | op_separator = strdup(optarg); 123 | op_align = 0; 124 | break; 125 | 126 | case 'n': 127 | op_n = MAX(1, atoi(optarg)); 128 | break; 129 | 130 | case 'U': 131 | op_username = strdup(optarg); 132 | break; 133 | 134 | case 'W': 135 | password = getpass("Password: "); 136 | break; 137 | 138 | case 'j': 139 | op_json = 1; 140 | break; 141 | 142 | case 'v': 143 | printf("%s\n", VERSION); 144 | exit_nicely(0); 145 | break; 146 | 147 | default: 148 | fprintf(stderr, "Try pg_tail --help for more information.\n"); 149 | exit_nicely(0); 150 | } 151 | } 152 | 153 | if(!op_table || !op_columns) { 154 | fprintf(stderr, "Missing table or key (column).\n"); 155 | exit_nicely(0); 156 | } 157 | conn = PQsetdbLogin(op_pghost, op_pgport, NULL, NULL, op_dbname, op_username, password); 158 | if(password){ memset(password, 0, strlen(password)); } 159 | 160 | if (PQstatus(conn) != CONNECTION_OK) { 161 | fprintf(stderr, "Connection to database failed.\n%s\n", PQerrorMessage(conn)); 162 | exit_nicely(conn); 163 | } 164 | 165 | while(1) { 166 | 167 | if(current_key) 168 | snprintf(query, sizeof(query), 169 | "SELECT %s,row_to_json(row(%s)) FROM %s WHERE %s > '%s' ORDER BY %s ASC", 170 | op_columns, op_columns, op_table, op_key, current_key, op_key); 171 | else 172 | snprintf(query, sizeof(query), 173 | "SELECT * FROM (SELECT %s,row_to_json(row(%s)) FROM %s ORDER BY %s DESC LIMIT %d) AS tmp ORDER BY %s ASC", 174 | op_columns, op_columns, op_table, op_key, op_n, op_key); 175 | 176 | res = PQexec(conn, query); 177 | if(PQresultStatus(res) != PGRES_TUPLES_OK) { 178 | fprintf(stderr, "%s", PQerrorMessage(conn)); 179 | PQclear(res); 180 | exit_nicely(conn); 181 | } 182 | 183 | num_rows = PQntuples(res); 184 | num_fields = PQnfields(res); 185 | 186 | /* columns sizes */ 187 | for (i = 0; i < num_rows; i++) 188 | for (j = 0; j < num_fields; j++) { 189 | col_length[j] = MAX( col_length[j], strlen(PQfname(res, j)) ); 190 | col_length[j] = MAX( col_length[j], strlen(PQgetvalue(res, i, j)) ); 191 | } 192 | 193 | /* header if 1st lap */ 194 | if(!current_key && num_rows > 0 && !op_json) { 195 | for(i = 0; i < num_fields-1; i++) 196 | printf("%-*s%s", col_length[i], PQfname(res, i), op_separator); 197 | printf("\n"); 198 | fflush(stdout); 199 | } 200 | 201 | /* rows */ 202 | for (i = 0; i < num_rows; i++) { 203 | if(op_json){ 204 | printf("%s", PQgetvalue(res, i, num_fields-1)); 205 | } 206 | else{ 207 | for (j = 0; j < num_fields-1; j++) 208 | printf("%-*s%s", (op_align * col_length[j]), PQgetvalue(res, i, j), op_separator); 209 | } 210 | printf("\n"); 211 | fflush(stdout); 212 | } 213 | 214 | if(num_rows > 0) { 215 | free(current_key); 216 | current_key = strdup(PQgetvalue(res, i-1, 0)); 217 | } 218 | 219 | PQclear(res); 220 | sleep(op_interval); 221 | } 222 | 223 | } 224 | --------------------------------------------------------------------------------