1#! /usr/bin/php -qC
2<?php
3
4/*
5 * This file is part of pgFouine.
6 *
7 * pgFouine - a PostgreSQL log analyzer
8 * Copyright (c) 2005-2008 Guillaume Smet
9 *
10 * pgFouine is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or (at
13 * your option) any later version.
14 *
15 * pgFouine is distributed in the hope that it will be useful, but
16 * WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18 * General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with pgFouine; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA  02110-1301, USA.
23 */
24
25ini_set('max_execution_time', 18000);
26ini_set('log_errors', true);
27ini_set('display_errors', false);
28
29if(strpos(phpversion(), '4.4') === 0) {
30	error_reporting(E_ALL - E_NOTICE);
31} else {
32	error_reporting(E_ALL);
33}
34
35ini_set('include_path', '!!PREFIX!!/share/pgfouine');
36
37include('version.php');
38require_once('include/lib/common.lib.php');
39require_once('include/base.lib.php');
40require_once('include/listeners/listeners.lib.php');
41require_once('include/postgresql/postgresql.lib.php');
42require_once('include/reporting/reports.lib.php');
43
44$stderr = fopen('php://stderr', 'w');
45
46function usage($error = false) {
47	if($error) {
48		stderr('Error: '.$error);
49		echo "\n";
50	}
51	echo 'Usage: '.$GLOBALS['executable'].' -file <file> [-top <n>] [-format <format>] [-logtype <logtype>] [-report [outputfile=]<block1,block2>]
52  -file <file>                           log file to analyze
53  -                                      read the log from stdin instead of -file
54  -top <n>                               number of queries in lists. Default is 20.
55  -format <format>                       output format: html, html-with-graphs or text. Default is html.
56  -logtype <logtype>                     log type: syslog, stderr or csvlog. Default is syslog.
57                                          for stderr, you have to use the following log_line_prefix: \'%t [%p]: [%l-1] \'
58  -report [outputfile=]<block1,block2>   list of report blocks separated by a comma
59                                         report blocks can be: overall, hourly, bytype, slowest, n-mosttime,
60                                          n-mostfrequent, n-slowestaverage, history, n-mostfrequenterrors,
61                                          tsung, csv-query
62                                         you can add several -report options if you want to generate several reports at once
63  -examples <n>                          maximum number of examples for a normalized query
64  -onlyselect                            ignore all queries but SELECT
65  -from "<date>"                         ignore lines logged before this date (uses strtotime)
66  -to "<date>"                           ignore lines logged after this date (uses strtotime)
67  -database <database>                   consider only queries on this database
68                                         (supports db1,db2 and /regexp/)
69  -user <user>                           consider only queries executed by this user
70                                         (supports user1,user2 and /regexp/)
71  -keepformatting                        keep the formatting of the query
72  -maxquerylength <length>               maximum length of a query: we cut it if it exceeds this length
73  -durationunit <s|ms>                   unit used to display the durations. Default is s(econds).
74  -title <title>                         define the title of the reports
75  -syslogident <ident>                   PostgreSQL syslog identity. Default is postgres.
76  -memorylimit <n>                       PHP memory limit in MB. Default is 512.
77  -quiet                                 quiet mode
78  -debug                                 debug mode
79  -profile                               profile mode
80  -help                                  this help
81';
82	if($error) {
83		exit(1);
84	} else {
85		exit(0);
86	}
87}
88
89function checkOutputFilePath($filePath) {
90	if(!$filePath) {
91		return false;
92	}
93
94	$tmpOutputFilePath = $filePath;
95	$tmpOutputDirectory = dirname($tmpOutputFilePath);
96	$tmpOutputFileName = basename($tmpOutputFilePath);
97
98	if(file_exists($tmpOutputFilePath) && (!is_file($tmpOutputFilePath) || !is_writable($tmpOutputFilePath))) {
99		usage($tmpOutputFilePath.' already exists and is not a file or is not writable');
100		return false;
101	} elseif(!is_dir($tmpOutputDirectory) || !is_writable($tmpOutputDirectory)) {
102		usage($tmpOutputDirectory.' is not a directory, does not exist or is not writable');
103		return false;
104	} elseif(!$tmpOutputFileName) {
105		usage('cannot find a valid basename in '.$tmpOutputFilePath);
106		return false;
107	} else {
108		$outputFilePath = realpath($tmpOutputDirectory).'/'.$tmpOutputFileName;
109		return $outputFilePath;
110	}
111}
112
113if(isset($_SERVER['argv']) && (!isset($argv) || empty($argv))) {
114	$argv = $_SERVER['argv'];
115}
116if(is_array($argv)) {
117	$executable = array_shift($argv);
118} else {
119	$argv = array();
120	$executable = 'unknown';
121}
122
123$options = array();
124$argvCount = count($argv);
125for($i = 0; $i < $argvCount; $i++) {
126	if(strpos($argv[$i], '-') === 0) {
127		if($argv[$i] == '-') {
128			define('CONFIG_STDIN', true);
129		} else {
130			$optionKey = substr($argv[$i], 1);
131			$value = false;
132			if(($i+1 < $argvCount) && (strpos($argv[$i+1], '-') !== 0)) {
133				$value = $argv[$i+1];
134				$i++;
135			}
136			if($optionKey == 'report' || $optionKey == 'reports') {
137				if(!isset($options['reports'])) {
138					$options['reports'] = array();
139				}
140				$options['reports'][] = $value;
141			} else {
142				$options[$optionKey] = $value;
143			}
144		}
145	} else {
146		usage('invalid options format');
147	}
148}
149
150if(isset($options['memorylimit']) && ((int) $options['memorylimit']) > 0) {
151	$memoryLimit = (int) $options['memorylimit'];
152} else {
153	$memoryLimit = 512;
154}
155ini_set('memory_limit', $memoryLimit.'M');
156
157if(!defined('CONFIG_STDIN')) {
158	define('CONFIG_STDIN', false);
159}
160
161if(isset($options['help']) || isset($options['h']) || isset($options['-help'])) {
162	usage();
163}
164
165if(isset($options['debug'])) {
166	define('DEBUG', 1);
167} else {
168	define('DEBUG', 0);
169}
170if(isset($options['profile'])) {
171	define('PROFILE', 1);
172} else {
173	define('PROFILE', 0);
174}
175
176define('CONFIG_FILTER', false);
177
178if(!CONFIG_STDIN) {
179	if(!isset($options['file'])) {
180		usage('the -file option is required');
181	} elseif(!$options['file']) {
182		usage('you have to specify a file path');
183	} elseif(!is_readable($options['file'])) {
184		usage('file '.$options['file'].' cannot be read');
185	} else {
186		$filePath = realpath($options['file']);
187	}
188} else {
189	$filePath = 'php://stdin';
190}
191
192if(isset($options['title'])) {
193	define('CONFIG_REPORT_TITLE', $options['title']);
194} else {
195	define('CONFIG_REPORT_TITLE', 'pgFouine: PostgreSQL log analysis report');
196}
197
198if(isset($options['top'])) {
199	if((int) $options['top'] > 0) {
200		$top = (int) $options['top'];
201	} else {
202		usage('top option should be a valid integer');
203	}
204} else {
205	$top = 20;
206}
207define('CONFIG_TOP_QUERIES_NUMBER', $top);
208
209$outputToFiles = false;
210$supportedReportBlocks = array(
211	'overall' => 'OverallStatsReport',
212	'bytype' => 'QueriesByTypeReport',
213	'hourly' => 'HourlyStatsReport',
214	'slowest' => 'SlowestQueriesReport',
215	'n-mosttime' => 'NormalizedQueriesMostTimeReport',
216	'n-mostfrequent' => 'NormalizedQueriesMostFrequentReport',
217	'n-slowestaverage' => 'NormalizedQueriesSlowestAverageReport',
218	'history' => 'QueriesHistoryReport',
219	'historyperpid' => 'QueriesHistoryPerPidReport',
220	'n-mostfrequenterrors' => 'NormalizedErrorsMostFrequentReport',
221	'tsung' => 'TsungSessionsReport',
222	'csv-query' => 'CsvQueriesHistoryReport'
223);
224$defaultReportBlocks = array('overall', 'bytype', 'n-mosttime', 'slowest', 'n-mostfrequent', 'n-slowestaverage');
225
226$reports = array();
227if(isset($options['reports'])) {
228	foreach($options['reports'] AS $report) {
229		if(strpos($report, '=') !== false) {
230			list($outputFilePath, $blocks) = explode('=', $report);
231			$outputToFiles = true;
232		} elseif(strpos($report, '.') !== false) {
233			$outputFilePath = $report;
234			$blocks = 'default';
235			$outputToFiles = true;
236		} else {
237			$outputFilePath = false;
238			$blocks = $report;
239			$outputToFiles = false;
240		}
241		if($blocks == 'default') {
242			$selectedBlocks = $defaultReportBlocks;
243			$notSupportedBlocks = array();
244		} elseif($blocks == 'all') {
245			$selectedBlocks = array_keys($supportedReportBlocks);
246			$notSupportedBlocks = array();
247		} else {
248			$selectedBlocks = explode(',', $blocks);
249			$notSupportedBlocks = array_diff($selectedBlocks, array_keys($supportedReportBlocks));
250		}
251
252		if(empty($notSupportedBlocks)) {
253			$outputFilePath = checkOutputFilePath($outputFilePath);
254			$reports[] = array(
255				'blocks' => $selectedBlocks,
256				'file' => $outputFilePath
257			);
258		} else {
259			usage('report types not supported: '.implode(',', $notSupportedBlocks));
260		}
261	}
262} else {
263	$reports[] = array(
264		'blocks' => $defaultReportBlocks,
265		'file' => false
266	);
267}
268
269$supportedFormats = array('text' => 'TextReportAggregator', 'html' => 'HtmlReportAggregator', 'html-with-graphs' => 'HtmlWithGraphsReportAggregator');
270if(isset($options['format'])) {
271	if(array_key_exists($options['format'], $supportedFormats)) {
272		if($options['format'] == 'html-with-graphs') {
273			if(!function_exists('imagegd2')) {
274				usage('HTML with graphs format requires GD2 library and extension');
275			}
276			if(!function_exists('imagettfbbox')) {
277				usage('HTML with graphs format requires Freetype support');
278			}
279			if(!$outputToFiles) {
280				usage('you need to define an output file to use HTML with graphs format (use -report outputfile=block1,block2,...)');
281			}
282		}
283		$aggregator = $supportedFormats[$options['format']];
284	} else {
285		usage('format not supported');
286	}
287} else {
288	$aggregator = $supportedFormats['html'];
289}
290
291$supportedLogTypes = array(
292	'syslog' => 'SyslogPostgreSQLParser',
293	'stderr' => 'StderrPostgreSQLParser',
294	'csvlog' => 'CsvlogPostgreSQLParser',
295);
296$logtype = '';
297if(isset($options['logtype'])) {
298	if(array_key_exists($options['logtype'], $supportedLogTypes)) {
299		$parser = $supportedLogTypes[$options['logtype']];
300		$logtype = $options['logtype'];
301	} else {
302		usage('log type not supported');
303	}
304} else {
305	$parser = $supportedLogTypes['syslog'];
306	$logtype = 'syslog';
307}
308
309if(isset($options['examples'])) {
310	$maxExamples = (int) $options['examples'];
311} else {
312	$maxExamples = 3;
313}
314define('CONFIG_MAX_NUMBER_OF_EXAMPLES', $maxExamples);
315
316if(isset($options['onlyselect'])) {
317	define('CONFIG_ONLY_SELECT', true);
318} else {
319	define('CONFIG_ONLY_SELECT', false);
320}
321
322if(isset($options['database']) && !empty($options['database'])) {
323	$options['database'] = trim($options['database']);
324	if(substr($options['database'], 0, 1) == '/' && substr($options['database'], -1, 1) == '/') {
325		// the filter is probably a regexp
326		if(@preg_match($options['database'], $value) === false) {
327			usage('database filter regexp is not valid');
328		} else {
329			define('CONFIG_DATABASE_REGEXP', $options['database']);
330		}
331	} elseif(strpos($options['database'], ',') !== false) {
332		// the filter is a list
333		$databases = explode(',', $options['database']);
334		$databases = array_map('trim', $databases);
335		define('CONFIG_DATABASE_LIST', implode(',', $databases));
336	} else {
337		define('CONFIG_DATABASE', $options['database']);
338	}
339}
340if(!defined('CONFIG_DATABASE')) define('CONFIG_DATABASE', false);
341if(!defined('CONFIG_DATABASE_LIST')) define('CONFIG_DATABASE_LIST', false);
342if(!defined('CONFIG_DATABASE_REGEXP')) define('CONFIG_DATABASE_REGEXP', false);
343
344if(isset($options['user']) && !empty($options['user'])) {
345	$options['user'] = trim($options['user']);
346	if(substr($options['user'], 0, 1) == '/' && substr($options['user'], -1, 1) == '/') {
347		// the filter is probably a regexp
348		if(@preg_match($options['user'], $value) === false) {
349			usage('user filter regexp is not valid');
350		} else {
351			define('CONFIG_USER_REGEXP', $options['user']);
352		}
353	} elseif(strpos($options['user'], ',') !== false) {
354		// the filter is a list
355		$users = explode(',', $options['user']);
356		$users = array_map('trim', $users);
357		define('CONFIG_USER_LIST', implode(',', $users));
358	} else {
359		define('CONFIG_USER', $options['user']);
360	}
361}
362if(!defined('CONFIG_USER')) define('CONFIG_USER', false);
363if(!defined('CONFIG_USER_LIST')) define('CONFIG_USER_LIST', false);
364if(!defined('CONFIG_USER_REGEXP')) define('CONFIG_USER_REGEXP', false);
365
366if(isset($options['keepformatting'])) {
367	define('CONFIG_KEEP_FORMATTING', true);
368} else {
369	define('CONFIG_KEEP_FORMATTING', false);
370}
371
372if(isset($options['maxquerylength']) && is_numeric($options['maxquerylength'])) {
373	define('CONFIG_MAX_QUERY_LENGTH', $options['maxquerylength']);
374} else {
375	define('CONFIG_MAX_QUERY_LENGTH', 0);
376}
377
378if(isset($options['durationunit']) && $options['durationunit'] == 'ms') {
379	define('CONFIG_DURATION_UNIT', 'ms');
380} else {
381	define('CONFIG_DURATION_UNIT', 's');
382}
383
384if(isset($options['from']) && !empty($options['from'])) {
385	$fromTimestamp = strtotime($options['from']);
386	if($fromTimestamp <= 0) {
387		$fromTimestamp = false;
388	}
389} else {
390	$fromTimestamp = false;
391}
392
393if(isset($options['to']) && !empty($options['to'])) {
394	$toTimestamp = strtotime($options['to']);
395	if($toTimestamp <= 0) {
396		$toTimestamp = false;
397	}
398} else {
399	$toTimestamp = false;
400}
401
402if($fromTimestamp || $toTimestamp) {
403	define('CONFIG_TIMESTAMP_FILTER', true);
404	if($fromTimestamp) {
405		define('CONFIG_FROM_TIMESTAMP', $fromTimestamp);
406	} else {
407		define('CONFIG_FROM_TIMESTAMP', MIN_TIMESTAMP);
408	}
409	if($toTimestamp) {
410		define('CONFIG_TO_TIMESTAMP', $toTimestamp);
411	} else {
412		define('CONFIG_TO_TIMESTAMP', MAX_TIMESTAMP);
413	}
414} else {
415	define('CONFIG_TIMESTAMP_FILTER', false);
416}
417
418if(isset($options['syslogident'])) {
419	define('CONFIG_SYSLOG_IDENTITY', $options['syslogident']);
420} else {
421	define('CONFIG_SYSLOG_IDENTITY', 'postgres');
422}
423
424if(isset($options['quiet'])) {
425	define('CONFIG_QUIET', true);
426} else {
427	define('CONFIG_QUIET', false);
428}
429
430if($logtype == 'csvlog') {
431	$logReader = new CsvlogLogReader($filePath, $parser, 'PostgreSQLAccumulator');
432} else {
433	$logReader = new GenericLogReader($filePath, $parser, 'PostgreSQLAccumulator');
434}
435
436foreach($reports AS $report) {
437	$reportAggregator = new $aggregator($logReader, $report['file']);
438	foreach($report['blocks'] AS $block) {
439		$reportAggregator->addReportBlock($supportedReportBlocks[$block]);
440	}
441	$logReader->addReportAggregator($reportAggregator);
442	unset($reportAggregator);
443}
444
445$logReader->parse();
446$logReader->output();
447
448fclose($stderr);
449
450exit(0);
451
452?>