├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Command ├── SchemaCommand.php └── SchemaUpdateCommand.php ├── Controller └── DefaultController.php ├── DependencyInjection ├── Configuration.php └── LexikMonologBrowserExtension.php ├── Form └── LogSearchType.php ├── Formatter └── NormalizerFormatter.php ├── Handler └── DoctrineDBALHandler.php ├── LICENCE ├── LexikMonologBrowserBundle.php ├── Model ├── Log.php ├── LogRepository.php └── SchemaBuilder.php ├── Processor └── WebExtendedProcessor.php ├── README.md ├── Resources ├── config │ ├── routing.xml │ └── services.xml ├── public │ ├── css │ │ ├── datepicker.css │ │ └── log.css │ └── js │ │ ├── bootstrap-datepicker.js │ │ └── log.js ├── screen │ ├── list.jpg │ └── show.jpg ├── translations │ ├── LexikMonologBrowserBundle.de.yml │ ├── LexikMonologBrowserBundle.en.yml │ ├── LexikMonologBrowserBundle.fr.yml │ ├── LexikMonologBrowserBundle.it.yml │ └── LexikMonologBrowserBundle.ro.yml └── views │ ├── Default │ ├── index.html.twig │ └── show.html.twig │ ├── layout.html.twig │ └── utils.html.twig ├── Tests ├── DependencyInjection │ └── LexikMonologDoctrineExtensionTest.php └── bootstrap.php ├── composer.json └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | composer.phar 4 | .buildpath 5 | .project 6 | .settings 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | 9 | env: 10 | - SYMFONY_VERSION=2.3.* 11 | - SYMFONY_VERSION=2.6.* 12 | - SYMFONY_VERSION=2.7.* 13 | 14 | before_script: 15 | - composer require symfony/framework-bundle:${SYMFONY_VERSION} --no-update 16 | - composer update --dev --prefer-source 17 | 18 | script: phpunit 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | ### v0.2.0 (****-**-**) ### 5 | 6 | * fix #1 (renamed "get" column name to "http_get" according to MySQL reserved words) 7 | 8 | ### v0.1.0 (2013-05-25) ### 9 | 10 | * Initial bundle release 11 | -------------------------------------------------------------------------------- /Command/SchemaCommand.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class SchemaCommand extends ContainerAwareCommand 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | protected function configure() 20 | { 21 | $this 22 | ->setName('lexik:monolog-browser:schema-create') 23 | ->setDescription('Create schema to log Monolog entries') 24 | ; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function execute(InputInterface $input, OutputInterface $output) 31 | { 32 | $loggerClosure = function($message) use ($output) { 33 | $output->writeln($message); 34 | }; 35 | 36 | $tableName = $this->getContainer()->getParameter('lexik_monolog_browser.doctrine.table_name'); 37 | 38 | $schemaBuilder = new SchemaBuilder( 39 | $this->getContainer()->get('lexik_monolog_browser.doctrine_dbal.connection'), 40 | $tableName 41 | ); 42 | 43 | $error = false; 44 | try { 45 | $schemaBuilder->create($loggerClosure); 46 | $output->writeln(sprintf('Created table %s for Doctrine Monolog connection', $tableName)); 47 | } catch (\Exception $e) { 48 | $output->writeln(sprintf('Could not create table %s for Doctrine Monolog connection', $tableName)); 49 | $output->writeln(sprintf('%s', $e->getMessage())); 50 | $error = true; 51 | } 52 | 53 | return $error ? 1 : 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Command/SchemaUpdateCommand.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class SchemaUpdateCommand extends ContainerAwareCommand 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | protected function configure() 20 | { 21 | $this 22 | ->setName('lexik:monolog-browser:schema-update') 23 | ->setDescription('Update Monolog table from schema') 24 | ; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function execute(InputInterface $input, OutputInterface $output) 31 | { 32 | $connection = $this->getContainer()->get('lexik_monolog_browser.doctrine_dbal.connection'); 33 | $tableName = $this->getContainer()->getParameter('lexik_monolog_browser.doctrine.table_name'); 34 | 35 | $schemaBuilder = new SchemaBuilder($connection, $tableName); 36 | 37 | $sqls = $schemaBuilder->getSchemaDiff()->toSql($connection->getDatabasePlatform()); 38 | 39 | if (0 == count($sqls)) { 40 | $output->writeln('Nothing to update - your database is already in sync with the current Monolog schema.'); 41 | 42 | return; 43 | } 44 | 45 | $output->writeln('ATTENTION: This operation may not be executed in a production environment, use Doctrine Migrations instead.'); 46 | $output->writeln(sprintf('SQL operations to execute to Monolog table "%s":', $tableName)); 47 | $output->writeln(implode(';' . PHP_EOL, $sqls)); 48 | 49 | $dialog = $this->getHelperSet()->get('dialog'); 50 | if (!$dialog->askConfirmation( 51 | $output, 52 | 'Do you want to execute these SQL operations?', 53 | false 54 | )) { 55 | return; 56 | } 57 | 58 | $error = false; 59 | try { 60 | $schemaBuilder->update(); 61 | $output->writeln(sprintf('Successfully updated Monolog table "%s"! "%s" queries were executed', $tableName, count($sqls))); 62 | } catch (\Exception $e) { 63 | $output->writeln(sprintf('Could not update Monolog table "%s"...', $tableName)); 64 | $output->writeln(sprintf('%s', $e->getMessage())); 65 | $error = true; 66 | } 67 | 68 | return $error ? 1 : 0; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Controller/DefaultController.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class DefaultController extends Controller 16 | { 17 | /** 18 | * @param Request $request 19 | * @return \Symfony\Component\HttpFoundation\Response 20 | */ 21 | public function indexAction(Request $request) 22 | { 23 | try { 24 | $query = $this->getLogRepository()->getLogsQueryBuilder(); 25 | 26 | $filter = $this->get('form.factory')->create(new LogSearchType(), null, array( 27 | 'query_builder' => $query, 28 | 'log_levels' => $this->getLogRepository()->getLogsLevel(), 29 | )); 30 | 31 | $filter->submit($request->get($filter->getName())); 32 | 33 | $pagination = $this->get('knp_paginator')->paginate( 34 | $query, 35 | $request->query->get('page', 1), 36 | $this->container->getParameter('lexik_monolog_browser.logs_per_page') 37 | ); 38 | } catch (DBALException $e) { 39 | $this->get('session')->getFlashBag()->add('error', $e->getMessage()); 40 | $pagination = array(); 41 | } 42 | 43 | return $this->render('LexikMonologBrowserBundle:Default:index.html.twig', array( 44 | 'filter' => isset($filter) ? $filter->createView() : null, 45 | 'pagination' => $pagination, 46 | 'base_layout' => $this->getBaseLayout(), 47 | )); 48 | } 49 | 50 | /** 51 | * @param Request $request 52 | * @param integer $id 53 | * @return \Symfony\Component\HttpFoundation\Response 54 | * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException 55 | */ 56 | public function showAction(Request $request, $id) 57 | { 58 | $log = $this->getLogRepository()->getLogById($id); 59 | 60 | if (null === $log) { 61 | throw $this->createNotFoundException('The log entry does not exist'); 62 | } 63 | 64 | $similarLogsQuery = $this->getLogRepository()->getSimilarLogsQueryBuilder($log); 65 | 66 | $similarLogs = $this->get('knp_paginator')->paginate( 67 | $similarLogsQuery, 68 | $request->query->get('page', 1), 69 | 10 70 | ); 71 | 72 | return $this->render('LexikMonologBrowserBundle:Default:show.html.twig', array( 73 | 'log' => $log, 74 | 'similar_logs' => $similarLogs, 75 | 'base_layout' => $this->getBaseLayout(), 76 | )); 77 | } 78 | 79 | /** 80 | * @return string 81 | */ 82 | protected function getBaseLayout() 83 | { 84 | return $this->container->getParameter('lexik_monolog_browser.base_layout'); 85 | } 86 | 87 | /** 88 | * @return \Lexik\Bundle\MonologBrowserBundle\Model\LogRepository 89 | */ 90 | protected function getLogRepository() 91 | { 92 | return $this->get('lexik_monolog_browser.model.log_repository'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | root('lexik_monolog_browser'); 22 | 23 | $rootNode 24 | ->children() 25 | ->scalarNode('base_layout') 26 | ->cannotBeEmpty() 27 | ->defaultValue('LexikMonologBrowserBundle::layout.html.twig') 28 | ->end() 29 | ->scalarNode('logs_per_page') 30 | ->cannotBeEmpty() 31 | ->defaultValue(25) 32 | ->beforeNormalization() 33 | ->ifString() 34 | ->then(function($v) { return (int) $v; }) 35 | ->end() 36 | ->end() 37 | ->arrayNode('doctrine') 38 | ->children() 39 | ->scalarNode('table_name')->defaultValue('monolog_entries')->end() 40 | ->scalarNode('connection_name')->end() 41 | ->arrayNode('connection') 42 | ->cannotBeEmpty() 43 | ->children() 44 | ->scalarNode('driver')->end() 45 | ->scalarNode('driverClass')->end() 46 | ->scalarNode('pdo')->end() 47 | ->scalarNode('dbname')->end() 48 | ->scalarNode('host')->defaultValue('localhost')->end() 49 | ->scalarNode('port')->defaultNull()->end() 50 | ->scalarNode('user')->defaultValue('root')->end() 51 | ->scalarNode('password')->defaultNull()->end() 52 | ->scalarNode('charset')->defaultValue('UTF8')->end() 53 | ->scalarNode('path')->info(' The filesystem path to the database file for SQLite')->end() 54 | ->booleanNode('memory')->info('True if the SQLite database should be in-memory (non-persistent)')->end() 55 | ->scalarNode('unix_socket')->info('The unix socket to use for MySQL')->end() 56 | ->end() 57 | ->end() 58 | ->end() 59 | ->end() 60 | ->end() 61 | ->validate() 62 | ->ifTrue(function($v) { 63 | if (!isset($v['doctrine'])) { 64 | return true; 65 | } 66 | 67 | return !isset($v['doctrine']['connection_name']) && !isset($v['doctrine']['connection']); 68 | }) 69 | ->thenInvalid('You must provide a valid "connection_name" or "connection" definition.') 70 | ->end() 71 | ->validate() 72 | ->ifTrue(function($v) { 73 | if (!isset($v['doctrine'])) { 74 | return true; 75 | } 76 | 77 | return isset($v['doctrine']['connection_name']) && isset($v['doctrine']['connection']); 78 | }) 79 | ->thenInvalid('You cannot specify both options "connection_name" and "connection".') 80 | ->end() 81 | ; 82 | 83 | return $treeBuilder; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /DependencyInjection/LexikMonologBrowserExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 25 | 26 | $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 27 | $loader->load('services.xml'); 28 | 29 | $container->setParameter('lexik_monolog_browser.base_layout', $config['base_layout']); 30 | $container->setParameter('lexik_monolog_browser.logs_per_page', $config['logs_per_page']); 31 | 32 | $container->setParameter('lexik_monolog_browser.doctrine.table_name', $config['doctrine']['table_name']); 33 | 34 | if (isset($config['doctrine']['connection_name'])) { 35 | $container->setAlias('lexik_monolog_browser.doctrine_dbal.connection', sprintf('doctrine.dbal.%s_connection', $config['doctrine']['connection_name'])); 36 | } 37 | 38 | if (isset($config['doctrine']['connection'])) { 39 | $connectionDefinition = new Definition('Doctrine\DBAL\Connection', array($config['doctrine']['connection'])); 40 | $connectionDefinition->setFactoryClass('Doctrine\DBAL\DriverManager'); 41 | $connectionDefinition->setFactoryMethod('getConnection'); 42 | $container->setDefinition('lexik_monolog_browser.doctrine_dbal.connection', $connectionDefinition); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Form/LogSearchType.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class LogSearchType extends AbstractType 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function buildForm(FormBuilderInterface $builder, array $options) 24 | { 25 | $builder 26 | ->add('term', 'search', array( 27 | 'required' => false, 28 | )) 29 | ->add('level', 'choice', array( 30 | 'choices' => $options['log_levels'], 31 | 'required' => false, 32 | )) 33 | ->add('date_from', 'datetime', array( 34 | 'date_widget' => 'single_text', 35 | 'date_format' => 'MM/dd/yyyy', 36 | 'time_widget' => 'text', 37 | 'required' => false, 38 | )) 39 | ->add('date_to', 'datetime', array( 40 | 'date_widget' => 'single_text', 41 | 'date_format' => 'MM/dd/yyyy', 42 | 'time_widget' => 'text', 43 | 'required' => false, 44 | )) 45 | ; 46 | 47 | $qb = $options['query_builder']; 48 | $convertDateToDatabaseValue = function(\DateTime $date) use ($qb) { 49 | return Type::getType('datetime')->convertToDatabaseValue($date, $qb->getConnection()->getDatabasePlatform()); 50 | }; 51 | 52 | $builder->addEventListener(FormEvents::POST_BIND, function(FormEvent $event) use ($qb, $convertDateToDatabaseValue) { 53 | $data = $event->getData(); 54 | 55 | if (null !== $data['term']) { 56 | $qb->andWhere('l.message LIKE :message') 57 | ->setParameter('message', '%'.str_replace(' ', '%', $data['term']).'%') 58 | ->orWhere('l.channel LIKE :channel') 59 | ->setParameter('channel', $data['term'].'%'); 60 | } 61 | 62 | if (null !== $data['level']) { 63 | $qb->andWhere('l.level = :level') 64 | ->setParameter('level', $data['level']); 65 | } 66 | 67 | if ($data['date_from'] instanceof \DateTime) { 68 | $qb->andWhere('l.datetime >= :date_from') 69 | ->setParameter('date_from', $convertDateToDatabaseValue($data['date_from'])); 70 | } 71 | 72 | if ($data['date_to'] instanceof \DateTime) { 73 | $qb->andWhere('l.datetime <= :date_to') 74 | ->setParameter('date_to', $convertDateToDatabaseValue($data['date_to'])); 75 | } 76 | }); 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function setDefaultOptions(OptionsResolverInterface $resolver) 83 | { 84 | $resolver 85 | ->setRequired(array( 86 | 'query_builder', 87 | )) 88 | ->setDefaults(array( 89 | 'log_levels' => array(), 90 | 'csrf_protection' => false, 91 | )) 92 | ->setAllowedTypes(array( 93 | 'log_levels' => 'array', 94 | 'query_builder' => '\Doctrine\DBAL\Query\QueryBuilder', 95 | )) 96 | ; 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function getName() 103 | { 104 | return 'search'; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Formatter/NormalizerFormatter.php: -------------------------------------------------------------------------------- 1 | &$value) { 15 | if (is_array($value)) { 16 | $value = json_encode($value); 17 | } 18 | } 19 | } 20 | 21 | return $data; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Handler/DoctrineDBALHandler.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class DoctrineDBALHandler extends AbstractProcessingHandler 20 | { 21 | /** 22 | * @var Connection $connection 23 | */ 24 | private $connection; 25 | 26 | /** 27 | * @var string $tableName 28 | */ 29 | private $tableName; 30 | 31 | /** 32 | * @param Connection $connection 33 | * @param string $tableName 34 | * @param int $level 35 | * @param string $bubble 36 | */ 37 | public function __construct(Connection $connection, $tableName, $level = Logger::DEBUG, $bubble = true) 38 | { 39 | $this->connection = $connection; 40 | $this->tableName = $tableName; 41 | 42 | parent::__construct($level, $bubble); 43 | 44 | $this->pushProcessor(new WebProcessor()); 45 | $this->pushProcessor(new WebExtendedProcessor()); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | protected function write(array $record) 52 | { 53 | $record = $record['formatted']; 54 | 55 | try { 56 | $this->connection->insert($this->tableName, $record); 57 | } catch (\Exception $e) { 58 | } 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | protected function getDefaultFormatter() 65 | { 66 | return new NormalizerFormatter(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011-2012 Lexik 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LexikMonologBrowserBundle.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class Log 9 | { 10 | protected $id; 11 | protected $channel; 12 | protected $level; 13 | protected $levelName; 14 | protected $message; 15 | protected $date; 16 | protected $context; 17 | protected $extra; 18 | protected $serverData; 19 | protected $postData; 20 | protected $getData; 21 | 22 | public function __construct(array $data) 23 | { 24 | if (!isset($data['id'])) { 25 | throw new \InvalidArgumentException(); 26 | } 27 | 28 | $this->id = $data['id']; 29 | $this->channel = $data['channel']; 30 | $this->level = $data['level']; 31 | $this->levelName = $data['level_name']; 32 | $this->message = $data['message']; 33 | $this->date = new \DateTime($data['datetime']); 34 | $this->context = isset($data['context']) ? json_decode($data['context'], true) : array(); 35 | $this->extra = isset($data['extra']) ? json_decode($data['extra'], true) : array(); 36 | $this->serverData = isset($data['http_server']) ? json_decode($data['http_server'], true) : array(); 37 | $this->postData = isset($data['http_post']) ? json_decode($data['http_post'], true) : array(); 38 | $this->getData = isset($data['http_get']) ? json_decode($data['http_get'], true) : array(); 39 | } 40 | 41 | public function __toString() 42 | { 43 | return mb_strlen($this->message) > 100 ? sprintf('%s...', mb_substr($this->message, 0, 100)) : $this->message; 44 | } 45 | 46 | public function getId() 47 | { 48 | return $this->id; 49 | } 50 | 51 | public function getChannel() 52 | { 53 | return $this->channel; 54 | } 55 | 56 | public function getLevel() 57 | { 58 | return $this->level; 59 | } 60 | 61 | public function getLevelName() 62 | { 63 | return $this->levelName; 64 | } 65 | 66 | public function getMessage() 67 | { 68 | return $this->message; 69 | } 70 | 71 | public function getDate() 72 | { 73 | return $this->date; 74 | } 75 | 76 | public function getContext() 77 | { 78 | return $this->context; 79 | } 80 | 81 | public function getExtra() 82 | { 83 | return $this->extra; 84 | } 85 | 86 | public function getServerData() 87 | { 88 | return $this->serverData; 89 | } 90 | 91 | public function getPostData() 92 | { 93 | return $this->postData; 94 | } 95 | 96 | public function getGetData() 97 | { 98 | return $this->getData; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Model/LogRepository.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class LogRepository 13 | { 14 | /** 15 | * @var Connection $conn 16 | */ 17 | protected $conn; 18 | 19 | /** 20 | * @var string $tableName 21 | */ 22 | private $tableName; 23 | 24 | /** 25 | * @param Connection $conn 26 | * @param string $tableName 27 | */ 28 | public function __construct(Connection $conn, $tableName) 29 | { 30 | $this->conn = $conn; 31 | $this->tableName = $tableName; 32 | } 33 | 34 | /** 35 | * @return \Doctrine\DBAL\Query\QueryBuilder 36 | */ 37 | protected function createQueryBuilder() 38 | { 39 | return $this->conn->createQueryBuilder(); 40 | } 41 | 42 | /** 43 | * Initialize a QueryBuilder of latest log entries. 44 | * 45 | * @return \Doctrine\DBAL\Query\QueryBuilder 46 | */ 47 | public function getLogsQueryBuilder() 48 | { 49 | return $this->createQueryBuilder() 50 | ->select('l.channel, l.level, l.level_name, l.message, MAX(l.id) AS id, MAX(l.datetime) AS datetime, COUNT(l.id) AS count') 51 | ->from($this->tableName, 'l') 52 | ->groupBy('l.channel, l.level, l.level_name, l.message') 53 | ->orderBy('datetime', 'DESC'); 54 | } 55 | 56 | /** 57 | * Retrieve a log entry by his ID. 58 | * 59 | * @param integer $id 60 | * 61 | * @return Log|null 62 | */ 63 | public function getLogById($id) 64 | { 65 | $log = $this->createQueryBuilder() 66 | ->select('l.*') 67 | ->from($this->tableName, 'l') 68 | ->where('l.id = :id') 69 | ->setParameter(':id', $id) 70 | ->execute() 71 | ->fetch(); 72 | 73 | if (false !== $log) { 74 | return new Log($log); 75 | } 76 | } 77 | 78 | /** 79 | * Retrieve last log entry. 80 | * 81 | * @return Log|null 82 | */ 83 | public function getLastLog() 84 | { 85 | $log = $this->createQueryBuilder() 86 | ->select('l.*') 87 | ->from($this->tableName, 'l') 88 | ->orderBy('l.id', 'DESC') 89 | ->setMaxResults(1) 90 | ->execute() 91 | ->fetch(); 92 | 93 | if (false !== $log) { 94 | return new Log($log); 95 | } 96 | } 97 | 98 | /** 99 | * Retrieve similar logs of the given one. 100 | * 101 | * @param Log $log 102 | * 103 | * @return \Doctrine\DBAL\Query\QueryBuilder 104 | */ 105 | public function getSimilarLogsQueryBuilder(Log $log) 106 | { 107 | return $this->createQueryBuilder() 108 | ->select('l.id, l.channel, l.level, l.level_name, l.message, l.datetime') 109 | ->from($this->tableName, 'l') 110 | ->andWhere('l.message = :message') 111 | ->andWhere('l.channel = :channel') 112 | ->andWhere('l.level = :level') 113 | ->andWhere('l.id != :id') 114 | ->setParameter(':message', $log->getMessage()) 115 | ->setParameter(':channel', $log->getChannel()) 116 | ->setParameter(':level', $log->getLevel()) 117 | ->setParameter(':id', $log->getId()); 118 | } 119 | 120 | /** 121 | * Returns a array of levels with count entries used by logs. 122 | * 123 | * @return array 124 | */ 125 | public function getLogsLevel() 126 | { 127 | $levels = $this->createQueryBuilder() 128 | ->select('l.level, l.level_name, COUNT(l.id) AS count') 129 | ->from($this->tableName, 'l') 130 | ->groupBy('l.level, l.level_name') 131 | ->orderBy('l.level', 'DESC') 132 | ->execute() 133 | ->fetchAll(); 134 | 135 | $normalizedLevels = array(); 136 | foreach ($levels as $level) { 137 | $normalizedLevels[$level['level']] = sprintf('%s (%s)', $level['level_name'], $level['count']); 138 | } 139 | 140 | return $normalizedLevels; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Model/SchemaBuilder.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class SchemaBuilder 14 | { 15 | /** 16 | * @var Connection $conn 17 | */ 18 | protected $conn; 19 | 20 | protected $tableName; 21 | 22 | /** 23 | * @var Schema $schema 24 | */ 25 | protected $schema; 26 | 27 | public function __construct(Connection $conn, $tableName) 28 | { 29 | $this->conn = $conn; 30 | $this->tableName = $tableName; 31 | 32 | $this->schema = new Schema(); 33 | 34 | $entryTable = $this->schema->createTable($this->tableName); 35 | $entryTable->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => true)); 36 | $entryTable->addColumn('channel', 'string', array('length' => 255, 'notNull' => true)); 37 | $entryTable->addColumn('level', 'integer', array('notNull' => true)); 38 | $entryTable->addColumn('level_name', 'string', array('length' => 255, 'notNull' => true)); 39 | $entryTable->addColumn('message', 'text', array('notNull' => true)); 40 | $entryTable->addColumn('datetime', 'datetime', array('notNull' => true)); 41 | $entryTable->addColumn('context', 'text'); 42 | $entryTable->addColumn('extra', 'text'); 43 | $entryTable->addColumn('http_server', 'text'); 44 | $entryTable->addColumn('http_post', 'text'); 45 | $entryTable->addColumn('http_get', 'text'); 46 | $entryTable->setPrimaryKey(array('id')); 47 | } 48 | 49 | public function create(\Closure $logger = null) 50 | { 51 | $queries = $this->schema->toSql($this->conn->getDatabasePlatform()); 52 | 53 | $this->executeQueries($queries, $logger); 54 | } 55 | 56 | public function update(\Closure $logger = null) 57 | { 58 | $queries = $this->getSchemaDiff()->toSaveSql($this->conn->getDatabasePlatform()); 59 | 60 | $this->executeQueries($queries, $logger); 61 | } 62 | 63 | public function getSchemaDiff() 64 | { 65 | $diff = new SchemaDiff(); 66 | $comparator = new Comparator(); 67 | 68 | $tableDiff = $comparator->diffTable( 69 | $this->conn->getSchemaManager()->createSchema()->getTable($this->tableName), 70 | $this->schema->getTable($this->tableName) 71 | ); 72 | 73 | if (false !== $tableDiff) { 74 | $diff->changedTables[$this->tableName] = $tableDiff; 75 | } 76 | 77 | return $diff; 78 | } 79 | 80 | protected function executeQueries(array $queries, \Closure $logger = null) 81 | { 82 | $this->conn->beginTransaction(); 83 | 84 | try { 85 | foreach ($queries as $query) { 86 | if (null !== $logger) { 87 | $logger($query); 88 | } 89 | 90 | $this->conn->query($query); 91 | } 92 | 93 | $this->conn->commit(); 94 | } catch (\Exception $e) { 95 | $this->conn->rollback(); 96 | throw $e; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Processor/WebExtendedProcessor.php: -------------------------------------------------------------------------------- 1 | serverData = $serverData ?: $_SERVER; 30 | $this->postData = $postData ?: $_POST; 31 | $this->getData = $getData ?: $_GET; 32 | } 33 | 34 | /** 35 | * @param array $record 36 | * @return array 37 | */ 38 | public function __invoke(array $record) 39 | { 40 | // skip processing if for some reason request data 41 | // is not present (CLI or wonky SAPIs) 42 | if (!isset($this->serverData['REQUEST_URI'])) { 43 | return $record; 44 | } 45 | 46 | $record['http_server'] = $this->serverData; 47 | $record['http_post'] = $this->postData; 48 | $record['http_get'] = $this->getData; 49 | 50 | return $record; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | LexikMonologBrowserBundle 2 | ========================= 3 | 4 | [![Build Status](https://secure.travis-ci.org/lexik/LexikMonologBrowserBundle.png)](http://travis-ci.org/lexik/LexikMonologBrowserBundle) 5 | [![Latest Stable Version](https://poser.pugx.org/lexik/monolog-browser-bundle/v/stable)](https://packagist.org/packages/lexik/monolog-browser-bundle) 6 | [![SensioLabsInsight](https://insight.sensiolabs.com/projects/d73bade1-4158-4085-aab8-1042d3704a73/mini.png)](https://insight.sensiolabs.com/projects/d73bade1-4158-4085-aab8-1042d3704a73) 7 | 8 | This Bundle is deprecated 9 | ========================= 10 | 11 | This Symfony2 bundle provides a [Doctrine DBAL](https://github.com/doctrine/dbal) handler for [Monolog](https://github.com/Seldaek/monolog) and a web UI to display log entries. You can list, filter and paginate logs as you can see on the screenshot bellow: 12 | 13 | ![Log entries listing](https://github.com/lexik/LexikMonologBrowserBundle/raw/master/Resources/screen/list.jpg) 14 | ![Log entry show](https://github.com/lexik/LexikMonologBrowserBundle/raw/master/Resources/screen/show.jpg) 15 | 16 | As this bundle query your database on each raised log, it's relevant for small and medium projects, but if you have billion of logs consider using a specific log server like [sentry](http://getsentry.com/), [airbrake](https://airbrake.io/), etc. 17 | 18 | Requirements: 19 | ------------ 20 | 21 | * Symfony 2.1+ 22 | * KnpLabs/KnpPaginatorBundle 23 | 24 | Installation 25 | ------------ 26 | 27 | Installation with composer: 28 | 29 | ``` json 30 | ... 31 | "require": { 32 | ... 33 | "lexik/monolog-browser-bundle": "~1.0", 34 | ... 35 | }, 36 | ... 37 | ``` 38 | 39 | Next, be sure to enable these bundles in your `app/AppKernel.php` file: 40 | 41 | ``` php 42 | public function registerBundles() 43 | { 44 | return array( 45 | // ... 46 | new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(), 47 | new Lexik\Bundle\MonologBrowserBundle\LexikMonologBrowserBundle(), 48 | // ... 49 | ); 50 | } 51 | ``` 52 | 53 | Configuration 54 | ------------- 55 | 56 | First of all, you need to configure the Doctrine DBAL connection to use in the handler. You have 2 ways to do that: 57 | 58 | **By using an existing Doctrine connection:** 59 | 60 | Note: we set the `logging` and `profiling` option to false to avoid DI circular reference. 61 | 62 | ``` yaml 63 | # app/config/config.yml 64 | doctrine: 65 | dbal: 66 | connections: 67 | default: 68 | ... 69 | monolog: 70 | driver: pdo_sqlite 71 | dbname: monolog 72 | path: %kernel.root_dir%/cache/monolog2.db 73 | charset: UTF8 74 | logging: false 75 | profiling: false 76 | 77 | lexik_monolog_browser: 78 | doctrine: 79 | connection_name: monolog 80 | ``` 81 | 82 | **By creating a custom Doctrine connection for the bundle:** 83 | 84 | ``` yaml 85 | # app/config/config.yml 86 | lexik_monolog_browser: 87 | doctrine: 88 | connection: 89 | driver: pdo_sqlite 90 | driverClass: ~ 91 | pdo: ~ 92 | dbname: monolog 93 | host: localhost 94 | port: ~ 95 | user: root 96 | password: ~ 97 | charset: UTF8 98 | path: %kernel.root_dir%/db/monolog.db # The filesystem path to the database file for SQLite 99 | memory: ~ # True if the SQLite database should be in-memory (non-persistent) 100 | unix_socket: ~ # The unix socket to use for MySQL 101 | ``` 102 | 103 | Please refer to the [Doctrine DBAL connection configuration](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#configuration) for more details. 104 | 105 | Optionally you can override the schema table name (`monolog_entries` by default): 106 | 107 | ``` yaml 108 | # app/config/config.yml 109 | lexik_monolog_browser: 110 | doctrine: 111 | table_name: monolog_entries 112 | ``` 113 | 114 | Now your database is configured, you can generate the schema for your log entry table by running the following command: 115 | 116 | ``` 117 | ./app/console lexik:monolog-browser:schema-create 118 | # you should see as result: 119 | # Created table monolog_entries for Doctrine Monolog connection 120 | ``` 121 | 122 | Then, you can configure Monolog to use the Doctrine DBAL handler: 123 | 124 | ``` yaml 125 | # app/config/config_prod.yml # or any env 126 | monolog: 127 | handlers: 128 | main: 129 | type: fingers_crossed # or buffer 130 | level: error 131 | handler: lexik_monolog_browser 132 | app: 133 | type: buffer 134 | action_level: info 135 | channels: app 136 | handler: lexik_monolog_browser 137 | deprecation: 138 | type: buffer 139 | action_level: warning 140 | channels: deprecation 141 | handler: lexik_monolog_browser 142 | lexik_monolog_browser: 143 | type: service 144 | id: lexik_monolog_browser.handler.doctrine_dbal 145 | ``` 146 | 147 | Now you have enabled and configured the handler, you migth want to display log entries, just import the routing file: 148 | 149 | ``` yaml 150 | # app/config/routing.yml 151 | lexik_monolog_browser: 152 | resource: "@LexikMonologBrowserBundle/Resources/config/routing.xml" 153 | prefix: /admin/monolog 154 | ``` 155 | 156 | Translations 157 | ------------ 158 | 159 | If you wish to use default translations provided in this bundle, make sure you have enabled the translator in your config: 160 | 161 | ``` yaml 162 | # app/config/config.yml 163 | framework: 164 | translator: ~ 165 | ``` 166 | 167 | Overriding default layout 168 | ------------------------- 169 | 170 | You can override the default layout of the bundle by using the `base_layout` option: 171 | 172 | ``` yaml 173 | # app/config/config.yml 174 | lexik_monolog_browser: 175 | base_layout: "LexikMonologBrowserBundle::layout.html.twig" 176 | ``` 177 | 178 | or quite simply with the Symfony way by create a template on `app/Resources/LexikMonologBrowserBundle/views/layout.html.twig`. 179 | 180 | Updating the bundle 181 | ------------------- 182 | 183 | At each bundle updates, be careful to potential schema updates and because Monolog entries table is disconnected from the rest of your Doctrine entities or models, you have to manualy update the schema. 184 | 185 | The bundle comes with a `schema-update` command but in some cases, like on renaming columns, the default behavior is not perfect and you may have a look to Doctrine Migrations (you can read an example on PR #2). 186 | 187 | You can execute the command below to visualize SQL diff and execute schema updates: 188 | 189 | ``` 190 | ./app/console lexik:monolog-browser:schema-update 191 | ``` 192 | 193 | ToDo 194 | ---- 195 | 196 | * configure Processors to push into the Handler 197 | * abstract handler and connector for Doctrine and browse another like Elasticsearh 198 | * write Tests 199 | -------------------------------------------------------------------------------- /Resources/config/routing.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | LexikMonologBrowserBundle:Default:index 9 | 10 | 11 | 12 | LexikMonologBrowserBundle:Default:show 13 | 14 | 15 | -------------------------------------------------------------------------------- /Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | Lexik\Bundle\MonologBrowserBundle\Handler\DoctrineDBALHandler 9 | Lexik\Bundle\MonologBrowserBundle\Model\LogRepository 10 | 11 | 12 | 13 | 14 | 15 | %lexik_monolog_browser.doctrine.table_name% 16 | 17 | 18 | 19 | 20 | %lexik_monolog_browser.doctrine.table_name% 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Resources/public/css/datepicker.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Datepicker for Bootstrap 3 | * 4 | * Copyright 2012 Stefan Petre 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | */ 9 | .datepicker { 10 | top: 0; 11 | left: 0; 12 | padding: 4px; 13 | margin-top: 1px; 14 | -webkit-border-radius: 4px; 15 | -moz-border-radius: 4px; 16 | border-radius: 4px; 17 | /*.dow { 18 | border-top: 1px solid #ddd !important; 19 | }*/ 20 | 21 | } 22 | .datepicker:before { 23 | content: ''; 24 | display: inline-block; 25 | border-left: 7px solid transparent; 26 | border-right: 7px solid transparent; 27 | border-bottom: 7px solid #ccc; 28 | border-bottom-color: rgba(0, 0, 0, 0.2); 29 | position: absolute; 30 | top: -7px; 31 | left: 6px; 32 | } 33 | .datepicker:after { 34 | content: ''; 35 | display: inline-block; 36 | border-left: 6px solid transparent; 37 | border-right: 6px solid transparent; 38 | border-bottom: 6px solid #ffffff; 39 | position: absolute; 40 | top: -6px; 41 | left: 7px; 42 | } 43 | .datepicker > div { 44 | display: none; 45 | } 46 | .datepicker table { 47 | width: 100%; 48 | margin: 0; 49 | } 50 | .datepicker td, 51 | .datepicker th { 52 | text-align: center; 53 | width: 20px; 54 | height: 20px; 55 | -webkit-border-radius: 4px; 56 | -moz-border-radius: 4px; 57 | border-radius: 4px; 58 | } 59 | .datepicker td.day:hover { 60 | background: #eeeeee; 61 | cursor: pointer; 62 | } 63 | .datepicker td.day.disabled { 64 | color: #eeeeee; 65 | } 66 | .datepicker td.old, 67 | .datepicker td.new { 68 | color: #999999; 69 | } 70 | .datepicker td.active, 71 | .datepicker td.active:hover { 72 | color: #ffffff; 73 | background-color: #006dcc; 74 | background-image: -moz-linear-gradient(top, #0088cc, #0044cc); 75 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); 76 | background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); 77 | background-image: -o-linear-gradient(top, #0088cc, #0044cc); 78 | background-image: linear-gradient(to bottom, #0088cc, #0044cc); 79 | background-repeat: repeat-x; 80 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); 81 | border-color: #0044cc #0044cc #002a80; 82 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 83 | *background-color: #0044cc; 84 | /* Darken IE7 buttons by default so they stand out more given they won't have borders */ 85 | 86 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 87 | color: #fff; 88 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 89 | } 90 | .datepicker td.active:hover, 91 | .datepicker td.active:hover:hover, 92 | .datepicker td.active:focus, 93 | .datepicker td.active:hover:focus, 94 | .datepicker td.active:active, 95 | .datepicker td.active:hover:active, 96 | .datepicker td.active.active, 97 | .datepicker td.active:hover.active, 98 | .datepicker td.active.disabled, 99 | .datepicker td.active:hover.disabled, 100 | .datepicker td.active[disabled], 101 | .datepicker td.active:hover[disabled] { 102 | color: #ffffff; 103 | background-color: #0044cc; 104 | *background-color: #003bb3; 105 | } 106 | .datepicker td.active:active, 107 | .datepicker td.active:hover:active, 108 | .datepicker td.active.active, 109 | .datepicker td.active:hover.active { 110 | background-color: #003399 \9; 111 | } 112 | .datepicker td span { 113 | display: block; 114 | width: 47px; 115 | height: 54px; 116 | line-height: 54px; 117 | float: left; 118 | margin: 2px; 119 | cursor: pointer; 120 | -webkit-border-radius: 4px; 121 | -moz-border-radius: 4px; 122 | border-radius: 4px; 123 | } 124 | .datepicker td span:hover { 125 | background: #eeeeee; 126 | } 127 | .datepicker td span.active { 128 | color: #ffffff; 129 | background-color: #006dcc; 130 | background-image: -moz-linear-gradient(top, #0088cc, #0044cc); 131 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); 132 | background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); 133 | background-image: -o-linear-gradient(top, #0088cc, #0044cc); 134 | background-image: linear-gradient(to bottom, #0088cc, #0044cc); 135 | background-repeat: repeat-x; 136 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); 137 | border-color: #0044cc #0044cc #002a80; 138 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 139 | *background-color: #0044cc; 140 | /* Darken IE7 buttons by default so they stand out more given they won't have borders */ 141 | 142 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 143 | color: #fff; 144 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 145 | } 146 | .datepicker td span.active:hover, 147 | .datepicker td span.active:focus, 148 | .datepicker td span.active:active, 149 | .datepicker td span.active.active, 150 | .datepicker td span.active.disabled, 151 | .datepicker td span.active[disabled] { 152 | color: #ffffff; 153 | background-color: #0044cc; 154 | *background-color: #003bb3; 155 | } 156 | .datepicker td span.active:active, 157 | .datepicker td span.active.active { 158 | background-color: #003399 \9; 159 | } 160 | .datepicker td span.old { 161 | color: #999999; 162 | } 163 | .datepicker th.switch { 164 | width: 145px; 165 | } 166 | .datepicker th.next, 167 | .datepicker th.prev { 168 | font-size: 21px; 169 | } 170 | .datepicker thead tr:first-child th { 171 | cursor: pointer; 172 | } 173 | .datepicker thead tr:first-child th:hover { 174 | background: #eeeeee; 175 | } 176 | .input-append.date .add-on i, 177 | .input-prepend.date .add-on i { 178 | display: block; 179 | cursor: pointer; 180 | width: 16px; 181 | height: 16px; 182 | } -------------------------------------------------------------------------------- /Resources/public/css/log.css: -------------------------------------------------------------------------------- 1 | .monolog-browser-header h1 { 2 | font-family: Georgia, "Times New Roman", Times, serif; 3 | font-size: 22px; 4 | font-weight: normal; 5 | line-height: 30px; 6 | color: #313131; 7 | word-break: break-all; 8 | } 9 | 10 | table.monolog-browser-results td { 11 | word-break: break-all; 12 | padding: 8px 5px; 13 | } 14 | table.monolog-browser-results td:first-child { white-space: nowrap; } 15 | 16 | .monolog-browser-filters .filter-term input { width: 100%; } 17 | .monolog-browser-filters .filter-level select { width: 100%; } 18 | .monolog-browser-filters .filter-datefrom { float: left; margin-right: 10px; } 19 | .monolog-browser-filters .filter-daterange .datepicker { width: 75px; } 20 | .monolog-browser-filters .filter-daterange .hour, 21 | .monolog-browser-filters .filter-daterange .minute { width: 20px; } 22 | .monolog-browser-filters .filter-submit { padding-top: 25px; } 23 | -------------------------------------------------------------------------------- /Resources/public/js/bootstrap-datepicker.js: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-datepicker.js 3 | * http://www.eyecon.ro/bootstrap-datepicker 4 | * ========================================================= 5 | * Copyright 2012 Stefan Petre 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | !function( $ ) { 21 | 22 | // Picker object 23 | 24 | var Datepicker = function(element, options){ 25 | this.element = $(element); 26 | this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy'); 27 | this.picker = $(DPGlobal.template) 28 | .appendTo('body') 29 | .on({ 30 | click: $.proxy(this.click, this)//, 31 | //mousedown: $.proxy(this.mousedown, this) 32 | }); 33 | this.isInput = this.element.is('input'); 34 | this.component = this.element.is('.date') ? this.element.find('.add-on') : false; 35 | 36 | if (this.isInput) { 37 | this.element.on({ 38 | focus: $.proxy(this.show, this), 39 | //blur: $.proxy(this.hide, this), 40 | keyup: $.proxy(this.update, this) 41 | }); 42 | } else { 43 | if (this.component){ 44 | this.component.on('click', $.proxy(this.show, this)); 45 | } else { 46 | this.element.on('click', $.proxy(this.show, this)); 47 | } 48 | } 49 | 50 | this.minViewMode = options.minViewMode||this.element.data('date-minviewmode')||0; 51 | if (typeof this.minViewMode === 'string') { 52 | switch (this.minViewMode) { 53 | case 'months': 54 | this.minViewMode = 1; 55 | break; 56 | case 'years': 57 | this.minViewMode = 2; 58 | break; 59 | default: 60 | this.minViewMode = 0; 61 | break; 62 | } 63 | } 64 | this.viewMode = options.viewMode||this.element.data('date-viewmode')||0; 65 | if (typeof this.viewMode === 'string') { 66 | switch (this.viewMode) { 67 | case 'months': 68 | this.viewMode = 1; 69 | break; 70 | case 'years': 71 | this.viewMode = 2; 72 | break; 73 | default: 74 | this.viewMode = 0; 75 | break; 76 | } 77 | } 78 | this.startViewMode = this.viewMode; 79 | this.weekStart = options.weekStart||this.element.data('date-weekstart')||0; 80 | this.weekEnd = this.weekStart === 0 ? 6 : this.weekStart - 1; 81 | this.onRender = options.onRender; 82 | this.fillDow(); 83 | this.fillMonths(); 84 | this.update(); 85 | this.showMode(); 86 | }; 87 | 88 | Datepicker.prototype = { 89 | constructor: Datepicker, 90 | 91 | show: function(e) { 92 | this.picker.show(); 93 | this.height = this.component ? this.component.outerHeight() : this.element.outerHeight(); 94 | this.place(); 95 | $(window).on('resize', $.proxy(this.place, this)); 96 | if (e ) { 97 | e.stopPropagation(); 98 | e.preventDefault(); 99 | } 100 | if (!this.isInput) { 101 | } 102 | var that = this; 103 | $(document).on('mousedown', function(ev){ 104 | if ($(ev.target).closest('.datepicker').length == 0) { 105 | that.hide(); 106 | } 107 | }); 108 | this.element.trigger({ 109 | type: 'show', 110 | date: this.date 111 | }); 112 | }, 113 | 114 | hide: function(){ 115 | this.picker.hide(); 116 | $(window).off('resize', this.place); 117 | this.viewMode = this.startViewMode; 118 | this.showMode(); 119 | if (!this.isInput) { 120 | $(document).off('mousedown', this.hide); 121 | } 122 | this.set(); 123 | this.element.trigger({ 124 | type: 'hide', 125 | date: this.date 126 | }); 127 | }, 128 | 129 | set: function() { 130 | var formated = DPGlobal.formatDate(this.date, this.format); 131 | if (!this.isInput) { 132 | if (this.component){ 133 | this.element.find('input').prop('value', formated); 134 | } 135 | this.element.data('date', formated); 136 | } else { 137 | this.element.prop('value', formated); 138 | } 139 | }, 140 | 141 | setValue: function(newDate) { 142 | if (typeof newDate === 'string') { 143 | this.date = DPGlobal.parseDate(newDate, this.format); 144 | } else { 145 | this.date = new Date(newDate); 146 | } 147 | this.set(); 148 | this.viewDate = new Date(this.date.getFullYear(), this.date.getMonth(), 1, 0, 0, 0, 0); 149 | this.fill(); 150 | }, 151 | 152 | place: function(){ 153 | var offset = this.component ? this.component.offset() : this.element.offset(); 154 | this.picker.css({ 155 | top: offset.top + this.height, 156 | left: offset.left 157 | }); 158 | }, 159 | 160 | update: function(newDate){ 161 | this.date = DPGlobal.parseDate( 162 | typeof newDate === 'string' ? newDate : (this.isInput ? this.element.prop('value') : this.element.data('date')), 163 | this.format 164 | ); 165 | this.viewDate = new Date(this.date.getFullYear(), this.date.getMonth(), 1, 0, 0, 0, 0); 166 | this.fill(); 167 | }, 168 | 169 | fillDow: function(){ 170 | var dowCnt = this.weekStart; 171 | var html = ''; 172 | while (dowCnt < this.weekStart + 7) { 173 | html += ''+DPGlobal.dates.daysMin[(dowCnt++)%7]+''; 174 | } 175 | html += ''; 176 | this.picker.find('.datepicker-days thead').append(html); 177 | }, 178 | 179 | fillMonths: function(){ 180 | var html = ''; 181 | var i = 0 182 | while (i < 12) { 183 | html += ''+DPGlobal.dates.monthsShort[i++]+''; 184 | } 185 | this.picker.find('.datepicker-months td').append(html); 186 | }, 187 | 188 | fill: function() { 189 | var d = new Date(this.viewDate), 190 | year = d.getFullYear(), 191 | month = d.getMonth(), 192 | currentDate = this.date.valueOf(); 193 | this.picker.find('.datepicker-days th:eq(1)') 194 | .text(DPGlobal.dates.months[month]+' '+year); 195 | var prevMonth = new Date(year, month-1, 28,0,0,0,0), 196 | day = DPGlobal.getDaysInMonth(prevMonth.getFullYear(), prevMonth.getMonth()); 197 | prevMonth.setDate(day); 198 | prevMonth.setDate(day - (prevMonth.getDay() - this.weekStart + 7)%7); 199 | var nextMonth = new Date(prevMonth); 200 | nextMonth.setDate(nextMonth.getDate() + 42); 201 | nextMonth = nextMonth.valueOf(); 202 | html = []; 203 | var clsName; 204 | while(prevMonth.valueOf() < nextMonth) { 205 | if (prevMonth.getDay() === this.weekStart) { 206 | html.push(''); 207 | } 208 | clsName = this.onRender(prevMonth); 209 | if (prevMonth.getMonth() < month) { 210 | clsName += ' old'; 211 | } else if (prevMonth.getMonth() > month) { 212 | clsName += ' new'; 213 | } 214 | if (prevMonth.valueOf() === currentDate) { 215 | clsName += ' active'; 216 | } 217 | html.push(''+prevMonth.getDate() + ''); 218 | if (prevMonth.getDay() === this.weekEnd) { 219 | html.push(''); 220 | } 221 | prevMonth.setDate(prevMonth.getDate()+1); 222 | } 223 | this.picker.find('.datepicker-days tbody').empty().append(html.join('')); 224 | var currentYear = this.date.getFullYear(); 225 | 226 | var months = this.picker.find('.datepicker-months') 227 | .find('th:eq(1)') 228 | .text(year) 229 | .end() 230 | .find('span').removeClass('active'); 231 | if (currentYear === year) { 232 | months.eq(this.date.getMonth()).addClass('active'); 233 | } 234 | 235 | html = ''; 236 | year = parseInt(year/10, 10) * 10; 237 | var yearCont = this.picker.find('.datepicker-years') 238 | .find('th:eq(1)') 239 | .text(year + '-' + (year + 9)) 240 | .end() 241 | .find('td'); 242 | year -= 1; 243 | for (var i = -1; i < 11; i++) { 244 | html += ''+year+''; 245 | year += 1; 246 | } 247 | yearCont.html(html); 248 | }, 249 | 250 | click: function(e) { 251 | e.stopPropagation(); 252 | e.preventDefault(); 253 | var target = $(e.target).closest('span, td, th'); 254 | if (target.length === 1) { 255 | switch(target[0].nodeName.toLowerCase()) { 256 | case 'th': 257 | switch(target[0].className) { 258 | case 'switch': 259 | this.showMode(1); 260 | break; 261 | case 'prev': 262 | case 'next': 263 | this.viewDate['set'+DPGlobal.modes[this.viewMode].navFnc].call( 264 | this.viewDate, 265 | this.viewDate['get'+DPGlobal.modes[this.viewMode].navFnc].call(this.viewDate) + 266 | DPGlobal.modes[this.viewMode].navStep * (target[0].className === 'prev' ? -1 : 1) 267 | ); 268 | this.fill(); 269 | this.set(); 270 | break; 271 | } 272 | break; 273 | case 'span': 274 | if (target.is('.month')) { 275 | var month = target.parent().find('span').index(target); 276 | this.viewDate.setMonth(month); 277 | } else { 278 | var year = parseInt(target.text(), 10)||0; 279 | this.viewDate.setFullYear(year); 280 | } 281 | if (this.viewMode !== 0) { 282 | this.date = new Date(this.viewDate); 283 | this.element.trigger({ 284 | type: 'changeDate', 285 | date: this.date, 286 | viewMode: DPGlobal.modes[this.viewMode].clsName 287 | }); 288 | } 289 | this.showMode(-1); 290 | this.fill(); 291 | this.set(); 292 | break; 293 | case 'td': 294 | if (target.is('.day') && !target.is('.disabled')){ 295 | var day = parseInt(target.text(), 10)||1; 296 | var month = this.viewDate.getMonth(); 297 | if (target.is('.old')) { 298 | month -= 1; 299 | } else if (target.is('.new')) { 300 | month += 1; 301 | } 302 | var year = this.viewDate.getFullYear(); 303 | this.date = new Date(year, month, day,0,0,0,0); 304 | this.viewDate = new Date(year, month, Math.min(28, day),0,0,0,0); 305 | this.fill(); 306 | this.set(); 307 | this.element.trigger({ 308 | type: 'changeDate', 309 | date: this.date, 310 | viewMode: DPGlobal.modes[this.viewMode].clsName 311 | }); 312 | } 313 | break; 314 | } 315 | } 316 | }, 317 | 318 | mousedown: function(e){ 319 | e.stopPropagation(); 320 | e.preventDefault(); 321 | }, 322 | 323 | showMode: function(dir) { 324 | if (dir) { 325 | this.viewMode = Math.max(this.minViewMode, Math.min(2, this.viewMode + dir)); 326 | } 327 | this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show(); 328 | } 329 | }; 330 | 331 | $.fn.datepicker = function ( option, val ) { 332 | return this.each(function () { 333 | var $this = $(this), 334 | data = $this.data('datepicker'), 335 | options = typeof option === 'object' && option; 336 | if (!data) { 337 | $this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options)))); 338 | } 339 | if (typeof option === 'string') data[option](val); 340 | }); 341 | }; 342 | 343 | $.fn.datepicker.defaults = { 344 | onRender: function(date) { 345 | return ''; 346 | } 347 | }; 348 | $.fn.datepicker.Constructor = Datepicker; 349 | 350 | var DPGlobal = { 351 | modes: [ 352 | { 353 | clsName: 'days', 354 | navFnc: 'Month', 355 | navStep: 1 356 | }, 357 | { 358 | clsName: 'months', 359 | navFnc: 'FullYear', 360 | navStep: 1 361 | }, 362 | { 363 | clsName: 'years', 364 | navFnc: 'FullYear', 365 | navStep: 10 366 | }], 367 | dates:{ 368 | days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], 369 | daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], 370 | daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], 371 | months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], 372 | monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] 373 | }, 374 | isLeapYear: function (year) { 375 | return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)) 376 | }, 377 | getDaysInMonth: function (year, month) { 378 | return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month] 379 | }, 380 | parseFormat: function(format){ 381 | var separator = format.match(/[.\/\-\s].*?/), 382 | parts = format.split(/\W+/); 383 | if (!separator || !parts || parts.length === 0){ 384 | throw new Error("Invalid date format."); 385 | } 386 | return {separator: separator, parts: parts}; 387 | }, 388 | parseDate: function(date, format) { 389 | var parts = date.split(format.separator), 390 | date = new Date(), 391 | val; 392 | date.setHours(0); 393 | date.setMinutes(0); 394 | date.setSeconds(0); 395 | date.setMilliseconds(0); 396 | if (parts.length === format.parts.length) { 397 | var year = date.getFullYear(), day = date.getDate(), month = date.getMonth(); 398 | for (var i=0, cnt = format.parts.length; i < cnt; i++) { 399 | val = parseInt(parts[i], 10)||1; 400 | switch(format.parts[i]) { 401 | case 'dd': 402 | case 'd': 403 | day = val; 404 | date.setDate(val); 405 | break; 406 | case 'mm': 407 | case 'm': 408 | month = val - 1; 409 | date.setMonth(val - 1); 410 | break; 411 | case 'yy': 412 | year = 2000 + val; 413 | date.setFullYear(2000 + val); 414 | break; 415 | case 'yyyy': 416 | year = val; 417 | date.setFullYear(val); 418 | break; 419 | } 420 | } 421 | date = new Date(year, month, day, 0 ,0 ,0); 422 | } 423 | return date; 424 | }, 425 | formatDate: function(date, format){ 426 | var val = { 427 | d: date.getDate(), 428 | m: date.getMonth() + 1, 429 | yy: date.getFullYear().toString().substring(2), 430 | yyyy: date.getFullYear() 431 | }; 432 | val.dd = (val.d < 10 ? '0' : '') + val.d; 433 | val.mm = (val.m < 10 ? '0' : '') + val.m; 434 | var date = []; 435 | for (var i=0, cnt = format.parts.length; i < cnt; i++) { 436 | date.push(val[format.parts[i]]); 437 | } 438 | return date.join(format.separator); 439 | }, 440 | headTemplate: ''+ 441 | ''+ 442 | '‹'+ 443 | ''+ 444 | '›'+ 445 | ''+ 446 | '', 447 | contTemplate: '' 448 | }; 449 | DPGlobal.template = ''; 469 | 470 | }( window.jQuery ); -------------------------------------------------------------------------------- /Resources/public/js/log.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $('.datepicker').datepicker({ 3 | format: 'mm/dd/yyyy' 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /Resources/screen/list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexik/LexikMonologBrowserBundle/f9c7a9323763dc392e2b20aa995d44570f10bde4/Resources/screen/list.jpg -------------------------------------------------------------------------------- /Resources/screen/show.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lexik/LexikMonologBrowserBundle/f9c7a9323763dc392e2b20aa995d44570f10bde4/Resources/screen/show.jpg -------------------------------------------------------------------------------- /Resources/translations/LexikMonologBrowserBundle.de.yml: -------------------------------------------------------------------------------- 1 | log: 2 | search: 3 | term: Suche (Nachricht oder Kanal) 4 | level: Level 5 | date_from: Von 6 | date_to: Bis 7 | results: 8 | datetime: Datum 9 | message: Nachricht 10 | show: 11 | back: Zurück zu den Logs 12 | extra: Extra 13 | context: Kontext 14 | request: Request 15 | similar: Ähnliche Logs 16 | -------------------------------------------------------------------------------- /Resources/translations/LexikMonologBrowserBundle.en.yml: -------------------------------------------------------------------------------- 1 | log: 2 | search: 3 | term: Search (message or channel) 4 | level: Level 5 | date_from: From 6 | date_to: To 7 | results: 8 | datetime: Date 9 | message: Message 10 | show: 11 | back: Back to logs 12 | extra: Extra 13 | context: Context 14 | request: Request 15 | similar: Similar logs 16 | -------------------------------------------------------------------------------- /Resources/translations/LexikMonologBrowserBundle.fr.yml: -------------------------------------------------------------------------------- 1 | log: 2 | search: 3 | term: Recherche (message ou channel) 4 | level: Niveau 5 | date_from: De 6 | date_to: A 7 | results: 8 | datetime: Date 9 | message: Message 10 | show: 11 | back: Retours aux logs 12 | extra: Extra 13 | context: Context 14 | request: Request 15 | similar: Logs similaires 16 | -------------------------------------------------------------------------------- /Resources/translations/LexikMonologBrowserBundle.it.yml: -------------------------------------------------------------------------------- 1 | log: 2 | search: 3 | term: Ricerca (messaggio o channel) 4 | level: Livello 5 | date_from: Da 6 | date_to: A 7 | results: 8 | datetime: Data 9 | message: Messaggio 10 | show: 11 | back: Ritorna ai logs 12 | extra: Extra 13 | context: Contesto 14 | request: Rechiesta 15 | similar: Logs simili 16 | -------------------------------------------------------------------------------- /Resources/translations/LexikMonologBrowserBundle.ro.yml: -------------------------------------------------------------------------------- 1 | log: 2 | search: 3 | term: Căutare (mesaj sau canal) 4 | level: Nivel 5 | date_from: De la 6 | date_to: Până la 7 | results: 8 | datetime: Dată 9 | message: Mesaj 10 | show: 11 | back: Înapoi la jurnal 12 | extra: Extra 13 | context: Context 14 | request: Cerere 15 | similar: Jurnale similare 16 | -------------------------------------------------------------------------------- /Resources/views/Default/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends base_layout %} 2 | 3 | {% trans_default_domain 'LexikMonologBrowserBundle' %} 4 | 5 | {% import "LexikMonologBrowserBundle::utils.html.twig" as utils %} 6 | 7 | {% block monolog_browser_content %} 8 | {% if filter %} 9 |
10 |
11 |
12 | {{ form_label(filter.term, 'log.search.term' | trans) }} 13 | {{ form_widget(filter.term) }} 14 | {{ form_errors(filter.term) }} 15 |
16 |
17 | {{ form_label(filter.level, 'log.search.level' | trans) }} 18 | {{ form_widget(filter.level) }} 19 | {{ form_errors(filter.level) }} 20 |
21 |
22 |
23 | {{ form_label(filter.date_from, 'log.search.date_from' | trans) }} 24 | {{ form_widget(filter.date_from.date, { 'attr': { 'class': 'datepicker' } }) }} 25 | {{ form_widget(filter.date_from.time.hour, { 'attr': { 'class': 'hour' } }) }} : {{ form_widget(filter.date_from.time.minute, { 'attr': { 'class': 'minute' } }) }} 26 | {{ form_errors(filter.date_from) }} 27 |
28 | 29 |
30 | {{ form_label(filter.date_to, 'log.search.date_to' | trans) }} 31 | {{ form_widget(filter.date_to.date, { 'attr': { 'class': 'datepicker' } }) }} 32 | {{ form_widget(filter.date_to.time.hour, { 'attr': { 'class': 'hour' } }) }} : {{ form_widget(filter.date_to.time.minute, { 'attr': { 'class': 'minute' } }) }} 33 | {{ form_errors(filter.date_to) }} 34 |
35 |
36 |
37 | 40 |
41 |
42 |
43 | {% endif %} 44 | 45 | {% if pagination %} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {% for log in pagination %} 57 | 58 | 59 | 60 | 65 | 68 | 69 | {% endfor %} 70 | 71 |
{{ knp_pagination_sortable(pagination, 'log.results.datetime' | trans, 'l.datetime') }}{{ knp_pagination_sortable(pagination, 'log.results.message' | trans, 'l.message') }}
{{ date(log.datetime) | date('F j H:i:s') }}{{ utils.render_count_badge(log.count) }} 61 | 62 | {{ log.message }} 63 | 64 | 66 | {{ utils.render_level_label(log.level, (log.channel ~ '.' ~ log.level_name)) }} 67 |
72 | 73 | 76 | {% endif %} 77 | {% endblock %} 78 | -------------------------------------------------------------------------------- /Resources/views/Default/show.html.twig: -------------------------------------------------------------------------------- 1 | {% extends base_layout %} 2 | 3 | {% trans_default_domain 'LexikMonologBrowserBundle' %} 4 | 5 | {% import "LexikMonologBrowserBundle::utils.html.twig" as utils %} 6 | 7 | {% block monolog_browser_content %} 8 | {{ block('log_header') }} 9 | {{ block('log_content') }} 10 | {% endblock %} 11 | 12 | {% block log_header %} 13 | 14 | 15 | {{ 'log.show.back' | trans }} 16 | 17 | 18 | 25 | {% endblock %} 26 | 27 | {% block log_content %} 28 |
29 | 35 |
36 | {% import _self as show_macros %} 37 |
38 | {{ show_macros.render_data_table(log.extra) }} 39 |
40 |
41 | {{ show_macros.render_data_table(log.context) }} 42 |
43 |
44 |

SERVER

45 | {{ show_macros.render_data_table(log.serverData) }} 46 |

POST

47 | {{ show_macros.render_data_table(log.postData) }} 48 |

GET

49 | {{ show_macros.render_data_table(log.getData) }} 50 |
51 |
52 | {% if similar_logs | length %} 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {% for log in similar_logs %} 63 | 64 | 65 | 70 | 73 | 74 | {% endfor %} 75 | 76 |
{{ knp_pagination_sortable(similar_logs, 'log.results.datetime' | trans, 'l.datetime') }}{{ knp_pagination_sortable(similar_logs, 'log.results.message' | trans, 'l.message') }}
{{ date(log.datetime) | date('F j H:i:s') }} 66 | 67 | {{ log.message }} 68 | 69 | 71 | {{ utils.render_level_label(log.level, (log.channel ~ '.' ~ log.level_name)) }} 72 |
77 | 78 | 81 | {% endif %} 82 |
83 |
84 |
85 | {% endblock %} 86 | 87 | {% macro render_data_table(data) %} 88 | 89 | 90 | {% for label, value in data %} 91 | 92 | 93 | 94 | 95 | {% endfor %} 96 | 97 |
{{ label }}{{ value }}
98 | {% endmacro %} 99 | -------------------------------------------------------------------------------- /Resources/views/layout.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | {% for type, messages in app.session.flashbag.all() %} 12 | {% for message in messages %} 13 |
14 | {{ message }} 15 |
16 | {% endfor %} 17 | {% endfor %} 18 | 19 | {% block monolog_browser_content %} 20 | {% endblock %} 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Resources/views/utils.html.twig: -------------------------------------------------------------------------------- 1 | {% macro render_level_label(level, label, attr) %} 2 | {% set level_class = '' %} 3 | {% if level == constant('Monolog\\Logger::DEBUG') %} 4 | {% elseif level == constant('Monolog\\Logger::INFO') %} 5 | {% set level_class = 'label-info' %} 6 | {% elseif level == constant('Monolog\\Logger::NOTICE') %} 7 | {% set level_class = 'label-info' %} 8 | {% elseif level == constant('Monolog\\Logger::WARNING') %} 9 | {% set level_class = 'label-warning' %} 10 | {% elseif level == constant('Monolog\\Logger::ERROR') %} 11 | {% set level_class = 'label-warning' %} 12 | {% else %} 13 | {% set level_class = 'label-important' %} 14 | {% endif %} 15 | 16 | {% set attr = (attr|default({}))|merge({ 'class': (attr.class|default('') ~ ' label ' ~ level_class)|trim}) %} 17 | 18 | {{ label }} 19 | 20 | {% endmacro %} 21 | 22 | {% macro render_count_badge(count) %} 23 | 24 | {{ count }} 25 | 26 | {% endmacro %} 27 | -------------------------------------------------------------------------------- /Tests/DependencyInjection/LexikMonologDoctrineExtensionTest.php: -------------------------------------------------------------------------------- 1 | load(array($this->getConfig()), $container = new ContainerBuilder()); 16 | 17 | // parameters 18 | $this->assertEquals('test_layout.html.twig', $container->getParameter('lexik_monolog_browser.base_layout')); 19 | $this->assertEquals('logs', $container->getParameter('lexik_monolog_browser.doctrine.table_name')); 20 | 21 | // services 22 | $this->assertTrue($container->hasDefinition('lexik_monolog_browser.doctrine_dbal.connection')); 23 | $this->assertTrue($container->hasDefinition('lexik_monolog_browser.handler.doctrine_dbal')); 24 | } 25 | 26 | protected function getConfig() 27 | { 28 | return array( 29 | 'base_layout' => 'test_layout.html.twig', 30 | 'doctrine' => array( 31 | 'table_name' => 'logs', 32 | 'connection' => array( 33 | 'driver' => 'pdo_sqlite', 34 | 'dbname' => 'monolog', 35 | 'memory' => true, 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | loadClass('Doctrine\DBAL\DriverManager'); 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lexik/monolog-browser-bundle", 3 | "type": "symfony-bundle", 4 | "description": "This Symfony2 bundle provides a Doctrine DBAL handler for Monolog and a web UI to display log entries", 5 | "keywords": ["Symfony2", "bundle", "monolog", "logger", "doctrine"], 6 | "homepage": "https://github.com/lexik/LexikMonologBrowserBundle", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Jeremy Barthe", 11 | "email": "j.barthe@lexik.fr" 12 | }, 13 | { 14 | "name": "Dev Lexik", 15 | "email": "dev@lexik.fr" 16 | } 17 | ], 18 | "minimum-stability": "dev", 19 | "require": { 20 | "php": ">=5.3.2", 21 | "symfony/framework-bundle": "~2.1", 22 | "knplabs/knp-paginator-bundle": ">=2.3" 23 | }, 24 | "require-dev": { 25 | "monolog/monolog": "*", 26 | "doctrine/dbal": "*" 27 | }, 28 | "autoload": { 29 | "psr-0": { "Lexik\\Bundle\\MonologBrowserBundle": "" } 30 | }, 31 | "target-dir": "Lexik/Bundle/MonologBrowserBundle" 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ./Tests 8 | 9 | 10 | 11 | 12 | 13 | ./ 14 | 15 | ./Resources 16 | ./Tests 17 | ./vendor 18 | 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------