Symfony error logging

Symfony comes with a minimalist PSR-3 logger: Logger. In conformance with the twelve-factor app methodology, it sends messages starting from the WARNING level to stderr.

Edit this page

Logging

Symfony comes with a minimalist PSR-3 logger: Logger.
In conformance with the twelve-factor app methodology, it sends messages starting from the
WARNING level to stderr.

The minimal log level can be changed by setting the SHELL_VERBOSITY environment variable:

SHELL_VERBOSITY value Minimum log level
-1 ERROR
1 NOTICE
2 INFO
3 DEBUG

The minimum log level, the default output and the log format can also be changed by
passing the appropriate arguments to the constructor of Logger.
To do so, override the «logger» service definition.

Logging a Message

To log a message, inject the default logger in your controller or service:

The logger service has different methods for different logging levels/priorities.
See LoggerInterface for a list of all of the methods on the logger.

Monolog

Symfony integrates seamlessly with Monolog, the most popular PHP logging
library, to create and store log messages in a variety of different places
and trigger various actions.

For instance, using Monolog you can configure the logger to do different things based on the
level of a message (e.g. send an email when an error occurs).

Run this command to install the Monolog based logger before using it:

The following sections assume that Monolog is installed.

Where Logs are Stored

By default, log entries are written to the var/log/dev.log file when you’re
in the dev environment.

In the prod environment, logs are written to STDERR PHP stream, which
works best in modern containerized applications deployed to servers without
disk write permissions.

If you prefer to store production logs in a file, set the path of your
log handler(s) to the path of the file to use (e.g. var/log/prod.log).

Handlers: Writing Logs to different Locations

The logger has a stack of handlers, and each can be used to write the log entries
to different locations (e.g. files, database, Slack, etc).

Tip

You can also configure logging «channels», which are like categories. Each
channel can have its own handlers, which means you can store different log
messages in different places. See How to Log Messages to different Files.

Symfony pre-configures some basic handlers in the default monolog.yaml
config files. Check these out for some real-world examples.

This example uses two handlers: stream (to write to a file) and syslog
to write logs using the syslog function:

  • YAML
  • XML
  • PHP

This defines a stack of handlers and each handler is called in the order that it’s
defined.

Note

If you want to override the monolog configuration via another config
file, you will need to redefine the entire handlers stack. The configuration
from the two files cannot be merged because the order matters and a merge does
not allow you to control the order.

Handlers that Modify Log Entries

Instead of writing log files somewhere, some handlers are used to filter or modify
log entries before sending them to other handlers. One powerful, built-in handler
called fingers_crossed is used in the prod environment by default. It stores
all log messages during a request but only passes them to a second handler if
one of the messages reaches an action_level. Take this example:

  • YAML
  • XML
  • PHP

Now, if even one log entry has an error level or higher, then all log entries
for that request are saved to a file via the file_log handler. That means that
your log file will contain all the details about the problematic request — making
debugging much easier!

Tip

The handler named «file_log» will not be included in the stack itself as
it is used as a nested handler of the fingers_crossed handler.

All Built-in Handlers

Monolog comes with many built-in handlers for emailing logs, sending them to Loggly,
or notifying you in Slack. These are documented inside of MonologBundle itself. For
a full list, see Monolog Configuration.

How to Rotate your Log Files

Over time, log files can grow to be huge, both while developing and on
production. One best-practice solution is to use a tool like the logrotate
Linux command to rotate log files before they become too large.

Another option is to have Monolog rotate the files for you by using the
rotating_file handler. This handler creates a new log file every day
and can also remove old files automatically. To use it, set the type
option of your handler to rotating_file:

  • YAML
  • XML
  • PHP

Using a Logger inside a Service

If your application uses service autoconfiguration,
any service whose class implements PsrLogLoggerAwareInterface will
receive a call to its method setLogger() with the default logger service
passed as a service.

If you want to use in your own services a pre-configured logger which uses a
specific channel (app by default), you can either autowire monolog channels
or use the monolog.logger tag with the channel property as explained in the
Dependency Injection reference.

Learn more

  • How to Configure Monolog to Email Errors
  • How to Log Messages to different Files
  • How to Define a Custom Logging Formatter
  • How to Add extra Data to Log Messages via a Processor
  • Handlers
  • How to Configure Monolog to Exclude Specific HTTP Codes from the Log
  • How to Configure Monolog to Display Console Messages

Логирование

Дата обновления перевода 2021-12-24

Логирование

Symfony поставляется с минималистичным логгером PSR-3: Logger.
В соответствии с двенадцатифакторной методологией приложений, он отправляет сообщения, начиная
с уровня WARNING до stderr.

Минимальный уровень лога может быть изменен путем установки переменной окружения SHELL_VERBOSITY:

???????? SHELL_VERBOSITY ??????????? ??????? ????
-1 ERROR
1 NOTICE
2 INFO
3 DEBUG

Минимальный уровень лога, вывод по умолчанию и формат лога могут также быть изменены
путем передачи соответствующих аргументов коструктору Logger.
Чтобы сделать это, переопределите определение сервиса «logger» .

Логирование сообщения

Чтобы логировать сообщение, внедрите логгер по умолчанию в ваш контроллер или сервис:

Сервис logger имеет разные методы для разных уровней/приоритетов логирования. См.
LoggerInterface, чтобы увидеть список всех методов логгера.

Monolog

Symfony безупречно интегрируется с Monolog, наиболее популярной PHP-библиотекой
логгирования, чтобы создавать и хранить сообщения логов в множестве разнообразных
место и запускать разные действия.

Например, используя Monolog вы можете сконфигурировать логгер так, чтобы он делал разные
вещи, основываясь на уровне сообщения (например, отправлять email при возникновении ошибки).

Выполните эту команду, чтобы установить логгер, основанный на Monolog, перед его использованием:

Следующие разделы предполагают, что Monolog установлен.

Где хранятся логи

По умолчанию, логи записываются в файл var/log/dev.log когда вы находитесь в
окружении dev.

В окружении prod, логи записываются в стрим STDERR PHP, что лучше всего
работает в современных контейнерных приложениях, развернутых на серверах без
разрешений записи на диск.

Если вы предпочитаете хранить логи производства в файле, установите path
обработчика(ов) ваших логов по пути файла для использования (например, var/log/prod.log).

Обработчики: Запись логов в разных локациях

Логгер имеет кучу обработчиков и каждый может быть использован для записи логов в
различных локациях (например, файлах, базе данных, Slack, и т.д.).

Tip

Вы также можете конфигурировать «каналы» логирования, которые схожи с категориями.
Каждый канал может иметь свои собственные обработчики, что означает, что вы можете
хранить разные сообщения логов в разных местах. См. Обработчики каналов.

Symfony предварительно конфигурирует некоторые базовые обработчики в файлах
конфигурации по умолчанию monolog.yaml. Посмотрите на них, чтобы увидеть
настоящие примеры.

Этот пример использует два обработчика:stream (чтобы записывать в файл)
и syslog, чтобы записывать логи, используя функцию syslog:

  • YAML
  • XML
  • PHP

Это определяет стек обработчиков и каждый обработчик называется в порядке, в
котором он определен.

Note

Если вы хотите переопределить конфигурацию monolog через другой файл конфигурации,
вам нужно будет переопределить весь стег handlers. Конфигурация из двух файлов не может
быть объединена, потому что порядок важен, а слияние не позволяет контролировать очередность.

Обработчики, которые изменяют записи логов

Вмето того, чтобы записовать файлы логов где-либо, некоторые обработчики используются,
чтобы фильтровать или изменять записи логов перед тем, как отправлять их другим
обработчикам. Один мощный встроенный обработчик под названием fingers_crossed
используется в окружении prod по умолчанию. Он сохраняет все сообщения логов во
время запроса, но передает им второму обработчику только если одно из сообщений
достигает уровня action_level. Возьмем этот пример:

  • YAML
  • XML
  • PHP

Теперь, даже если одна запись лога имеет уровень error или выше, то все записи
логов для этого запроса сохраняются в файл с помощью обработчика file_log. Это
означает, что ваш файл логов будет содержать все детали о проблематичном запросе,
что значительно облегчит отладку!

Tip

Обработчик, называнный «file_log» не будет включен в стопку, так как он
используется в качестве гнездового обработчика fingers_crossed.

Все встроенные обработчики

Monolog поставляется со многими встроенными обработчиками для отправки логов по почте,
отправки их в Loggly, или для оповещения вас в Slack. Они документируются внутри самого
MonologBundle. Для полного списка, см. Конфигурация Monolog.

Как чередовать ваши файлы логов

Со временем, файлы логов могут вырасти огромными, как во время разработки,
так и в производстве. Наилучшей практикой решения является исползование
инструмента вроде команды Linux logrotate для чередования файлов логов
до того, как они станут слишком большими.

Еще одна опция — заставить Monolog чередовать файлы за вас, используя обработчик
rotating_file. Этот обработчик создает новый файл логов каждый день, а также
может удалять старые файлы автоматически. Чтобы использовать его, просто установите
опциюtype вашего обработчика как rotating_file:

  • YAML
  • XML
  • PHP

Использование логгера внутри сервиса

Если ваше приложение использует автоконфигурацию сервиса ,
любой сервис, клас которого реализует PsrLogLoggerAwareInterface, будет получать
вызов к своему методу setLogger() с сервисами логгера по умолчанию переданными в
качестве сервиса.

Если вы хотите использовать свои обственные сервисы, предварительно сконфигурированный логгер,
который использует определенный канал (по умолчанию app), вы можете либо
автомонтировать кналы monolog или использовать тег
monolog.logger со свойством channel, как объясняется в
справочнике внедрения зависимостей .

Узнать больше

  • Как сконфигурировать Monolog так, чтобы он отправлял ошибки по электронной почте
  • Как записывать логи сообщений в разные файлы
  • Как определить пользовательский форматировщик логирования
  • Как добавлять дополнительные данные в сообщения логов через процессор
  • Обработчики
  • Как сконфигурировать Monolog для исключения конкретных HTTP-кодов из лога
  • Как сконфигурировать Monolog так, чтобы он отображал конспольные сообщения

This will be a super long article, this subject is way more complex than it originally sounds. As always, I’ll be updating this soon with more info and possibly more examples as I figure it out better.

First off, you are not limited to just one logger in Symfony. However, the most popular is Monolog and Symfony has built in support making it easier to implement. So that is what I cover here.

One thing you might want to use a logger for is logging specific exceptions or errors to specific locations. You may also just want to log some value in your code for debugging purposes to see what is happening or being sent to a particular method/function. These are good use cases for logging.

While most errors/exceptions already display with lots of info in the browser, you may be like me and want certain things logged so you can review the entire pattern over a period of time.

The steps required to log sound sort of simple:

  1. Install the logger
  2. Configure the logger, decide how you want it to actually work. You don’t have to configure it with yaml. Doing so makes it so that you can autowire loggers instead of having to create a new object passing values in each time which will usually be the same values.
  3. Get the logger into your class some how( this can vary depending on whether it is a controller or a service/class)
  4. Use the logger to log something like an exception.

Installing

Your project might already have monolog logger installed since Symfony uses it by default to log everything. It doesn’t matter. Running the line below won’t hurt even if it is installed already.

To install the Monolog Logger you simply open your terminal, navigate to the root of your project and type the following :

composer require symfony/monolog-bundle

That is all. Now the monolog bundle is installed and you can use it after you configure it. And that is where the fun and confusion begin. Symfony uses Monolog so it may already be installed  depending on how you created your project.

Handlers

First decide what types of loggers you want to use. You will probably see people using the stream type logger most often. This is really generic and is the default. I don’t like the stream handler because it just keeps growing infinitely.

I prefer storing different logs to different places. You can even store the logs in a database if you want. The good news is you can use more than one type of logger, and configure them however you want.

First off, here is a long list of handler types you can use with Monolog. This is going to be SUPER LONG. I’d rather just list it here than link to a file. This list is taken from the code on github

* Possible handler types and related configurations (brackets indicate optional params):
 *
 * - service:
 *   - id
 *
 * - stream:
 *   - path: string
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [file_permission]: int|null, defaults to null (0644)
 *   - [use_locking]: bool, defaults to false
 *
 * - console:
 *   - [verbosity_levels]: level => verbosity configuration
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [console_formatter_options]: array
 *
 * - firephp:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - browser_console:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - gelf:
 *   - publisher: {id: ...} or {hostname: ..., port: ..., chunk_size: ...}
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - chromephp:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - rotating_file:
 *   - path: string
 *   - [max_files]: files to keep, defaults to zero (infinite)
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [file_permission]: string|null, defaults to null
 *   - [use_locking]: bool, defaults to false
 *   - [filename_format]: string, defaults to '{filename}-{date}'
 *   - [date_format]: string, defaults to 'Y-m-d'
 *
 * - mongo:
 *   - mongo:
 *      - id: optional if host is given
 *      - host: database host name, optional if id is given
 *      - [port]: defaults to 27017
 *      - [user]: database user name
 *      - pass: mandatory only if user is present
 *      - [database]: defaults to monolog
 *      - [collection]: defaults to logs
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - elasticsearch:
 *   - elasticsearch:
 *      - id: optional if host is given
 *      - host: elastic search host name. Do not prepend with http(s)://
 *      - [port]: defaults to 9200
 *      - [transport]: transport protocol (http by default)
 *      - [user]: elastic search user name
 *      - [password]: elastic search user password
 *   - [index]: index name, defaults to monolog
 *   - [document_type]: document_type, defaults to logs
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - redis:
 *   - redis:
 *      - id: optional if host is given
 *      - host: 127.0.0.1
 *      - password: null
 *      - port: 6379
 *      - database: 0
 *      - key_name: monolog_redis
 *
 * - predis:
 *   - redis:
 *      - id: optional if host is given
 *      - host: tcp://10.0.0.1:6379
 *      - key_name: monolog_redis
 *
 * - fingers_crossed:
 *   - handler: the wrapped handler's name
 *   - [action_level|activation_strategy]: minimum level or service id to activate the handler, defaults to WARNING
 *   - [excluded_404s]: if set, the strategy will be changed to one that excludes 404s coming from URLs matching any of those patterns
 *   - [excluded_http_codes]: if set, the strategy will be changed to one that excludes specific HTTP codes (requires Symfony Monolog bridge 4.1+)
 *   - [buffer_size]: defaults to 0 (unlimited)
 *   - [stop_buffering]: bool to disable buffering once the handler has been activated, defaults to true
 *   - [passthru_level]: level name or int value for messages to always flush, disabled by default
 *   - [bubble]: bool, defaults to true
 *
 * - filter:
 *   - handler: the wrapped handler's name
 *   - [accepted_levels]: list of levels to accept
 *   - [min_level]: minimum level to accept (only used if accepted_levels not specified)
 *   - [max_level]: maximum level to accept (only used if accepted_levels not specified)
 *   - [bubble]: bool, defaults to true
 *
 * - buffer:
 *   - handler: the wrapped handler's name
 *   - [buffer_size]: defaults to 0 (unlimited)
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [flush_on_overflow]: bool, defaults to false
 *
 * - deduplication:
 *   - handler: the wrapped handler's name
 *   - [store]: The file/path where the deduplication log should be kept, defaults to %kernel.cache_dir%/monolog_dedup_*
 *   - [deduplication_level]: The minimum logging level for log records to be looked at for deduplication purposes, defaults to ERROR
 *   - [time]: The period (in seconds) during which duplicate entries should be suppressed after a given log is sent through, defaults to 60
 *   - [bubble]: bool, defaults to true
 *
 * - group:
 *   - members: the wrapped handlers by name
 *   - [bubble]: bool, defaults to true
 *
 * - whatfailuregroup:
 *   - members: the wrapped handlers by name
 *   - [bubble]: bool, defaults to true
 *
 * - syslog:
 *   - ident: string
 *   - [facility]: defaults to 'user', use any of the LOG_* facility constant but without LOG_ prefix, e.g. user for LOG_USER
 *   - [logopts]: defaults to LOG_PID
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - syslogudp:
 *   - host: syslogd host name
 *   - [port]: defaults to 514
 *   - [facility]: defaults to 'user', use any of the LOG_* facility constant but without LOG_ prefix, e.g. user for LOG_USER
 *   - [logopts]: defaults to LOG_PID
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [ident]: string, defaults to
 *
 * - swift_mailer:
 *   - from_email: optional if email_prototype is given
 *   - to_email: optional if email_prototype is given
 *   - subject: optional if email_prototype is given
 *   - [email_prototype]: service id of a message, defaults to a default message with the three fields above
 *   - [content_type]: optional if email_prototype is given, defaults to text/plain
 *   - [mailer]: mailer service, defaults to mailer
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [lazy]: use service lazy loading, bool, defaults to true
 *
 * - native_mailer:
 *   - from_email: string
 *   - to_email: string
 *   - subject: string
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [headers]: optional array containing additional headers: ['Foo: Bar', '...']
 *
 * - symfony_mailer:
 *   - from_email: optional if email_prototype is given
 *   - to_email: optional if email_prototype is given
 *   - subject: optional if email_prototype is given
 *   - [email_prototype]: service id of a message, defaults to a default message with the three fields above
 *   - [mailer]: mailer service id, defaults to mailer.mailer
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - socket:
 *   - connection_string: string
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *   - [persistent]: bool
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - pushover:
 *   - token: pushover api token
 *   - user: user id or array of ids
 *   - [title]: optional title for messages, defaults to the server hostname
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - raven / sentry:
 *   - dsn: connection string
 *   - client_id: Raven client custom service id (optional)
 *   - [release]: release number of the application that will be attached to logs, defaults to null
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [auto_log_stacks]: bool, defaults to false
 *   - [environment]: string, default to null (no env specified)
 *
 * - sentry:
 *   - hub_id: Sentry hub custom service id (optional)
 *
 * - newrelic:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [app_name]: new relic app name, default null
 *
 * - hipchat:
 *   - token: hipchat api token
 *   - room: room id or name
 *   - [notify]: defaults to false
 *   - [nickname]: defaults to Monolog
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [use_ssl]: bool, defaults to true
 *   - [message_format]: text or html, defaults to text
 *   - [host]: defaults to "api.hipchat.com"
 *   - [api_version]: defaults to "v1"
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - slack:
 *   - token: slack api token
 *   - channel: channel name (with starting #)
 *   - [bot_name]: defaults to Monolog
 *   - [icon_emoji]: defaults to null
 *   - [use_attachment]: bool, defaults to true
 *   - [use_short_attachment]: bool, defaults to false
 *   - [include_extra]: bool, defaults to false
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - slackwebhook:
 *   - webhook_url: slack webhook URL
 *   - channel: channel name (with starting #)
 *   - [bot_name]: defaults to Monolog
 *   - [icon_emoji]: defaults to null
 *   - [use_attachment]: bool, defaults to true
 *   - [use_short_attachment]: bool, defaults to false
 *   - [include_extra]: bool, defaults to false
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - slackbot:
 *   - team: slack team slug
 *   - token: slackbot token
 *   - channel: channel name (with starting #)
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - cube:
 *   - url: http/udp url to the cube server
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - amqp:
 *   - exchange: service id of an AMQPExchange
 *   - [exchange_name]: string, defaults to log
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - error_log:
 *   - [message_type]: int 0 or 4, defaults to 0
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - null:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - test:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - debug:
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - loggly:
 *   - token: loggly api token
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [tags]: tag names
 *
 * - logentries:
 *   - token: logentries api token
 *   - [use_ssl]: whether or not SSL encryption should be used, defaults to true
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *   - [timeout]: float
 *   - [connection_timeout]: float
 *
 * - insightops:
 *   - token: Log token supplied by InsightOps
 *   - region: Region where InsightOps account is hosted. Could be 'us' or 'eu'. Defaults to 'us'
 *   - [use_ssl]: whether or not SSL encryption should be used, defaults to true
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - flowdock:
 *   - token: flowdock api token
 *   - source: human readable identifier of the application
 *   - from_email: email address of the message sender
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - rollbar:
 *   - id: RollbarNotifier service (mandatory if token is not provided)
 *   - token: rollbar api token (skip if you provide a RollbarNotifier service id)
 *   - [config]: config values from https://github.com/rollbar/rollbar-php#configuration-reference
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * - server_log:
 *   - host: server log host. ex: 127.0.0.1:9911
 *   - [level]: level name or int value, defaults to DEBUG
 *   - [bubble]: bool, defaults to true
 *
 * All handlers can also be marked with `nested: true` to make sure they are never added explicitly to the stack
 *
 * @author Jordi Boggiano <j.boggiano@seld.be>
 * @author Christophe Coevoet <stof@notk.org>

That is a lot. Some are obvious, others you will need to google and figure out.

You can get as wild and creative as you want though.

Config location is everything

Where you place your configuration is very important. A little more hint here.

If you want to log just during Development you put the configuration inside the dev folder. /app/config/packages/dev/monolog.yaml

If you want to log only during production you put the configuration in the prod folder. /app/config/packages/prod/monolog.yaml

If you want to log in all environments you place the configuration in the main configuration folder. /app/config/packages/monolog.yaml

So if you want a specific logger that logs to a specific file to be available in all environments define it in the “packages” folder.

It isn’t too bad once you figure it out.

Here is the official documentation on how configuration works. Read it if you need more information.

An example configuration

Before getting too much further lets look at the default file I have for the configuration of monolog in the development environment.

#file name -> /config/packages/dev/monolog.yaml
monolog:
    handlers:
        main:
            type: rotating_file
            max_files: 3
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            #path: php://stderr
            level: debug
            channels: ["!event"]
        # uncomment to get logging in your browser
        # you may have to allow bigger header sizes in your Web server configuration
        #firephp:
        #    type: firephp
        #    level: info
        #chromephp:
        #    type: chromephp
        #    level: info
        console:
            type: console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine", "!console"]

Yours may differ, I have probably made changes to this and forgotten at this point or some changes in the Symfony code base may have happened.

After the word handlers you list each handler one tab level in.  Each handler gets it’s own configuration settings. Basically you pick a handler from the long list above, then configure it properly for your needs.

For local development I prefer the rotating_file option. Since it seems like it is the only one that you can put a limit on the number of files.

The stream option just infinitely fills a single file forever and ever. Unless you have some sort of log rotator as mentioned in the documentation.

If you have the time and urge you could even store the messages in a database then create a user interface in your admin panel to view the messages then delete them. Otherwise you will need some way of limiting the logs and log sizes.

The documentation refers to this list as a stack of handlers. You can have as many as you want, like I mentioned above.

Here is the default production configuration taken from here in the Symfony docs.

# file name -> /config/packages/prod/monolog.yaml
monolog:
    handlers:
        filter_for_errors:
            type: fingers_crossed
            # if *one* log is error or higher, pass *all* to file_log
            action_level: error
            handler: file_log

        # now passed *all* logs, but only if one log is error or higher
        file_log:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"

        # still passed *all* logs, and still only logs error or higher
        syslog_handler:
            type: syslog
            level: error

The Symfony docs say the following about production logs.

In the prod environment, logs are written to STDERR PHP stream, which works best in modern containerized applications deployed to servers without disk write permissions.

I’ll probably remove the line that logs locally to a file the file_log line. No need to infinitely fill a log I don’t read. You will need to decide what handlers you want in production and configure them.

Handler names

As you can see in the development configuration file above, there is a handler named “main”. You can give your handlers any name you want, but you need to tell Symfony what type it is with the type directive like you see in main.

main: 
  type: rotating_file 
  max_files: 3 
  path: "%kernel.logs_dir%/%kernel.environment%.log" 
  level: debug 
  channels: ["!event"]

The word “main” can be anything you desire, but notice how you must use type, then use one of the above handler names, then provide the options. Read more about handler names here in the Symfony docs. Basically you can call the handler anything even “asdfg” as long as you use the word “type” and list the type and other required information. More on this below.

Bubble?

That is the question!

One property that you will see on all of the types above is “bubble” Further digging into the github monolog documentation reveals the following about it.

Handlers also have a $bubble property which defines whether they block the record or not if they handled it. In this example, setting the MailHandler‘s $bubble argument to false means that records handled by the MailHandler will not propagate to the StreamHandler anymore.

Basically this means you need to define your loggers in the proper order if you want to have the handling stop at a particular logger for a particular situation. Otherwise each logger in your list does something each time and you might not want that. Like do you want errors to go to a log and also to the console during development?

One thing you may want to think about is where do you want your errors, exceptions and other logged data to be logged? This answer will be different for production vs development for sure. You may want to log just exceptions to a database in production, so that you can query it and show them in an admin user interface for example.

This part of the documentation speaks about logging to different locations.

Channels aka logging to multiple places

What are these channels?

The documentation speaks of channels. What are these channels? In the code above for the monolog.yaml development configuration you will see this line

 channels: ["!event", "!doctrine", "!console"]

Here in the documentation it says the following about channels

The Symfony Framework organizes log messages into channels. By default, there are several channels, including doctrine, event, security, request and more. The channel is printed in the log message and can also be used to direct different channels to different places/files.

Aha. So that is what those are. I wonder what the ! means? Does that mean it ignores those or does it mean it accepts those? Yes it does. Basically the above line would log everything except (event, doctrine and console.) Below you can see a list of all the channels.

So to clarify if you want a handler to log a message for a specific channel you can use the following syntax in the handler configuration.

channels: [security, doctrine]

If you want the handler to ignore a channel then you use this syntax in the handler configuration

channels: ["!event", "!doctrine", "!console"]

You can really get as insanely complex as you want with this logging stuff.

By the way how do you know what logging channels are available?

I prefer the following command, because these can be auto-wired :

php bin/console debug:autowiring

Scroll down and look for monolog listings. You will see something like this.(These can all be auto-wired)

console bin output

These are the channels you have.Basically whenever one of these experiences an issue Symfony sends out an event and the handlers you list decide if they want to do something about it. This way using the channels and multiple handlers you can log things exactly where you want them.  You can even log the same event/message multiple times in multiple locations.

As the docs mention, by default Symfony logs all events to the same location. It is up to you to change that to your projects needs.

Another useful command is the following which allows you to get detailed info about each, it also lists more, but not all are auto-wireable.

php bin/console debug:container monolog

That is interactive and you can choose one of the listings to get more information about it. I find the first method useful to find what I actually can access.

Creating Custom Channels

Sometimes you need to log a specific issue to a specific file or location. For this you need to create your own Channel. The documentation is really weak on this subject so lets dig deep into it.

I get tired of the foo bar BS so here I create a custom channel named “chicken” just so it sticks out for this example.

To create the channel you simple list it at the top of the monolog.yaml file like this.

monolog:
  channels: ['chicken']
  handlers:
    main:
      type: rotating_file
      max_files: 3
      path: "%kernel.logs_dir%/%kernel.environment%.log"
      #path: php://stderr
      level: debug
      channels: [ "!event" ]

A little confusing how you use that syntax in more than one location. In one it listens for channels in another it creates channels.

Wait what?

Maybe channel_list or something else to differentiate would have been better?

Placing the directive above the rest of the handler code creates the new channel or channels.  I don’t know if this is a requirement, but I know it works. To create more than one channel create strings separated by commas.

Now to check if anything happened I will use this command


php bin/console debug:autowiring

Now we have a chicken logger.

Now you can see “monolog.logger.chicken” logger that was created with that one line in the monolog.yaml configuration file.

That is how it is done.

How to use the logger?

There are two ways to use the logger, one you didn’t have to do any of this to use, the other is autowiring, which is why we did all of this for. The other way is to just initiate it and pass the values to the constructor like the documentation for Monolog shows.

Even though all that configuration seems like a pain, in the end it makes things easier by allowing us to auto-wire the logger we want. This saves a lot of having to create the same boilerplate code over and over all throughout our app.

Taking another look at the output of the bin/console debug:container command from above we can see the following

Lets now use the chickenLogger

Each line after the first monolog.logger contains what looks like a PHP variable. It is in fact a PHP variable, one you can use for type hinting. How this works is you type hint LoggerInterface, then use which variable you want and Symfony automagically loads it for you.

farth too easy meme

It feels too easy to be true

Back to the chickenLoggerExample. What I want to do with the chickenLogger is use it to log exceptions to their own exceptions log for easier viewing. This is useful in a situation like using a library to create and convert images.

class UserImageProcessor
{

    private LoggerInterface $chickenLogger;

    public function __construct(LoggerInterface $chickenLogger){
        $this->chickenLogger = $chickenLogger;
        $this->chickenLogger->debug('lets log some chickens');
    }

See in the constructor how I passed the variable $chickenLogger and now Symfony passes me the chickenLogger Channel and I can now log chickens. Logging chickens is a very handy feature in the real world.

And below is the whole monolog.yaml file I am using to log my chickens.

monolog:
  channels: ['chicken']
  handlers:
    main:
      type: rotating_file
      max_files: 3
      path: "%kernel.logs_dir%/%kernel.environment%.log"
      #path: php://stderr
      level: debug
      channels: [ "!event", "!chicken" ]

    exceptions:
      type: rotating_file
      max_files: 2
      path: "%kernel.logs_dir%/exceptions.log"
      file_permission: '765'
      level: debug
      channels: ['chicken']


    # uncomment to get logging in your browser
    # you may have to allow bigger header sizes in your Web server configuration
    #firephp:
    #    type: firephp
    #    level: info
    #chromephp:
    #    type: chromephp
    #    level: info
    console:
      type: console
      process_psr_3_messages: false
      channels: [ "!event", "!doctrine", "!console" ]

Notice I used the file permissions option. That is a handy one. See the exceptions handler is using the chicken channel. Again I could have named the “exceptions:” section anything even “chirpy-birds” symfony doesn’t care, only the directives matter.

Now when I log my chickens, the main handler section will ignore the chickens, but my exceptions section will log my chickens and so will the console. I’d have to add “!chicken” to the consoles “channels” section or I can add “bubbles: false” to the exceptions section to stop it.

Now you know!!!

Links about Loggers

Logging to Rollbar -> Helps you organize and view logs and much more.

Logging with Monolog -> A short article

Logging -> Documentation link ->overall general information

How to log messages to different files -> more from the documentations

Using the logger -> more from the docs on how to use it.

Logging

Symfony comes with a minimalist PSR-3 logger: :class:`Symfony\Component\HttpKernel\Log\Logger`.
In conformance with the twelve-factor app methodology, it sends messages starting from the
WARNING level to stderr.

The minimal log level can be changed by setting the SHELL_VERBOSITY environment variable:

SHELL_VERBOSITY value Minimum log level
-1 ERROR
1 NOTICE
2 INFO
3 DEBUG

The minimum log level, the default output and the log format can also be changed by
passing the appropriate arguments to the constructor of :class:`Symfony\Component\HttpKernel\Log\Logger`.
To do so, :ref:`override the «logger» service definition <service-psr4-loader>`.

Logging a Message

To log a message, inject the default logger in your controller or service:

use PsrLogLoggerInterface;

public function index(LoggerInterface $logger)
{
    $logger->info('I just got the logger');
    $logger->error('An error occurred');

    $logger->critical('I left the oven on!', [
        // include extra "context" info in your logs
        'cause' => 'in_hurry',
    ]);

    // ...
}

The logger service has different methods for different logging levels/priorities.
See LoggerInterface for a list of all of the methods on the logger.

Monolog

Symfony integrates seamlessly with Monolog, the most popular PHP logging
library, to create and store log messages in a variety of different places
and trigger various actions.

For instance, using Monolog you can configure the logger to do different things based on the
level of a message (e.g. :doc:`send an email when an error occurs </logging/monolog_email>`).

Run this command to install the Monolog based logger before using it:

$ composer require symfony/monolog-bundle

The following sections assume that Monolog is installed.

Where Logs are Stored

By default, log entries are written to the var/log/dev.log file when you’re
in the dev environment.

In the prod environment, logs are written to STDERR PHP stream, which
works best in modern containerized applications deployed to servers without
disk write permissions.

If you prefer to store production logs in a file, set the path of your
log handler(s) to the path of the file to use (e.g. var/log/prod.log).

Handlers: Writing Logs to different Locations

The logger has a stack of handlers, and each can be used to write the log entries
to different locations (e.g. files, database, Slack, etc).

Tip

You can also configure logging «channels», which are like categories. Each
channel can have its own handlers, which means you can store different log
messages in different places. See :doc:`/logging/channels_handlers`.

Symfony pre-configures some basic handlers in the default monolog.yaml
config files. Check these out for some real-world examples.

This example uses two handlers: stream (to write to a file) and syslog
to write logs using the :phpfunction:`syslog` function:

.. configuration-block::

    .. code-block:: yaml

        # config/packages/prod/monolog.yaml
        monolog:
            handlers:
                # this "file_log" key could be anything
                file_log:
                    type: stream
                    # log to var/log/(environment).log
                    path: "%kernel.logs_dir%/%kernel.environment%.log"
                    # log *all* messages (debug is lowest level)
                    level: debug

                syslog_handler:
                    type: syslog
                    # log error-level messages and higher
                    level: error

    .. code-block:: xml

        <!-- config/packages/prod/monolog.xml -->
        <?xml version="1.0" encoding="UTF-8" ?>
        <container xmlns="http://symfony.com/schema/dic/services"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:monolog="http://symfony.com/schema/dic/monolog"
            xsi:schemaLocation="http://symfony.com/schema/dic/services
                https://symfony.com/schema/dic/services/services-1.0.xsd
                http://symfony.com/schema/dic/monolog
                https://symfony.com/schema/dic/monolog/monolog-1.0.xsd">

            <monolog:config>
                <!-- this "file_log" key could be anything -->
                <monolog:handler name="file_log"
                    type="stream"
                    path="%kernel.logs_dir%/%kernel.environment%.log"
                    level="debug"/><!-- log *all* messages (debug is lowest level) -->

                <monolog:handler name="syslog_handler"
                    type="syslog"
                    level="error"/><!-- log error-level messages and higher -->
            </monolog:config>
        </container>

    .. code-block:: php

        // config/packages/prod/monolog.php
        use SymfonyConfigMonologConfig;

        return static function (MonologConfig $monolog) {
            // this "file_log" key could be anything
            $monolog->handler('file_log')
                ->type('stream')
                // log to var/logs/(environment).log
                ->path('%kernel.logs_dir%/%kernel.environment%.log')
                // log *all* messages (debug is lowest level)
                ->level('debug');

            $monolog->handler('syslog_handler')
                ->type('syslog')
                // log error-level messages and higher
                ->level('error');
        };

This defines a stack of handlers and each handler is called in the order that it’s
defined.

Note

If you want to override the monolog configuration via another config
file, you will need to redefine the entire handlers stack. The configuration
from the two files cannot be merged because the order matters and a merge does
not allow you to control the order.

Handlers that Modify Log Entries

Instead of writing log files somewhere, some handlers are used to filter or modify
log entries before sending them to other handlers. One powerful, built-in handler
called fingers_crossed is used in the prod environment by default. It stores
all log messages during a request but only passes them to a second handler if
one of the messages reaches an action_level. Take this example:

.. configuration-block::

    .. code-block:: yaml

        # config/packages/prod/monolog.yaml
        monolog:
            handlers:
                filter_for_errors:
                    type: fingers_crossed
                    # if *one* log is error or higher, pass *all* to file_log
                    action_level: error
                    handler: file_log

                # now passed *all* logs, but only if one log is error or higher
                file_log:
                    type: stream
                    path: "%kernel.logs_dir%/%kernel.environment%.log"

                # still passed *all* logs, and still only logs error or higher
                syslog_handler:
                    type: syslog
                    level: error

    .. code-block:: xml

        <!-- config/packages/prod/monolog.xml -->
        <?xml version="1.0" encoding="UTF-8" ?>
        <container xmlns="http://symfony.com/schema/dic/services"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:monolog="http://symfony.com/schema/dic/monolog"
            xsi:schemaLocation="http://symfony.com/schema/dic/services
                https://symfony.com/schema/dic/services/services-1.0.xsd
                http://symfony.com/schema/dic/monolog
                https://symfony.com/schema/dic/monolog/monolog-1.0.xsd">

            <monolog:config>
                <!-- if *one* log is error or higher, pass *all* to file_log -->
                <monolog:handler name="filter_for_errors"
                    type="fingers_crossed"
                    action-level="error"
                    handler="file_log"
                />

                <!-- now passed *all* logs, but only if one log is error or higher -->
                <monolog:handler name="file_log"
                    type="stream"
                    path="%kernel.logs_dir%/%kernel.environment%.log"
                    level="debug"
                />

                <!-- still passed *all* logs, and still only logs error or higher -->
                <monolog:handler name="syslog_handler"
                    type="syslog"
                    level="error"
                />
            </monolog:config>
        </container>

    .. code-block:: php

        // config/packages/prod/monolog.php
        use SymfonyConfigMonologConfig;

        return static function (MonologConfig $monolog) {
            $monolog->handler('filter_for_errors')
                ->type('fingers_crossed')
                // if *one* log is error or higher, pass *all* to file_log
                ->actionLevel('error')
                ->handler('file_log')
            ;

            // now passed *all* logs, but only if one log is error or higher
            $monolog->handler('file_log')
                ->type('stream')
                ->path('%kernel.logs_dir%/%kernel.environment%.log')
                ->level('debug')
            ;

            // still passed *all* logs, and still only logs error or higher
            $monolog->handler('syslog_handler')
                ->type('syslog')
                ->level('error')
            ;
        };

Now, if even one log entry has an error level or higher, then all log entries
for that request are saved to a file via the file_log handler. That means that
your log file will contain all the details about the problematic request — making
debugging much easier!

Tip

The handler named «file_log» will not be included in the stack itself as
it is used as a nested handler of the fingers_crossed handler.

All Built-in Handlers

Monolog comes with many built-in handlers for emailing logs, sending them to Loggly,
or notifying you in Slack. These are documented inside of MonologBundle itself. For
a full list, see Monolog Configuration.

How to Rotate your Log Files

Over time, log files can grow to be huge, both while developing and on
production. One best-practice solution is to use a tool like the logrotate
Linux command to rotate log files before they become too large.

Another option is to have Monolog rotate the files for you by using the
rotating_file handler. This handler creates a new log file every day
and can also remove old files automatically. To use it, set the type
option of your handler to rotating_file:

.. configuration-block::

    .. code-block:: yaml

        # config/packages/prod/monolog.yaml
        monolog:
            handlers:
                main:
                    type:  rotating_file
                    path:  '%kernel.logs_dir%/%kernel.environment%.log'
                    level: debug
                    # max number of log files to keep
                    # defaults to zero, which means infinite files
                    max_files: 10

    .. code-block:: xml

        <!-- config/packages/prod/monolog.xml -->
        <?xml version="1.0" encoding="UTF-8" ?>
        <container xmlns="http://symfony.com/schema/dic/services"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xmlns:monolog="http://symfony.com/schema/dic/monolog"
            xsi:schemaLocation="http://symfony.com/schema/dic/services
                https://symfony.com/schema/dic/services/services-1.0.xsd
                http://symfony.com/schema/dic/monolog
                https://symfony.com/schema/dic/monolog/monolog-1.0.xsd">

            <monolog:config>
                <!-- "max-files": max number of log files to keep
                     defaults to zero, which means infinite files -->
                <monolog:handler name="main"
                    type="rotating_file"
                    path="%kernel.logs_dir%/%kernel.environment%.log"
                    level="debug"
                    max-files="10"
                />
            </monolog:config>
        </container>

    .. code-block:: php

        // config/packages/prod/monolog.php
        use SymfonyConfigMonologConfig;

        return static function (MonologConfig $monolog) {
            $monolog->handler('main')
                ->type('rotating_file')
                ->path('%kernel.logs_dir%/%kernel.environment%.log')
                ->level('debug')
                // max number of log files to keep
                // defaults to zero, which means infinite files
                ->maxFiles(10);
        };

Using a Logger inside a Service

If your application uses :ref:`service autoconfiguration <services-autoconfigure>`,
any service whose class implements PsrLogLoggerAwareInterface will
receive a call to its method setLogger() with the default logger service
passed as a service.

If you want to use in your own services a pre-configured logger which uses a
specific channel (app by default), you can either :ref:`autowire monolog channels <monolog-autowire-channels>`
or use the monolog.logger tag with the channel property as explained in the
:ref:`Dependency Injection reference <dic_tags-monolog>`.

Adding extra Data to each Log (e.g. a unique request token)

Monolog also supports processors: functions that can dynamically add extra
information to your log entries.

See :doc:`/logging/processors` for details.

Learn more

.. toctree::
    :maxdepth: 1

    logging/monolog_email
    logging/channels_handlers
    logging/formatter
    logging/processors
    logging/handlers
    logging/monolog_exclude_http_codes
    logging/monolog_console

Сразу после установки фремворка Symfony 5 , в нем уже поставляется возможность минималистского логирования PSR-3: Logger. В соответствии с методологией приложения из двенадцати факторов приложение отправляет сообщения, начиная с уровня WARNING, в stderr.

Минимальный уровень журнала можно изменить, установив переменную среды SHELL_VERBOSITY:

Значение SHELL_VERBOSITY Минимальный уровень логирования
-1 ERROR
1 NOTICE
2 INFO
3 DEBUG

Минимальный уровень журнала, вывод по умолчанию и формат журнала также можно изменить, передав соответствующие аргументы конструктору Logger.

Чтобы записать сообщение в лог, передайте логгер как зависимость в ваш контроллер:

use PsrLogLoggerInterface;

public function index(LoggerInterface $logger)
{
    $logger->info('I just got the logger');
    $logger->error('An error occurred');

    $logger->critical('I left the oven on!', [
        // include extra "context" info in your logs
        'cause' => 'in_hurry',
    ]);

    // ...
}

Сервис Logger имеет разные методы для разных уровней / приоритетов логирования сообщений. Посмотрите LoggerInterface для списка всех методов в регистраторе.

Установка Monolog в Symfony 5.

Symfony легко интегрируется с Monolog, самой популярной библиотекой журналирования PHP, для создания и хранения сообщений журнала в разных местах и запуска различных действий.

Например, используя Monolog, вы можете настроить логирование на разные действия в зависимости от уровня сообщения (например, отправка электронного письма при возникновении ошибки).

Выполните эту команду, чтобы установить логирование на основе пакета Monolog перед его использованием:

 composer require symfony/monolog-bundle

Где храняться логи в Symfony?

По умолчанию записи журнала записываются в файл var/log/dev.log, когда вы находитесь в среде разработчика dev.

В среде prod журналы записываются в var/log/prod.log, но только во время запроса, в котором была сделана ошибка или запись в журнале с высоким приоритетом (error() , critical(), alert() или emergency()).

Чтобы контролировать это, вы будете настраивать различные обработчики, которые обрабатывают записи журнала, иногда изменяют их и, в конечном счете, сохраняют их.

Обработчики: запись логов сообщений в разные места.

Логгер имеет стек обработчиков, и каждый из них может быть использован для записи записей журнала в разные места (например, файлы, база данных, Slack и т.д.).

Вы также можете настроить логирование «каналов», которые похожи на категории. Каждый канал может иметь свои собственные обработчики, что означает, что вы можете хранить разные сообщения журнала в разных местах.

Symfony предварительно настраивает некоторые базовые обработчики в стандартных файлах конфигурации monolog.yaml. Проверьте это для некоторых реальных примеров.

В этом примере используются два обработчика: stream (для записи в файл) и syslog для записи журналов с использованием функции syslog:

# config/packages/prod/monolog.yaml
monolog:
    handlers:
        # this "file_log" key could be anything
        file_log:
            type: stream
            # log to var/log/(environment).log
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            # log *all* messages (debug is lowest level)
            level: debug

        syslog_handler:
            type: syslog
            # log error-level messages and higher
            level: error

Это определяет стек обработчиков, и каждый обработчик вызывается в порядке, который он определил.

Обработчики, которые изменяют записи в журнале.

Вместо того, чтобы записывать файлы журнала где-нибудь, некоторые обработчики используются для фильтрации или изменения записей журнала перед отправкой их другим обработчикам.

По умолчанию в среде prod используется один мощный встроенный обработчик finger_crossed. Он сохраняет все сообщения журнала во время запроса, но передает их только второму обработчику, если одно из сообщений достигает уровня action_level. Например как в этом примере:

# config/packages/prod/monolog.yaml
monolog:
    handlers:
        filter_for_errors:
            type: fingers_crossed
            # if *one* log is error or higher, pass *all* to file_log
            action_level: error
            handler: file_log

        # now passed *all* logs, but only if one log is error or higher
        file_log:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"

        # still passed *all* logs, and still only logs error or higher
        syslog_handler:
            type: syslog
            level: error

Теперь, если хотя бы одна запись журнала имеет уровень error или выше, тогда все записи журнала для этого запроса сохраняются в файл с помощью обработчика file_log. Это означает, что ваш файл журнала будет содержать все подробности о проблемном запросе, что значительно упростит отладку!

Обработчик с именем «file_log» не будет включен в сам стек, поскольку он используется как вложенный обработчик обработчика finger_crossed.

Если вы хотите переопределить конфигурацию monolog через другой файл конфигурации, вам нужно будет переопределить весь стек обработчиков. Конфигурация из двух файлов не может быть объединена, потому что порядок имеет значение, и объединение не позволяет контролировать заказ.

Еще больше встроенных обработчиков handlers.

Monolog поставляется со многими встроенными обработчиками для отправки журналов по электронной почте, отправки их в Loggly или уведомления в Slack. Они задокументированы внутри самого MonologBundle. Полный список см. В разделе «Конфигурация Monolog».

Как чередовать файлы логов.

Со временем файлы журнала могут стать огромными как при разработке, так и в производстве. Одно из лучших решений — использовать такой инструмент, как команда logrotate Linux, чтобы чередовать файлы журнала, прежде чем они станут слишком большими.

Другой вариант — заставить Monolog чередовать файлы за вас с помощью обработчика rotating_file. Этот обработчик создает новый файл журнала каждый день, а также может автоматически удалять старые файлы. Чтобы использовать его, установите опцию type вашего обработчика равным rotating_file:

# config/packages/prod/monolog.yaml
monolog:
    handlers:
        main:
            type:  rotating_file
            path:  '%kernel.logs_dir%/%kernel.environment%.log'
            level: debug
            # max number of log files to keep
            # defaults to zero, which means infinite files
            max_files: 10

In this example we are going to catch all our custom exceptions and log them in a log file. Apart from that, we will return an appropriate HTTP status code and text message to the user.

When developing an API application, catch all exceptions and throw your custom ones. This way, you have full control over «proper» exception handling as shown in this example. The whole point here is that, returning sensible responses to the end user, not accidental 500 codes!

monolog.yaml

Add channels: ['api'] entry to all «monolog.yaml» files so that our custom errors can be logged in «test».log», «dev».log» and «prod».log» files. Or, just create a custom handler to log them in custom log file instead.

monolog:
channels: ['api']
handlers:
...

ApiExceptionInterface

All your custom exceptions should implement this.

declare(strict_types=1);

namespace AppException;

interface ApiExceptionInterface
{
}

CountryException

All your custom exceptions should be same as this.

declare(strict_types=1);

namespace AppException;

use Exception;
use SymfonyComponentHttpKernelExceptionHttpException;

class CountryException extends HttpException implements ApiExceptionInterface
{
public function __construct(string $message, int $code, Exception $previous = null)
{
parent::__construct($code, $message, $previous);
}
}

ExceptionListener

If your API returns request validation errors as in JSON form, you can modify the response message below to meet your needs, such as returning a JSON response instead.

declare(strict_types=1);

namespace AppEvent;

use AppExceptionApiExceptionInterface;
use Exception;
use PsrLogLoggerInterface;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentHttpKernelEventGetResponseForExceptionEvent;

class ExceptionListener
{
private $logger;

public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}

public function onKernelException(GetResponseForExceptionEvent $event)
{
if (!$event->getException() instanceof ApiExceptionInterface) {
return;
}

$response = new Response($event->getException()->getMessage(), $event->getException()->getStatusCode());

$event->setResponse($response);

$this->log($event->getException());
}

private function log(ApiExceptionInterface $exception)
{
$log = [
'code' => $exception->getStatusCode(),
'message' => $exception->getMessage(),
'called' => [
'file' => $exception->getTrace()[0]['file'],
'line' => $exception->getTrace()[0]['line'],
],
'occurred' => [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
],
];

if ($exception->getPrevious() instanceof Exception) {
$log += [
'previous' => [
'message' => $exception->getPrevious()->getMessage(),
'exception' => get_class($exception->getPrevious()),
'file' => $exception->getPrevious()->getFile(),
'line' => $exception->getPrevious()->getLine(),
],
];
}

$this->logger->error(json_encode($log));
}
}

services:
AppEventExceptionListener:
arguments:
- '@monolog.logger.api'
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }

CountryController

declare(strict_types=1);

namespace AppController;

use AppServiceCountryServiceInterface;
use SensioBundleFrameworkExtraBundleConfigurationMethod;
use SensioBundleFrameworkExtraBundleConfigurationRoute;
use SymfonyComponentHttpFoundationJsonResponse;
use SymfonyComponentHttpFoundationResponse;

/**
* @Route("/countries")
*/
class CountryController
{
private $countryService;

public function __construct(CountryServiceInterface $countryService)
{
$this->countryService = $countryService;
}

/**
* @Route("/{id}")
* @Method({"GET"})
*/
public function getOneById(int $id): Response
{
$country = $this->countryService->getOneById($id);

return new JsonResponse($country, Response::HTTP_OK);
}

/**
* @Route("/{id}")
* @Method({"PATCH"})
*/
public function updateOneById(int $id): Response
{
$this->countryService->updateOneById($id);

return new JsonResponse(null, Response::HTTP_NO_CONTENT);
}
}

CountryServiceInterface

declare(strict_types=1);

namespace AppService;

interface CountryServiceInterface
{
public function getOneById(int $id);

public function updateOneById(int $id);
}

CountryService

declare(strict_types=1);

namespace AppService;

use AppEntityCountry;
use AppExceptionCountryException;
use RuntimeException;
use SymfonyComponentHttpFoundationResponse;

class CountryService implements CountryServiceInterface
{
public function getOneById(int $id)
{
$country = 'I am not a Country object';
if (!$country instanceof Country) {
throw new CountryException(
sprintf('Country "%d" was not found.', $id),
Response::HTTP_NOT_FOUND
);
}

return $country;
}

public function updateOneById(int $id)
{
try {
$this->internalException();
} catch (RuntimeException $e) {
throw new CountryException(
sprintf('Country "%d" state was changed before.', $id),
Response::HTTP_CONFLICT,
$e
);
}
}

private function internalException()
{
throw new RuntimeException('An internal error was encountered.');
}
}

Tests

This is what end user gets.

$ curl -i -X GET http://192.168.99.20:81/api/v1/countries/11

HTTP/1.1 404 Not Found
Server: nginx/1.6.2
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache, private
Date: Fri, 06 Jul 2018 22:14:07 GMT

Country "11" was not found.

$ curl -i -X PATCH http://192.168.99.20:81/api/v1/countries/1 -H 'Content-Type: application/json' -d '{"code":"gb"}'

HTTP/1.1 409 Conflict
Server: nginx/1.6.2
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache, private
Date: Fri, 06 Jul 2018 22:14:56 GMT

Country "1" state was changed before.

This is what our logs looks like.

[2018-07-07 09:39:35] api.ERROR: {"code":404,"message":"Country "1" was not found.","called":{"file":"/srv/www/api/src/Controller/CountryController.php","line":31},"occurred":{"file":"/srv/www/api/src/Service/CountryService.php","line":18}} [] []
[2018-07-07 09:39:48] api.ERROR: {"code":409,"message":"Country "1" state was changed before.","called":{"file":"/srv/www/api/src/Controller/CountryController.php","line":42},"occurred":{"file":"/srv/www/api/src/Service/CountryService.php","line":32},"previous":{"message":"An internal error was encountered.","exception":"RuntimeException","file":"/srv/www/api/src/Service/CountryService.php","line":42}} [] []

Clearer version.

{
"code": 404,
"message": "Country "1" was not found.",
"called": {
"file": "/srv/www/api/src/Controller/CountryController.php",
"line": 31
},
"occurred": {
"file": "/srv/www/api/src/Service/CountryService.php",
"line": 18
}
}

{
"code": 409,
"message": "Country "1" state was changed before.",
"called": {
"file": "/srv/www/api/src/Controller/CountryController.php",
"line": 42
},
"occurred": {
"file": "/srv/www/api/src/Service/CountryService.php",
"line": 32
},
"previous": {
"message": "An internal error was encountered.",
"exception": "RuntimeException",
"file": "/srv/www/api/src/Service/CountryService.php",
"line": 42
}
}

Logging is an important aspect of software development. In fact, in my opinion, it’s just as important as testing. Why?

Logs become the only source of truth when things go wrong.

That’s why in this post, you’ll learn more about logging, why you’d do it, and how to log using the Symfony framework.

This blog covers:

  • The simplest way of logging in Symfony
  • What to log and where to log it
  • How to use the monolog logger in Symfony

What Is Symfony?

Symfony is an open-source framework that thousands of developers have contributed to. Fabien Potencier originally created it, and SensioLabs maintains it. It has a huge community, with more than 600,000 developers from 120 countries.

Symfony is one of the most popular PHP frameworks for building large applications. It’s built on many decoupled, reusable PHP libraries called components. Symfony components are widely used in most PHP applications today. It’s the core component behind Adobe Commerce (previously Magento),  PrestaShop, Drupal, and others. Laravel, probably the most popular PHP framework today, uses many Symfony components.

How Can You Install It?

One of the easiest ways to install the Symfony framework is through the Symfony client. So before you go further, make sure you have PHP installed, whether it’s on macOS (here’s how), Ubuntu (here’s how), or Windows (here’s how).

You’ll also want to have Composer installed. Composer is a package manager for PHP. You can install it by following their official documentation. From the command line, run the code below to install Symfony client.

curl -sS https://get.symfony.com/cli/installer | bash

If you run the command above successfully, your screen will look like this:

To complete the installation, you’ll need to add Symfony’s path to your shell.

To do that, open your terminal and run:

  export PATH="$HOME/.symfony/bin:$PATH"

Let’s verify the installation by executing:

$ symfony -v

If properly installed, the output of your screen will be similar to the image below.

A Simple Console App With Symfony

For the demo, we’ll build a simple console application using the Symfony framework. Jorge Luis Borges said, “Life itself is a quotation.” So, in this application let’s display random quotes every five seconds.

You can keep this application simple or extend it further. For example, you can display it on your TV screen. (Your guests will admire you, pulling you aside to say, “This is amazing! Are you some sort of genius?”)Now that you have the Symfony client installed, you can start a brand-new app with the command below:

symfony new random-quote

The command above creates a project in the directory “random-quote”  with the project dependencies installed. It should less than two minutes, provided you have good internet speed.

Guzzle is a package in PHP that allows you to make HTTP requests to web services. Our goal is for demo app will get random quotes from an external web service using Guzzle. You can install it using Composer.

Let’s create a new HTTP client and define the base URL where you’ll obtain quotes.

Go to the root directory of the demo app and run:

 composer require guzzlehttp/guzzle

Create a new folder named Command in the src directory.

...
├── public
│   └── index.php
├── src
│   ├── Command
│   ├── Controller
│   └── Kernel.php
├── symfony.lock
...

Create a new class called Quote with this content:

//src/Command/Quote.php
<?php declare(strict_types=1);

namespace AppCommand;

use GuzzleHttpClient;

final class Quote
{
    /**
     * @var Client
     */
    private $client;

    /**
     * Quote constructor.
     */
    public function __construct()
    {
        $this->client = new Client(['base_uri' => 'https://api.quotable.io', 'verify' => false]);
    }

    /**
     * @return string
     */
    public function getQuote(): string
    {
        $response = $this->client->get('random');
        $contents = json_decode($response->getBody()->getContents(), true);

        return $contents['content'];
    }
}

You don’t want to verify the SSL certificate, so set verify to false.

To summarize, in the getQuote method above, the code performs the following tasks:

  • It makes a GET request to obtain a random quote.
  • Then it decodes the JSON response.
  • Finally, it returns the content of the API response.

Creating a Symfony Command

Symfony provides commands through bin/console. Commands are one of the ways you can interact with your application. Let’s create one now.

In the Command directory, create a new class QuoteCommand with this content:

//src/Command/QuoteCommand.php

<?php declare(strict_types=1);

namespace AppCommand;

use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;

final class QuoteCommand extends Command
{
    /**
     * @var Quote
     */
    private $qouteService;

    /**
     * @var string
     */
    protected static $defaultName = 'app:get-quote';

    public function __construct(Quote $quoteService)
    {
        $this->quoteService = $quoteService;
        parent::__construct(null);
    }

    /**
     * @return void
     */
    protected function configure()
    {
        $this->setDescription('This command gets a random quote from quotable.io.');
    }

    /**
     * @param InputInterface $input
     * @param OutputInterface $output
     * @return int|null|void
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln([
            'A doze of inspiration to power your day',
            '========================================',
            '',
        ]);
        $quoteSection = $output->section();

        while (true) {
            $quote = $this->quoteService->getQuote();
            $quoteSection->overwrite($quote);
            sleep(5);
        }

    }
}

In this code snippet, we injected the Quote class in QuoteCommand and wrapped it in a while loop so it runs continuously. A command in Symfony must implement two methods: configure and execute. 

  • The function configure allows you to describe what the command does and some other parameters like name and help text.
  • The function execute contains the logic that will be executed when the command is invoked.

The execute method pulls a new random quotation every five seconds using the class you injected.

Let’s test it out! If you run the application with the command

php bin/console app:get-quote

and everything goes fine, your console should become a powerful inspirational tool.

The output should be similar to the image below:

The Simplest Possible Logging That Could Work in Symfony

Now, if remember what I said at the very beginning of the post, you’ll know your random-quote app lacks an important feature: logging. Once the app leaves your development environment, it will be difficult to know when something’s going wrong. For example, if the quote API stops responding to your requests, you’d never realize it.

Symfony comes with a minimal logger that implements PSR-3. PSR is an acronym for “PHP standards recommendation.” Its aim is to maintain compatibility between the different logger services that implement the PSR-3 logger interface.

To use this logger, you need to configure it in config/services.yaml file. So let’s update config/services.yaml with:

#file config/services.yaml parameters: services: # default configuration for services in *this* file _defaults: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. logger: class: SymfonyComponentHttpKernelLogLogger arguments: ['info','var/log/dev.log'] # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name App: resource: '../src/*' exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' # controllers are imported separately to make sure services can be injected # as action arguments even if you don't extend any base controller class AppController: resource: '../src/Controller' tags: ['controller.service_arguments']

Now you can use the logger service in your code.

Update Quote class with this content:

<?php declare(strict_types=1);

namespace AppCommand;

use GuzzleHttpClient;
use PsrLogLoggerInterface;

final class Quote
{
    /**
     * @var Client
     */
    private $client;

    /**
     * @var LoggerInterface
     */
    private $logger;

    /**
     * Quote constructor.
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
        $this->client = new Client(['base_uri' => 'https://api.quotable.io', 'verify' => false]);
    }

    /**
     * @return string
     */
    public function getQuote(): string
    {
        $this->logger->info('Getting a new quote');
        $response = $this->client->get('random');
        $contents = json_decode($response->getBody()->getContents(), true);

        $this->logger->info("Got a new quote: {$contents['content']}");

        return $contents['content'];
    }
}

Notice that something fundamental has changed about our code. Now the Quote class gets a parameter of type LoggerInterface through its constructor.

Symfony has a cool feature called auto-wiring. Autowiring allows you to retrieve an instance of a class from a dependency injection container by type-hinting its interface in functions.

Using this injected dependency, we’re able to use the logger class. Take a look at the getQuote method. You’ll see we logged twice: immediately before and after retrieving a quotation.

If you restart the application, you’ll see the logger class in action. If the previous instance of your app is still running in your console, the new changes won’t be available to it yet. You’ll need to kill it and then restart by running the code below in your console:

php bin/console app:get-quote

If you look at the file “var/log/dev.log” after restarting the app, you should see some logs already written. The log content should be similar to this:

2019-07-19T05:31:09+00:00 [info] Getting a new quote
2019-07-19T05:31:09+00:00 [info] Got a new quote: The aim of life is self-development. To realize one's nature perfectly—that is what each of us is here for.

What Is Logging?

Let’s zoom out from the code and think a little more theoretically. What does logging really mean, and why should you do it?

Colin Eberhardt provides an answer in his post “The Art of Logging.”

Logging is the process of recording application actions and state to a secondary interface.

So, what are the ramifications of recording this information? Well, logging is all about providing visibility into apps in development or in production. You can’t get this visibility without recording certain actions and states.

Have you ever used a desktop or mobile app that crashed unexpectedly? Nine out 10 times this happens, you’ll see a prompt that asks you to click a button and send reports (logs) to the app’s developer. This is necessary because no developer has the sorcery to see what happens in production without logs. Logs become your glasses; they’re the key to seeing what happened and when.

But as important as logging is, if you don’t do it right, it can lead you into trouble.

How Not to Log

First off, you should know there are ways you shouldn’t log. For instance, don’t include sensitive data or personally identifying information in your log. User input often contains sensitive data.

Here’s an example of an incorrectly done log:

[2019-07-11 20:29:59] User with username:johndoe@example.com and password:123345 logged in at 126374846489 with IP 127.0.0.1

There are so many things wrong with the example above. For one thing, the log contains sensitive credentials: username and password. This is not a good practice.

If you ever need to log user input that contains sensitive data, you should mask the sensitive data.

How to Log Meaningfully

Logs can grow extensive over time. It’s tedious to sift through enormous numbers of log entries to find a specific message of interest. Even worse is if you find the message, but you don’t have enough context to know what really happened.  A log that that lacks context provides no value to its users.

That’s why you don’t want to log like this:

[2019-07-11 20:29:59] User login failed
[2019-07-11 20:29:59] Registration was successful

When you log, ask yourself two questions:

1. What purpose would this log serve?
2. Does the log include enough context to be useful?

Structure Your Log

Before you choose a structure for your log, it’s important to understand who its users will be. Most likely, its readers will be humans and machines.

Since that’s the case, your log structure should be in a format that’s readable by humans and can be parsed by machines.

Most importantly, you’ll want to stay consistent with your choice. For example, if you choose to log in JSON format, ensure you do so every time.

Use Log Levels

Log levels help you put log messages into groups that you or other users can sort through. You can think of log levels as tags or labels you attach to messages to prioritize them.

There are several log levels, and each serves a different purpose. If you want to learn more, you’re in the right place. DataSet blogs have wealth of information on logging best practices.

Evolving Your Approach

In the example above, you built a small application and implemented the minimal amount of logging possible in Symfony. However, many details are still missing. For example:

  1. Our application logs everything to file. This approach can grow large with time and eat up disk space. If you wanted to use an application like this regularly, you’d need to implement a log rotation strategy.
  2. In this application, we logged to a single file. But there could still be some form of grouping. For example, you could log emergency, critical, and debug logs in different files.
  3. In production, you’d probably want to use a managed log aggregation service. But in a development environment, logging to file on your local machine could be enough.
  4. You could set up an alert mechanism for critical or security logs. Aside from logging, you’d want to receive an alert by email or pager so you could react quickly.

As you can see, you could make significant improvements to this logging system.

Using a Logging Framework: Monolog Logger

Monolog is a modern and popular logger for PHP. It implements a PSR-3 logger interface for high interoperability. Symfony integrates seamlessly with Monolog.

To use Monolog, you’d need to install the monolog-bundle using Composer:

composer require symfony/monolog-bundle

After installation, you’ll see three new files in the config directory: config/packages/dev/monolog.yaml, config/packages/test/monolog.yaml, and config/packages/prod/monolog.yaml.

Symfony maintains separate logging configurations for different environments. Usually, you wouldn’t handle log messages in production the same way you would in a development or test environment.

Log Channels

Monolog brings some level of organization to your logs in the form of channels. Channels allow you to group your log messages. Channels are configurable, so you can decide how you want to handle a certain log channel.

You could have channel names like general, core, application, or any name you want. Symfony comes with some default channel names, including doctrine, event, and security. To check the channels that come bundled with Symfony, run the command below:

php bin/console debug:container monolog

As a result, you’ll see a list.

Log Handlers

If you look at config/packages/dev/monolog.yaml file, you’ll see a channel named main. It logs messages of minimum level debug to file. It uses stream handler, which is defined as type stream.

monolog:
    handlers:
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            channels: ["!event"]

Handlers are a set of classes that either modify or write log messages to a destination: a text file, an external log aggregation service, a database, or an email. A logger instance could have n handlers defined. When you log a message, your message transverses the handler stack. In other words, your log keeps moving from one handler to another until it has been handled by all of the handlers in the logger instance. A handler could modify a log message, log, or do nothing with it, based on the level of the message and the level the handler is configured to handle.

Want to see an exhaustive list of possible handlers? Take a look at a complete Monolog configuration file.

Previously, we configured our app to use the minimal loggers that came bundled with Symfony in config/services.yaml.

To swap Symfony’s minimal logger to the Monolog logger, you would need to change the logger object in config/service.yaml on line 13 to:

#file: config/services.yaml
logger:
    alias: 'monolog.logger'

Once you restart the app, it will use Monolog logger to log messages. This opens new possibilities.

You may want to notify the development team on Slack when a critical event happens. All you need to do is to configure a Slack handler as:

#config/packages/dev/monolog.yaml
monolog:
    handlers:
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            channels: ["!event"]
        slack:
            type: slackwebhook
            webhook_url: "https://hooks.slack.com/services/XXXX/XXXX/XXXXXX"
            channel: "#your-team-channel"
            level: critical

When you log a critical error message like this, your message posts to the Slack channel you configured.

$this->logger->critical('Authentication service is could not be reached: aborting application', [
    'requestID' => 'XXXX-XXX-XX',
  	'IP' => 'XX.XX.XX.X'
  	'username'  => 'XXXX'
]);

Log Rotation

Logs can grow huge over time. If you log to disk without a log rotation strategy in place, you could run of out disk space.

You can configure Monolog logger to log in to separate files every day. And it’s not only that. You can also set how many of those files to keep at a time. This ensures that logs don’t eat up too much of your disk.

To rotate your log, update config/packages/dev/monolog.yaml with this:

#file: config/packages/dev/monolog.yaml
monolog:
    handlers:
        main:
            type: rotating_file
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            max_files: 20
            channels: ["!event"]

Conclusion

Today you’ve learned about Symfony, the simplest possible logging, and how to log the right way. You built a little console application and integrated the most popular logging framework in PHP.

You can learn more by playing with the console app. For example, you could:

1. Play with different log levels.
2. Configure more Monolog handlers.
3. Integrate with managed log aggregating services, such as DataSet.
4. Create a custom log handler.

If you are not already using DataSet, get a fully-functioning free trial.

Want to look at the source code? You can find it on GitHub.

Keep logging!

In this video we are going to cover 10 ways in which I use Jordi Boggiano’s excellent Monolog library inside Symfony to make my life as a developer that much easier. Of course Jordi Boggiano / Seldaek is also the creator of Composer, and Packagist, and is someone to who the entire PHP community will — I’m sure — join me in saying an absolutely huge thank you too.

There’s a ton (well, 10) of things to cover in this video, so let’s get started.

1. Logging in a Controller

The very easiest way to get started with Monolog is to do so from any Symfony controller action:

// Symfony 4
public function myAction(LoggerInterface $logger)
{
    $logger->debug('detailed debug output');
    $logger->info('e.g. a user logged in');
}

// Symfony 2
public function myAction()
{
    $this->get('logger')->debug('detailed debug output');
    $this->get('logger')->info('e.g. a user logged in');
    $this->get('logger')->notice('normal but significant events');
    $this->get('logger')->warning('undesirable things that are not necessarily wrong');

    // or if logging a bunch at once, maybe...
    $logger = $this->get('logger');

    $logger->error(' Runtime errors that do not require immediate action');
    $logger->critical('Application component unavailable, unexpected exception');
    $logger->alert('Entire website down, database unavailable, etc');
    $logger->emergency('System is unusable');

    return $this->render(...)
}

Now, in the development environment, when you browse to the route that this controller action is for you should expect to see additional information on your Symfony web debug toolbar:


Monolog on the web debug toolbar

and when clicked:

Monolog output inside the Symfony profiler

and also inside the log file (var/logs/dev.log) itself:


Monolog output from the command line / Symfony debug log

2. Logging In A Symfony Service

Almost as easy as logging from a controller action is using Monolog inside a Symfony service. I say almost as you will need to inject the logger. If this is something you aren’t yet sure how to do, be sure to watch Symfony Services 101 where this is covered in more depth.

# /app/config/services.yml

# note you may not even need an explicit service definition
# if using Symfony 4, with autowiring

services:
    app.example_service:
        class: AppBundleServiceExampleService
        arguments:
            - "@logger"

And then in our service:

<?php

namespace AppBundleService;

use PsrLogLoggerInterface;

class ExampleService
{
    /**
     * @var LoggerInterface
     */
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function exampleMethod($ourVar)
    {
        $this->logger->debug('detailed debug output');
        $this->logger->info('e.g. a user logged in');
        $this->logger->notice('normal but significant events');

        // and so on
    }
}

Worth noting here is injecting something that conforms to LoggerInterface rather than tying directly to Monolog. In this instance, the likelihood of switching is slim, but you technically could use any PSR-3 compatible logger.

3. tail‘ing Your Logs

Logs are great, and having access to them via the Web Debug Toolbar is very nice.

As your projects grow in complexity, the Web Debug Toolbar becomes a nice-to-have, but the raw log files themselves become essential.

A very quick way to interact with your log files is to:

cd /your/project/
tail -f var/logs/dev.log

This gives you a running output of your log files without needing to have an open browser window to see your debug logs. And also even when the debug toolbar isn’t available. And also in prod… the list goes on as to when this single command is useful.

I have another video available if you’d like to learn a few more tips on working with Symfony from the command line.

4. We Can Do Better Than sprintf

Simple strings are great in helping you locate your logging statements, but inevitably you will need to log information related to the current execution of your code — or in layman’s terms: what them there variables contained at the time of when you dun’ run your code.

It’s really tempting to use sprintf, and it maybe enough to get the job done. But we can do better:

    public function exampleMethod($ourVar)
    {
        $this->logger->debug(sprintf('$ourVar was set to %s', $ourVar));

        // better
        $this->logger->debug('exampleMethod :: $ourVar at interesting time', [
            'our_var' => $ourVar
        ]);
    }

The convention I use in the second statement is of my own preference, you are completely free to write your log statements in any format you choose.

Now when you call exampleMethod('hello'), the log output looks a little different:

[2017-01-23 12:58:25] app.DEBUG: $ourVar was set to hello
[2017-01-23 12:58:25] app.DEBUG: detailed debug output {"our_var":"hello"}

Using that context array allows different handlers to work with our extra data in interesting ways. One of which we will see shortly.

5. Fingers Crossed

Sometimes naming things is hard. Sometimes, naming things is fun!

One thing you may have spotted when first prodding around (excuse the pun) in app/config/config_prod.yml is the monolog.handlers config for main:

// /app/config/config_prod.yml

monolog:
    handlers:
        main:
            type:         fingers_crossed
            action_level: error
            handler:      nested
        nested:
            type:  stream
            path:  "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
        console:
            type:  console

Most of this looks very ‘computersy’, but fingers_crossed?

Understanding this is really useful for two big reasons:

  1. It will help you understand how logging — by default — occurs in a Symfony production website, and;
  2. It will help you understand Monolog’s handler stack concept.

Starting with the second point, the handler stack.

Anything you log will go through the handler stack, from top to bottom — i.e. starting with main, then going to nested, and finally to console, unless stopped by a specific handler along the way.

Each handler has a type. This type defines which class to use, of which there are plenty of options, though not all are immediately available in Symfony (to the very best of my knowledge).

This default config_prod.yml configuration is really helpful, but initially confusing.

main won’t do anything unless triggered. But all log entries still pass through it.

nested will write any log entry to the var/logs/prod.log file, as the minimum log level is set to debug.

console will write messages out to the terminal on which any console command is run.

A very common question I get asked at this point is — then why don’t I see anything logged to my terminal when I browse my site in prod mode?

Good question, let’s answer it very shortly.

You now know that any log entry you — or anything else (e.g. Symfony) — makes will be pass through each handler in turn, unless the handler explicitly stops propagation.

That covers #2.

Now, back to point #1.

fingers_crossed can be thought of as a filter.

Messages have to pass through it, but it will hold onto them, and silently discard them — unless your tolerated action_level (or higher) is met.

By default, Symfony sets this level to error, meaning unless at some point in any given request an entry is logged with a severity of error or higher, the logs will be silently dropped at the end of the request, and the handlers further ‘down’ the handler stack will be none the wiser.

Ok, so we say let’s cross our fingers and hope everything goes to plan, but if not, Symfony please can you keep me the log files for the request that had the errors? Thanks.

And then a request comes in which does trigger the error log level (or higher) in some way, and every single log entry is given to the nested handler, which will itself write everything with debug level or greater (so, anything) to the on disk log file. Nice. Our prod.log file is kept clutter free, unless things go wrong, in which case we make note of everything.

But what about console?

Ahh, another special case. You don’t see anything logged to your stderr (terminal) by default, because console is only concerned with Symfony’s Console Commands.

For a more thorough dive into this concept, check out the Core Concepts documentation. It doesn’t take very long to read, and will give you a good understanding of any potential headscratchers.

6. One Line Log Rotation

It gives me no pleasure to say that a few years ago I received a phone call around tea time (6:30pm give or take) from a rather stressed out company director, stating that his entire website was down, and could I please drop everything and fix it.

It gives me some pleasure to say that thankfully, I wasn’t the source of the problem :)

What had happened was that this site had been in production for some time, and during that time, the log file had grown, and grown, and grown, until it had consumed the entire disk. It baffles me that as this was a fully managed server from a very big name in the industry that they let this happen, but there you go.

Anyway, for the love of Mike, rotate your log files.

I would urge you to use LogRotate in almost every circumstance.

But, sometimes that is not possible.

And if it isn’t, a very easy solution is to use the max_files option:

#/app/config/config_prod.yml

monolog:
    handlers:
        main:
            type:         fingers_crossed
            action_level: error
            handler:      nested
        nested:
            type:  stream
            path:  "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            max_files: 7

Where max_files is the number of days worth of logs to keep on disk.

Easy.

7. Use Channels To Log Critical Paths

By default, any log entry you add from your Symfony application will be put onto the app channel:

$ tail -f var/logs/dev.log

[2017-01-23 12:21:56] security.INFO: Populated the TokenStorage with an anonymous Token. [] []
[2017-01-23 12:21:56] app.INFO: e.g. a user logged in [] []
[2017-01-23 12:21:56] app.NOTICE: normal but significant events [] []
[2017-01-23 12:21:56] app.WARNING: undesirable things that are not necessarily wrong [] []
[2017-01-23 12:21:56] app.ERROR:  Runtime errors that do not require immediate action [] []
[2017-01-23 12:21:56] app.CRITICAL: Application component unavailable, unexpected exception [] []
[2017-01-23 12:21:56] app.ALERT: Entire website down, database unavailable, etc [] []
[2017-01-23 12:21:56] app.EMERGENCY: System is unusable [] []
[2017-01-23 12:21:56] request.INFO: Matched route "_wdt". {"route":"_wdt","route_parameters":{"_controller":"web_profiler.controller.profiler:toolbarAction","token":"a0f93a","_route":"_wdt"},"request_uri":"http://127.0.0.1:8000/_wdt/a0f93a","method":"GET"} []

This is fine, as you can quickly see where your app is doing things by grep‘ing for app, e.g.:

tail -f var/logs/prod.log | grep "app"

Which would ‘filter’ down to just logs relevant to your code:

$ tail -f var/logs/dev.log | grep "app"

[2017-01-23 12:21:56] app.INFO: e.g. a user logged in [] []
[2017-01-23 12:21:56] app.NOTICE: normal but significant events [] []
[2017-01-23 12:21:56] app.WARNING: undesirable things that are not necessarily wrong [] []
[2017-01-23 12:21:56] app.ERROR:  Runtime errors that do not require immediate action [] []
[2017-01-23 12:21:56] app.CRITICAL: Application component unavailable, unexpected exception [] []
[2017-01-23 12:21:56] app.ALERT: Entire website down, database unavailable, etc [] []
[2017-01-23 12:21:56] app.EMERGENCY: System is unusable [] []

But this becomes a little less useful on larger apps where a single request may traverse a bunch of ‘parts’ inside your system. Logging everything to app can lead to a bunch of ‘muddy’ log entries that make it trickier to single out just the particular concept of your system that is interesting / going wrong.

A way I find this particularly useful is to group anything related to a single concept into using the same channel. An example of this might be the process of user registration, or in connecting a user’s account to their social media profile, or a Symfony console command that triggers off some import business process.

Using Channels is really easy. We just need to tag our services with the name of the channel we wish to log too:

#/app/config/services.yml

services:
   app.example_service:
        class: AppBundleServiceExampleService
        arguments:
            - "@logger"
        tags:
            - { name: monolog.logger, channel: "my_channel" }

Now, when the ExampleService creates a new log entry, the entry will switch from app to my_channel in the logs:

$ tail -f var/logs/dev.log

[2017-01-23 12:22:31] my_channel.INFO: e.g. a user logged in [] []
[2017-01-23 12:22:31] my_channel.NOTICE: normal but significant events [] []
[2017-01-23 12:22:31] my_channel.WARNING: undesirable things that are not necessarily wrong [] []
[2017-01-23 12:22:31] my_channel.ERROR:  Runtime errors that do not require immediate action [] []

8. Log Channels To Their Own File

Depending on the complexity of your application it can sometimes be easier to create separate log files for the different critical paths (channels) through your application.

This is easy to achieve by combining a stream channel type with the channels option:

#/app/config/config_dev.yml

monolog:
    handlers:
        my_channel:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%-my_channel.log"
            level: debug
            channels: [my_channel]

Here any log file entries created by a logger that uses the channel of my_channel will also be written to a file called var/logs/dev-my_channel.log.

You can also leverage the channels option to filter any channels from the current handler:

#/app/config/config_dev.yml

monolog:
    handlers:
        my_channel:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%-my_channel.log"
            level: debug
            channels: [my_channel]
        main:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: debug
            channels: [!event, !my_channel]

Prefixing with the channel with ! will ensure it is excluded.

9. Logging From Symfony Console Commands

Logging from a Symfony console command is no different from logging in a Controller, or in a Service.

We need a logger instance, and then we can start logging. One way of doing this would be to extends ContainerAwareCommand, which you could then grab the logger via getContainer()->get('logger').

A better way — in my opinion — of defining console commands is to define them as services in their own right. This allows ‘standard’ service definition-style arguments which enable constructor injection.

services:
    app.example_command:
        class: AppBundleCommandExampleCommand
        arguments:
            - "@logger"
        tags:
            - { name: console.command }

And in ExampleCommand:

<?php

// /src/AppBundle/Command/ExampleCommand.php

namespace AppBundleCommand;

use AppBundleServiceExampleService;
use PsrLogLoggerInterface;
use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;

class ExampleCommand extends Command
{
    /**
     * @var LoggerInterface
     */
    private $logger;
    /**
     * @var ExampleService
     */
    private $exampleService;

    public function __construct(LoggerInterface $logger)
    {
        parent::__construct();

        $this->logger = $logger;
    }

    protected function configure()
    {
        $this
            // the name of the command (the part after "bin/console")
            ->setName('app:example-logger')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $this->logger->debug('debug message from console command');

        $output->writeln('hello from console command');
    }
}

Running this would write the debug log to the var/log/dev.log as you would expect. It would also output the contents of the writeln call to your screen.

The downside now is you have to then go and cat or tail your dev.log as another set of keystrokes. Boo.

Wouldn’t it be nicer if you could see your log output right there in the console? Of course it would.

monolog:
    handlers:
        # others removed for brevity
        console:
            type:   console
            channels: [!event, !doctrine]
            verbosity_levels:
                VERBOSITY_NORMAL: debug

By setting the level of VERBOSITY_NORMAL to debug we now get our logged statements output to the screen along with our writeln output. Thats nice, and the log entries are of course still logged to our dev.log file as we would expect.

In case you were wondering, VERBOSITY_NORMAL defaults to the level of warning, meaning logging an entry with a severity of warning or higher would log to the terminal without you having to do anything.

10. Log To Graylog From Monolog

A critical piece of ‘going live’ for my projects is to ensure I have an instance of Graylog up, running, and being logged too from my production environment. As your application(s) grow, if you end up with a cluster of servers, trying to tail a log file may be fruitless if you’re tests are hitting the other cluster node!

Getting Graylog up and running is outside the scope of this video, but I would point your in the direction of the official Graylog ansible role. If you are new to Ansible, I have a full free tutorial for you to enjoy.

To start logging to Graylog from Symfony you first need to add graylog2/gelf-php to your project.

Then, we need to define a formatter service to format our log entries in a Graylog compatible manner (GELF format):

# /app/config/services.yml

services:
    app.gelf_formatter:
        class: MonologFormatterGelfMessageFormatter
        arguments:
            - "my example server"

I would advise making the first argument the hostname of your server, as it will be used prominently by Graylog. As you most likely want to differentiate between your dev, prod, (and others), it makes sense to use a parameter here instead of a hardcoded value. I’m hardcoding as this is a demo.

Next, we need to tell Monolog about our new handler. I’m going to call mine gelf, but call it whatever. The type: gelf is the important bit.

The publisher section is the hostname and port of your Graylog server. I’m using a local server, and the standard port. Obviously this would need some firewall considerations in prod.

We have already covered level, but to recap this handler will capture everything with a log severity level of debug or higher (so, anything).

Lastly, we use our newly defined formatting service as the formatter:

# /app/config/config_dev.yml

monolog:
    handlers:
        gelf:
            type: gelf
            publisher:
                hostname: 192.168.0.31
                port: 12201
            level: debug
            formatter: app.gelf_formatter

Next, give your application a cache:clear, even in development.

Inside Graylog you can add in a GELF UDP input and the very next time you write a new log entry, boom, it’s in Graylog:

Logging to Graylog from Symfony

I hope you’ve found these ideas for using Monolog to be useful, and if you have any you’d like to share then do please leave them in the comments below.

The only way to understand what went wrong during the execution of a request is to review a trace of the execution process. Fortunately, as you’ll learn in this section, both PHP and symfony tend to log large amounts of this sort of data.

16.1.1. PHP Logs

PHP has an error_reporting parameter, defined in php.ini, that specifies which PHP events are logged. Symfony allows you to override this value, per application and environment, in the settings.yml file, as shown in Listing 16-1.

Listing 16-1 — Setting the Error Reporting Level, in frontend/config/settings.yml

prod:
 .settings:
    error_reporting:  <?php echo (E_PARSE | E_COMPILE_ERROR | E_ERROR | E_CORE_ERROR | E_USER_ERROR)."n" ?>

dev:
  .settings:
    error_reporting:  <?php echo (E_ALL | E_STRICT)."n" ?>

In order to avoid performance issues in the production environment, the server logs only the critical PHP errors. However, in the development environment, all types of events are logged, so that the developer can have all the information necessary to trace errors.

The location of the PHP log files depends on your php.ini configuration. If you never bothered about defining this location, PHP probably uses the logging facilities provided by your web server (such as the Apache error logs). In this case, you will find the PHP logs under the web server log directory.

16.1.2. Symfony Logs

In addition to the standard PHP logs, symfony can log a lot of custom events. You can find all the symfony logs under the myproject/log/ directory. There is one file per application and per environment. For instance, the development environment log file of the frontend application is named frontend_dev.log, the production one is named frontend_prod.log, and so on.

If you have a symfony application running, take a look at its log files. The syntax is very simple. For every event, one line is added to the log file of the application. Each line includes the exact time of the event, the nature of the event, the object being processed, and any additional relevant details. Listing 16-2 shows an example of symfony log file content.

Listing 16-2 — Sample Symfony Log File Content, in log/frontend_dev.php

Nov 15 16:30:25 symfony [info ] {sfAction} call "barActions->executemessages()"
Nov 15 16:30:25 symfony [debug] SELECT bd_message.ID, bd_message.SENDER_ID, bd_...
Nov 15 16:30:25 symfony [info ] {sfPropelLogger} executeQuery: SELECT bd_message.ID...
Nov 15 16:30:25 symfony [info ] {sfView} set slot "leftbar" (bar/index)
Nov 15 16:30:25 symfony [info ] {sfView} set slot "messageblock" (bar/mes...
Nov 15 16:30:25 symfony [info ] {sfView} execute view for template "messa...
Nov 15 16:30:25 symfony [info ] {sfView} render "/home/production/myproject/...
Nov 15 16:30:25 symfony [info ] {sfView} render to client

You can find many details in these files, including the actual SQL queries sent to the database, the templates called, the chain of calls between objects, and so on.

New in symfony 1.1: The format of the file logs is configurable by overriding the format and/or the time_format settings in factories.yml as shown in Listing 16-3.

Listing 16-3 — Changing the Log Format

all:
  logger:
    param:
      sf_file_debug:
        param:
          format:      %time% %type% [%priority%] %message%%EOL%
          time_format: %b %d %H:%M:%S

16.1.2.1. Symfony Log Level Configuration ###

There are eight levels of symfony log messages: emerg, alert, crit, err, warning, notice, info, and debug, which are the same as the PEAR::Log package (http://pear.php.net/package/Log/) levels. You can configure the maximum level to be logged in each environment in the factories.yml configuration file of each application, as demonstrated in Listing 16-4.

Listing 16-4 — Default Logging Configuration, in frontend/config/factories.yml

prod:
  logger:
    param:
      level: err

By default, in all environments except the production environment, all the messages are logged (up to the least important level, the debug level). In the production environment, logging is disabled by default; if you change logging_enabled to on in settings.yml, only the most important messages (from crit to emerg) appear in the logs.

You can change the logging level in the factories.yml file for each environment to limit the type of logged messages.

Tip To see if logging is enabled, call sfConfig::get('sf_logging_enabled').

16.1.2.2. Adding a Log Message ###

You can manually add a message in the symfony log file from your code by using one of the techniques described in Listing 16-5.

Listing 16-5 — Adding a Custom Log Message

$this->logMessage($message, $level);


<?php use_helper('Debug') ?>
<?php log_message($message, $level) ?>

$level can have the same values as in the log messages.

Alternatively, to write a message in the log from anywhere in your application, use the sfLogger methods directly, as shown in Listing 16-6. The available methods bear the same names as the log levels.

Listing 16-6 — Adding a Custom Log Message from Anywhere

if (sfConfig::get('sf_logging_enabled'))
{
  sfContext::getInstance()->getLogger()->info($message);
}

16.1.2.3. Purging and Rotating Log Files ###

Don’t forget to periodically purge the log/ directory of your applications, because these files have the strange habit of growing by several megabytes in a few days, depending, of course, on your traffic. Symfony provides a special log:clear task for this purpose, which you can launch regularly by hand or put in a cron table. For example, the following command erases the symfony log files:

For both better performance and security, you probably want to store symfony logs in several small files instead of one single large file. The ideal storage strategy for log files is to back up and empty the main log file regularly, but to keep only a limited number of backups. You can enable such a log rotation with a period of 7 days and a history (number of backups) of 10, as shown in Listing 16-7. You would work with one active log file plus ten backup files containing seven days’ worth of history each. Whenever the next period of seven days ends, the current active log file goes into backup, and the oldest backup is erased.

Listing 16-7 — Launching Log Rotation

> php symfony log:rotate frontend prod --period=7 --history=10

The backup log files are stored in the logs/history/ directory and suffixed with the date they were saved.



Понравилась статья? Поделить с друзьями:
  • Symfony custom error page
  • Symfony console error
  • Symfony 500 internal server error
  • Symfony 404 error
  • Symbols error huawei что это