diff --git a/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php b/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php index 7b7bf23c1a219..98b1d229220fe 100644 --- a/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php @@ -32,8 +32,17 @@ public function __invoke(array $record) { $hash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : ''; + $timestamp = $timestampRfc3339 = false; + if ($record['datetime'] instanceof \DateTimeInterface) { + $timestamp = $record['datetime']->getTimestamp(); + $timestampRfc3339 = $record['datetime']->format(\DateTimeInterface::RFC3339_EXTENDED); + } elseif (false !== $timestamp = strtotime($record['datetime'])) { + $timestampRfc3339 = (new \DateTimeImmutable($record['datetime']))->format(\DateTimeInterface::RFC3339_EXTENDED); + } + $this->records[$hash][] = [ - 'timestamp' => $record['datetime'] instanceof \DateTimeInterface ? $record['datetime']->getTimestamp() : strtotime($record['datetime']), + 'timestamp' => $timestamp, + 'timestamp_rfc3339' => $timestampRfc3339, 'message' => $record['message'], 'priority' => $record['level'], 'priorityName' => $record['level_name'], diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php index 6adec38a0c7f0..c576462d0abfe 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php @@ -43,6 +43,30 @@ public function providerDatetimeFormatTests(): array ]; } + /** + * @dataProvider providerDatetimeRfc3339FormatTests + */ + public function testDatetimeRfc3339Format(array $record, $expectedTimestamp) + { + $processor = new DebugProcessor(); + $processor($record); + + $records = $processor->getLogs(); + self::assertCount(1, $records); + self::assertSame($expectedTimestamp, $records[0]['timestamp_rfc3339']); + } + + public function providerDatetimeRfc3339FormatTests(): array + { + $record = $this->getRecord(); + + return [ + [array_merge($record, ['datetime' => new \DateTime('2019-01-01T00:01:00+00:00')]), '2019-01-01T00:01:00.000+00:00'], + [array_merge($record, ['datetime' => '2019-01-01T00:01:00+00:00']), '2019-01-01T00:01:00.000+00:00'], + [array_merge($record, ['datetime' => 'foo']), false], + ]; + } + public function testDebugProcessor() { $processor = new DebugProcessor(); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig index fc878fdba491d..7b1a35b748dd7 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig @@ -46,189 +46,181 @@ {% block panel %}

Log Messages

- {% if collector.logs is empty %} + {% if collector.processedLogs is empty %}

No log messages available.

{% else %} - {# sort collected logs in groups #} - {% set deprecation_logs, debug_logs, info_and_error_logs, silenced_logs = [], [], [], [] %} - {% set has_error_logs = false %} - {% for log in collector.logs %} - {% if log.scream is defined and not log.scream %} - {% set deprecation_logs = deprecation_logs|merge([log]) %} - {% elseif log.scream is defined and log.scream %} - {% set silenced_logs = silenced_logs|merge([log]) %} - {% elseif log.priorityName == 'DEBUG' %} - {% set debug_logs = debug_logs|merge([log]) %} - {% else %} - {% set info_and_error_logs = info_and_error_logs|merge([log]) %} - {% if log.priorityName != 'INFO' %} - {% set has_error_logs = true %} - {% endif %} - {% endif %} - {% endfor %} - -
-
-

Info. & Errors {{ collector.counterrors ?: info_and_error_logs|length }}

-

Informational and error log messages generated during the execution of the application.

- -
- {% if info_and_error_logs is empty %} -
-

There are no log messages of this level.

-
- {% else %} - {{ helper.render_table(info_and_error_logs, 'info', true) }} - {% endif %} -
+ {% set has_error_logs = collector.processedLogs|column('type')|filter(type => 'error' == type)|length > 0 %} + {% set has_deprecation_logs = collector.processedLogs|column('type')|filter(type => 'deprecation' == type)|length > 0 %} + + {% set filters = collector.filters %} +
+
+
    +
  • + + +
  • + +
  • + + +
  • + +
  • + + +
  • +
-
- {# 'deprecation_logs|length' is not used because deprecations are - now grouped and the group count doesn't match the message count #} -

Deprecations {{ collector.countdeprecations|default(0) }}

-

Log messages generated by using features marked as deprecated.

- -
- {% if deprecation_logs is empty %} -
-

There are no log messages about deprecated features.

+
+ + {{ include('@WebProfiler/Icon/filter.svg') }} + Level ({{ filters.priority|length - 1 }}) + + +
+ + + {% for label, value in filters.priority %} +
+ +
- {% else %} - {{ helper.render_table(deprecation_logs, 'deprecation', false, true) }} - {% endif %} + {% endfor %}
-
- -
-

Debug {{ debug_logs|length }}

-

Unimportant log messages generated during the execution of the application.

- -
- {% if debug_logs is empty %} -
-

There are no log messages of this level.

+ + +
+ + {{ include('@WebProfiler/Icon/filter.svg') }} + Channel ({{ filters.channel|length - 1 }}) + + +
+ + + {% for value in filters.channel %} +
+ +
- {% else %} - {{ helper.render_table(debug_logs, 'debug') }} - {% endif %} + {% endfor %}
-
- -
-

PHP Notices {{ collector.countscreams|default(0) }}

-

Log messages generated by PHP notices silenced with the @ operator.

+ +
-
- {% if silenced_logs is empty %} -
-

There are no log messages of this level.

-
- {% else %} - {{ helper.render_table(silenced_logs, 'silenced') }} - {% endif %} -
-
+ + + + + + + + + + + + + {% for log in collector.processedLogs %} + {% set css_class = 'error' == log.type ? 'error' + : (log.priorityName == 'WARNING' or 'deprecation' == log.type) ? 'warning' + : 'silenced' == log.type ? 'silenced' + %} + + - {% set compilerLogTotal = 0 %} - {% for logs in collector.compilerLogs %} - {% set compilerLogTotal = compilerLogTotal + logs|length %} - {% endfor %} -
-

Container {{ compilerLogTotal }}

-

Log messages generated during the compilation of the service container.

- -
- {% if collector.compilerLogs is empty %} -
-

There are no compiler log messages.

-
- {% else %} -
TimeMessage
+ + + {% if log.type in ['error', 'deprecation', 'silenced'] or 'WARNING' == log.priorityName %} + + {% if 'error' == log.type or 'WARNING' == log.priorityName %} + {{ log.priorityName|lower }} + {% else %} + {{ log.type|lower }} + {% endif %} + + {% endif %} +
- - - - - - - - - {% for class, logs in collector.compilerLogs %} - - - - - {% endfor %} - -
ClassMessages
- {% set context_id = 'context-compiler-' ~ loop.index %} - - {{ class }} - -
-
    - {% for log in logs %} -
  • {{ profiler_dump_log(log.message) }}
  • - {% endfor %} -
-
-
{{ logs|length }}
- {% endif %} -
-
+ + {{ helper.render_log_message('debug', loop.index, log) }} + + + {% endfor %} + + +
+

There are no log messages.

- + {% endif %} -{% endblock %} -{% macro render_table(logs, category = '', show_level = false, is_deprecation = false) %} - {% import _self as helper %} - {% set channel_is_defined = (logs|first).channel is defined %} - {% set filter = show_level or channel_is_defined %} - - - - - {% if show_level %}{% else %}{% endif %} - {% if channel_is_defined %}{% endif %} - - - - - - {% for log in logs %} - {% set css_class = is_deprecation ? '' - : log.priorityName in ['CRITICAL', 'ERROR', 'ALERT', 'EMERGENCY'] ? 'status-error' - : log.priorityName == 'WARNING' ? 'status-warning' - %} - - - - {% if channel_is_defined %} - + {% set compilerLogTotal = 0 %} + {% for logs in collector.compilerLogs %} + {% set compilerLogTotal = compilerLogTotal + logs|length %} + {% endfor %} - {% endif %} +
+ +

Container Compilation Logs ({{ compilerLogTotal }})

+

Log messages generated during the compilation of the service container.

+
-
+ {% if collector.compilerLogs is empty %} +
+

There are no compiler log messages.

+
+ {% else %} +
LevelTimeChannelMessage
- {% if show_level %} - {{ log.priorityName }} - {% endif %} - - - {% if log.channel is null %}n/a{% else %}{{ log.channel }}{% endif %} - {% if log.errorCount is defined and log.errorCount > 1 %} - ({{ log.errorCount }} times) - {% endif %} - {{ helper.render_log_message(category, loop.index, log) }}
+ + + + - {% endfor %} - -
MessagesClass
-{% endmacro %} + + + + {% for class, logs in collector.compilerLogs %} + + {{ logs|length }} + + {% set context_id = 'context-compiler-' ~ loop.index %} + + {{ class }} + +
+
    + {% for log in logs %} +
  • {{ profiler_dump_log(log.message) }}
  • + {% endfor %} +
+
+ + + {% endfor %} + + + {% endif %} + +{% endblock %} {% macro render_log_message(category, log_index, log) %} {% set has_context = log.context is defined and log.context is not empty %} @@ -238,26 +230,41 @@ {{ profiler_dump_log(log.message) }} {% else %} {{ profiler_dump_log(log.message, log.context) }} + {% endif %} -
+ + {% if has_trace %} + {% set trace_id = 'trace-' ~ category ~ '-' ~ log_index %} + Show trace -
- {{ profiler_dump(log.context, maxDepth=1) }} -
+
+ {{ profiler_dump(log.context.exception.trace, maxDepth=1) }} +
+ {% endif %} + + {% if has_context %} +
+ {{ profiler_dump(log.context, maxDepth=1) }} +
+ {% endif %} {% if has_trace %}
{{ profiler_dump(log.context.exception.trace, maxDepth=1) }}
{% endif %} - {% endif %} +
{% endmacro %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/filter.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/filter.svg new file mode 100644 index 0000000000000..8800f1c05d75c --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/filter.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig index 2dfa26918f420..e18700d15fc68 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base_js.html.twig @@ -709,104 +709,78 @@ if (typeof Sfjs === 'undefined' || typeof Sfjs.loadToolbar === 'undefined') { } }, - createFilters: function() { - document.querySelectorAll('[data-filters] [data-filter]').forEach(function (filter) { - var filters = filter.closest('[data-filters]'), - type = 'choice', - name = filter.dataset.filter, - ucName = name.charAt(0).toUpperCase()+name.slice(1), - list = document.createElement('ul'), - values = filters.dataset['filter'+ucName] || filters.querySelectorAll('[data-filter-'+name+']'), - labels = {}, - defaults = null, - indexed = {}, - processed = {}; - if (typeof values === 'string') { - type = 'level'; - labels = values.split(','); - values = values.toLowerCase().split(','); - defaults = values.length - 1; - } - addClass(list, 'filter-list'); - addClass(list, 'filter-list-'+type); - values.forEach(function (value, i) { - if (value instanceof HTMLElement) { - value = value.dataset['filter'+ucName]; - } - if (value in processed) { - return; - } - var option = document.createElement('li'), - label = i in labels ? labels[i] : value, - active = false, - matches; - if ('' === label) { - option.innerHTML = '(none)'; - } else { - option.innerText = label; - } - option.dataset.filter = value; - option.setAttribute('title', 1 === (matches = filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').length) ? 'Matches 1 row' : 'Matches '+matches+' rows'); - indexed[value] = i; - list.appendChild(option); - addEventListener(option, 'click', function () { - if ('choice' === type) { - filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { - if (option.dataset.filter === row.dataset['filter'+ucName]) { - toggleClass(row, 'filter-hidden-'+name); - } - }); - toggleClass(option, 'active'); - } else if ('level' === type) { - if (i === this.parentNode.querySelectorAll('.active').length - 1) { - return; - } - this.parentNode.querySelectorAll('li').forEach(function (currentOption, j) { - if (j <= i) { - addClass(currentOption, 'active'); - if (i === j) { - addClass(currentOption, 'last-active'); - } else { - removeClass(currentOption, 'last-active'); - } - } else { - removeClass(currentOption, 'active'); - removeClass(currentOption, 'last-active'); - } - }); - filters.querySelectorAll('[data-filter-'+name+']').forEach(function (row) { - if (i < indexed[row.dataset['filter'+ucName]]) { - addClass(row, 'filter-hidden-'+name); - } else { - removeClass(row, 'filter-hidden-'+name); - } - }); - } + initializeLogsTable: function() { + Sfjs.updateLogsTable(); + + document.querySelectorAll('.log-filter input').forEach((input) => { + input.addEventListener('change', () => { Sfjs.updateLogsTable(); }); + }); + + document.querySelectorAll('.filter-select-all-or-none a').forEach((link) => { + link.addEventListener('click', () => { + const selectAll = link.classList.contains('select-all'); + link.closest('.log-filter-content').querySelectorAll('input').forEach((input) => { + input.checked = selectAll; }); - if ('choice' === type) { - active = null === defaults || 0 <= defaults.indexOf(value); - } else if ('level' === type) { - active = i <= defaults; - if (active && i === defaults) { - addClass(option, 'last-active'); - } - } - if (active) { - addClass(option, 'active'); - } else { - filters.querySelectorAll('[data-filter-'+name+'="'+value+'"]').forEach(function (row) { - toggleClass(row, 'filter-hidden-'+name); - }); + + Sfjs.updateLogsTable(); + }); + }); + + document.body.addEventListener('click', (event) => { + document.querySelectorAll('details.log-filter').forEach((filterElement) => { + if (!filterElement.contains(event.target) && filterElement.open) { + filterElement.open = false; } - processed[value] = true; }); + }); + }, + + updateLogsTable: function() { + const selectedType = document.querySelector('#log-filter-type input:checked').value; + const priorities = document.querySelectorAll('#log-filter-priority input'); + const selectedPriorities = Array.from(priorities).filter((input) => input.checked).map((input) => input.value); + const channels = document.querySelectorAll('#log-filter-channel input'); + const selectedChannels = Array.from(channels).filter((input) => input.checked).map((input) => input.value); + + const logs = document.querySelector('table.logs'); + if (null === logs) { + return; + } + + // hide rows that don't match the current filters + let numVisibleRows = 0; + logs.querySelectorAll('tbody tr').forEach((row) => { + if ('all' !== selectedType && selectedType !== row.getAttribute('data-type')) { + row.style.display = 'none'; + return; + } - if (1 < list.childNodes.length) { - filter.appendChild(list); - filter.dataset.filtered = ''; + if (false === selectedPriorities.includes(row.getAttribute('data-priority'))) { + row.style.display = 'none'; + return; } + + if ('' !== row.getAttribute('data-channel') && false === selectedChannels.includes(row.getAttribute('data-channel'))) { + row.style.display = 'none'; + return; + } + + row.style.display = 'table-row'; + numVisibleRows++; }); - } + + document.querySelector('table.logs').style.display = 0 === numVisibleRows ? 'none' : 'table'; + document.querySelector('.no-logs-message').style.display = 0 === numVisibleRows ? 'block' : 'none'; + + // update the selected totals of all filters + document.querySelector('#log-filter-priority .filter-active-num').innerText = (priorities.length === selectedPriorities.length) ? 'All' : selectedPriorities.length; + document.querySelector('#log-filter-channel .filter-active-num').innerText = (channels.length === selectedChannels.length) ? 'All' : selectedChannels.length; + + // update the currently selected "log type" tab + document.querySelectorAll('#log-filter-type li').forEach((tab) => tab.classList.remove('active')); + document.querySelector(`#log-filter-type input[value="${selectedType}"]`).parentElement.classList.add('active'); + }, }; })(); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index 6bb39de5beb40..7f7db6cf1239c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -10,16 +10,28 @@ --page-background: #f9f9f9; --color-text: #222; --color-muted: #999; + --color-link: #218BC3; /* when updating any of these colors, do the same in toolbar.css.twig */ --color-success: #4f805d; --color-warning: #a46a1f; --color-error: #b0413e; + --badge-background: #f5f5f5; + --badge-color: #666; + --badge-warning-background: #FEF3C7; + --badge-warning-color: #B45309; + --badge-danger-background: #FEE2E2; + --badge-danger-color: #B91C1C; --tab-background: #fff; --tab-color: #444; --tab-active-background: #666; --tab-active-color: #fafafa; --tab-disabled-background: #f5f5f5; --tab-disabled-color: #999; + --log-filter-button-background: #fff; + --log-filter-button-border: #999; + --log-filter-button-color: #555; + --log-filter-active-num-color: #2563EB; + --log-timestamp-color: #555; --metric-value-background: #fff; --metric-value-color: inherit; --metric-unit-color: #999; @@ -54,13 +66,25 @@ --page-background: #36393e; --color-text: #e0e0e0; --color-muted: #777; + --color-link: #93C5FD; --color-error: #d43934; + --badge-background: #555; + --badge-color: #ddd; + --badge-warning-background: #B45309; + --badge-warning-color: #FEF3C7; + --badge-danger-background: #B91C1C; + --badge-danger-color: #FEE2E2; --tab-background: #555; --tab-color: #ccc; --tab-active-background: #888; --tab-active-color: #fafafa; --tab-disabled-background: var(--page-background); --tab-disabled-color: #777; + --log-filter-button-background: #555; + --log-filter-button-border: #999; + --log-filter-button-color: #ccc; + --log-filter-active-num-color: #93C5FD; + --log-timestamp-color: #ccc; --metric-value-background: #555; --metric-value-color: inherit; --metric-unit-color: #999; @@ -139,7 +163,7 @@ p { } a { - color: #218BC3; + color: var(--color-link); text-decoration: none; } a:hover { @@ -204,7 +228,7 @@ button { } .btn-link { border-color: transparent; - color: #218BC3; + color: var(--color-link); text-decoration: none; background-color: transparent; outline: none; @@ -1011,6 +1035,118 @@ tr.status-warning td { {# Logger panel ========================================================================= #} +.badge { + background: var(--badge-background); + border-radius: 4px; + color: var(--badge-color); + font-size: 12px; + font-weight: bold; + padding: 1px 4px; +} +.badge-warning { + background: var(--badge-warning-background); + color: var(--badge-warning-color); +} + +.log-filters { + display: flex; +} +.log-filters .log-filter { + position: relative; +} +.log-filters .log-filter + .log-filter { + margin-left: 15px; +} +.log-filters .log-filter summary { + align-items: center; + background: var(--log-filter-button-background); + border-radius: 2px; + border: 1px solid var(--log-filter-button-border); + color: var(--log-filter-button-color); + cursor: pointer; + display: flex; + padding: 5px 8px; +} +.log-filters .log-filter summary .icon { + height: 18px; + width: 18px; + margin: 0 7px 0 0; +} +.log-filters .log-filter summary svg { + height: 18px; + width: 18px; + opacity: 0.7; +} +.log-filters .log-filter summary .filter-active-num { + color: var(--log-filter-active-num-color); + font-weight: bold; + padding: 0 1px; +} +.log-filter .tab-navigation { + margin-bottom: 0; +} +.log-filter .tab-navigation li:first-child { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} +.log-filter .tab-navigation li:last-child { + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; +} +.log-filter .tab-navigation li { + border-color: var(--log-filter-button-border); + padding: 0; +} +.log-filter .tab-navigation li + li { + margin-left: -5px; +} +.log-filter .tab-navigation li .badge { + font-size: 13px; + padding: 0 6px; +} +.log-filter .tab-navigation li input { + display: none; +} +.log-filter .tab-navigation li label { + align-items: center; + cursor: pointer; + padding: 5px 10px; + display: inline-flex; + font-size: 14px; +} + +.log-filters .log-filter .log-filter-content { + background: var(--base-0); + border: 1px solid var(--table-border); + border-radius: 2px; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + padding: 15px; + position: absolute; + left: 0; + top: 36px; + max-width: 400px; + min-width: 200px; + z-index: 9999; +} +.log-filters .log-filter .log-filter-content .log-filter-option { + align-items: center; + display: flex; +} +.log-filter .filter-select-all-or-none { + margin-bottom: 10px; +} +.log-filter .filter-select-all-or-none a + a { + margin-left: 15px; +} +.log-filters .log-filter .log-filter-content .log-filter-option + .log-filter-option { + margin: 7px 0 0; +} +.log-filters .log-filter .log-filter-content .log-filter-option label { + cursor: pointer; + flex: 1; + padding-left: 10px; +} + table.logs .metadata { display: block; font-size: 12px; @@ -1018,6 +1154,75 @@ table.logs .metadata { .theme-dark tr.status-error td, .theme-dark tr.status-warning td { border-bottom: unset; border-top: unset; } +table.logs .log-timestamp { + color: var(--log-timestamp-color); +} +table.logs .log-metadata { + margin: 8px 0 0; +} +table.logs .log-metadata span { + display: inline-block; +} +table.logs .log-metadata span + span { + margin-left: 10px; +} +table.logs .log-metadata .log-channel { + color: var(--base-1); + font-size: 13px; + font-weight: bold; +} +table.logs .log-metadata .log-num-occurrences { + color: var(--color-muted); + font-size: 13px; +} +.log-type-badge { + display: inline-block; + font-family: var(--font-sans-serif); + margin-top: 5px; +} +.log-type-badge.badge-deprecation { + background: var(--badge-warning-background); + color: var(--badge-warning-color); +} +.log-type-badge.badge-error { + background: var(--badge-danger-background); + color: var(--badge-danger-color); +} +.log-type-badge.badge-silenced { + background: #EDE9FE; + color: #6D28D9; +} +.theme-dark .log-type-badge.badge-silenced { + background: #5B21B6; + color: #EDE9FE; +} + +tr.log-status-warning { + border-left: 4px solid #F59E0B; +} +tr.log-status-error { + border-left: 4px solid #EF4444; +} +tr.log-status-silenced { + border-left: 4px solid #A78BFA; +} + +.container-compilation-logs { + background: var(--table-background); + border: 1px solid var(--base-2); + margin-top: 30px; + padding: 15px; +} +.container-compilation-logs summary { + cursor: pointer; +} +.container-compilation-logs summary h4 { + margin: 0 0 5px; +} +.container-compilation-logs summary p { + margin: 0; +} + {# Doctrine panel ========================================================================= #} .sql-runnable { diff --git a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php index 2e34a5a1f43de..342c502b977dd 100644 --- a/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php +++ b/src/Symfony/Component/HttpKernel/DataCollector/LoggerDataCollector.php @@ -28,6 +28,7 @@ class LoggerDataCollector extends DataCollector implements LateDataCollectorInte private $containerPathPrefix; private $currentRequest; private $requestStack; + private $processedLogs; public function __construct(object $logger = null, string $containerPathPrefix = null, RequestStack $requestStack = null) { @@ -80,6 +81,81 @@ public function getLogs() return $this->data['logs'] ?? []; } + public function getProcessedLogs() + { + if (null !== $this->processedLogs) { + return $this->processedLogs; + } + + $rawLogs = $this->getLogs(); + if ([] === $rawLogs) { + return $this->processedLogs = $rawLogs; + } + + $logs = []; + foreach ($this->getLogs()->getValue() as $rawLog) { + $rawLogData = $rawLog->getValue(); + + if ($rawLogData['priority']->getValue() > 300) { + $logType = 'error'; + } elseif (isset($rawLogData['scream']) && false === $rawLogData['scream']->getValue()) { + $logType = 'deprecation'; + } elseif (isset($rawLogData['scream']) && true === $rawLogData['scream']->getValue()) { + $logType = 'silenced'; + } else { + $logType = 'regular'; + } + + $logs[] = [ + 'type' => $logType, + 'errorCounter' => isset($rawLogData['errorCounter']) ? $rawLogData['errorCounter']->getValue() : 1, + 'timestamp' => $rawLogData['timestamp_rfc3339']->getValue(), + 'priority' => $rawLogData['priority']->getValue(), + 'priorityName' => $rawLogData['priorityName']->getValue(), + 'channel' => $rawLogData['channel']->getValue(), + 'message' => $rawLogData['message'], + 'context' => $rawLogData['context'], + ]; + } + + // sort logs from oldest to newest + usort($logs, static function ($logA, $logB) { + return $logA['timestamp'] <=> $logB['timestamp']; + }); + + return $this->processedLogs = $logs; + } + + public function getFilters() + { + $filters = [ + 'channel' => [], + 'priority' => [ + 'Debug' => 100, + 'Info' => 200, + 'Warning' => 300, + 'Error' => 400, + 'Critical' => 500, + 'Alert' => 550, + 'Emergency' => 600, + ], + ]; + + $allChannels = []; + foreach ($this->getProcessedLogs() as $log) { + if ('' === trim($log['channel'])) { + continue; + } + + $allChannels[] = $log['channel']; + } + $channels = array_unique($allChannels); + sort($channels); + $filters['channel'] = $channels; + + return $filters; + } + public function getPriorities() { return $this->data['priorities'] ?? []; @@ -132,7 +208,7 @@ private function getContainerDeprecationLogs(): array $logs = []; foreach (unserialize($logContent) as $log) { $log['context'] = ['exception' => new SilencedErrorContext($log['type'], $log['file'], $log['line'], $log['trace'], $log['count'])]; - $log['timestamp'] = $bootTime; + $log['timestamp'] = (new \DateTimeImmutable())->setTimestamp($bootTime)->format(\DateTimeInterface::RFC3339_EXTENDED); $log['priority'] = 100; $log['priorityName'] = 'DEBUG'; $log['channel'] = null;