Skip to content

Commit

Permalink
Present log datetime in user timezone (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
frankdekker authored Aug 17, 2024
1 parent 0a0393a commit 6ba7367
Show file tree
Hide file tree
Showing 28 changed files with 273 additions and 124 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ read logs from any directory.
- 🔍 **Filter** by log level (error, info, debug, etc.), by channel, date range or log content inclusion or exclusion,
- 🔍 **Search** multiple log files at once,
- 🌑 **Dark mode**,
- 🕑 **Present log entries in your local timezone**,
- 🖥️ **Multiple host** support,
- 💾 **Download** or **delete** log files from the UI,
- ☎️ **API access** for folders, files & log entries,
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/views/LogView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const load = () => {
.set('query', searchStore.query, '')
.set('per_page', searchStore.perPage, '100')
.set('sort', searchStore.sort, 'desc')
.set('offset', offset.value, 0))
.set('offset', offset.value, 0)
.set('time_zone', Intl.DateTimeFormat().resolvedOptions().timeZone))
.catch((error: Error) => {
if (error.message === 'bad-request') {
badRequest.value = true;
Expand Down
30 changes: 1 addition & 29 deletions src/Entity/Index/LogRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,8 @@
use JsonSerializable;
use Psr\Log\LogLevel;

class LogRecord implements JsonSerializable, IdentifierAwareInterface
class LogRecord implements IdentifierAwareInterface
{
// bootstrap 5 text colors
public const LEVEL_CLASSES = [
LogLevel::DEBUG => 'text-info',
LogLevel::INFO => 'text-info',
LogLevel::NOTICE => 'text-info',
LogLevel::WARNING => 'text-warning',
LogLevel::ERROR => 'text-danger',
LogLevel::ALERT => 'text-danger',
LogLevel::CRITICAL => 'text-danger',
LogLevel::EMERGENCY => 'text-danger',
];

/**
* @param string|array<int|string, mixed> $context
* @param string|array<int|string, mixed> $extra
Expand All @@ -40,20 +28,4 @@ public function getIdentifier(): string
{
return $this->identifier;
}

/**
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return [
'datetime' => date('Y-m-d H:i:s', $this->date),
'level_name' => ucfirst($this->severity),
'level_class' => self::LEVEL_CLASSES[$this->severity] ?? 'text-info',
'channel' => $this->channel,
'text' => $this->message,
'context' => $this->context,
'extra' => $this->extra,
];
}
}
3 changes: 1 addition & 2 deletions src/Entity/Output/LogRecordsOutput.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@

namespace FD\LogViewer\Entity\Output;

use FD\LogViewer\Entity\Index\LogRecord;
use FD\LogViewer\Entity\Index\Paginator;
use FD\LogViewer\Entity\Index\PerformanceStats;
use JsonSerializable;

class LogRecordsOutput implements JsonSerializable
{
/**
* @param LogRecord[] $records
* @param array<array-key, mixed> $records
*/
public function __construct(
private readonly array $records,
Expand Down
2 changes: 2 additions & 0 deletions src/Entity/Request/LogQueryDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace FD\LogViewer\Entity\Request;

use DateTimeZone;
use FD\LogViewer\Entity\Expression\Expression;
use FD\LogViewer\Entity\Output\DirectionEnum;

Expand All @@ -15,6 +16,7 @@ class LogQueryDto
*/
public function __construct(
public readonly array $fileIdentifiers,
public readonly DateTimeZone $timeZone,
public readonly ?int $offset = null,
public readonly ?Expression $query = null,
public readonly DirectionEnum $direction = DirectionEnum::Desc,
Expand Down
2 changes: 2 additions & 0 deletions src/Resources/config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
use FD\LogViewer\Service\Parser\WordParser;
use FD\LogViewer\Service\PerformanceService;
use FD\LogViewer\Service\RemoteHost\Authenticator\AuthenticatorFactory;
use FD\LogViewer\Service\Serializer\LogRecordsNormalizer;
use FD\LogViewer\Service\VersionService;
use FD\LogViewer\Util\Clock;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
Expand Down Expand Up @@ -109,6 +110,7 @@
$services->set(LogFolderOutputProvider::class);
$services->set(LogFolderOutputSorter::class);
$services->set(LogRecordsOutputProvider::class);
$services->set(LogRecordsNormalizer::class);
$services->set(LogParser::class)->arg('$clock', inline_service(Clock::class));
$services->set(LogFileParserProvider::class)
->arg('$logParsers', tagged_iterator('fd.symfony.log.viewer.log_file_parser', 'name'));
Expand Down
4 changes: 2 additions & 2 deletions src/Resources/public/.vite/manifest.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"src/main.ts": {
"file": "assets/main-C7fre9xk.js",
"file": "assets/main-BzT1BGZa.js",
"name": "main",
"src": "src/main.ts",
"isEntry": true
},
"style.css": {
"file": "assets/style-BZSHBnOQ.css",
"file": "assets/style-BECflOX-.css",
"src": "style.css"
}
}

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions src/Service/File/LogQueryDtoFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@

namespace FD\LogViewer\Service\File;

use Exception;
use FD\LogViewer\Entity\Output\DirectionEnum;
use FD\LogViewer\Entity\Request\LogQueryDto;
use FD\LogViewer\Reader\String\StringReader;
use FD\LogViewer\Service\Parser\ExpressionParser;
use FD\LogViewer\Service\Parser\InvalidDateTimeException;
use FD\LogViewer\Util\DateUtil;
use Symfony\Component\HttpFoundation\Request;

class LogQueryDtoFactory
Expand All @@ -17,7 +19,7 @@ public function __construct(private readonly ExpressionParser $expressionParser)
}

/**
* @throws InvalidDateTimeException
* @throws InvalidDateTimeException|Exception
*/
public function create(Request $request): LogQueryDto
{
Expand All @@ -27,10 +29,11 @@ public function create(Request $request): LogQueryDto
$query = trim($request->query->get('query', ''));
$direction = DirectionEnum::from($request->query->get('sort', 'desc'));
$perPage = $request->query->getInt('per_page', 100);
$timeZone = DateUtil::tryParseTimezone($request->query->get('time_zone', ''), date_default_timezone_get());

// search expression
$expression = $query === '' ? null : $this->expressionParser->parse(new StringReader($query));
$expression = $query === '' ? null : $this->expressionParser->parse(new StringReader($query), $timeZone);

return new LogQueryDto($fileIdentifiers, $offset, $expression, $direction, $perPage);
return new LogQueryDto($fileIdentifiers, $timeZone, $offset, $expression, $direction, $perPage);
}
}
6 changes: 4 additions & 2 deletions src/Service/File/LogRecordsOutputProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
use FD\LogViewer\Iterator\LimitIterator;
use FD\LogViewer\Iterator\MultiLogRecordIterator;
use FD\LogViewer\Service\PerformanceService;
use FD\LogViewer\Service\Serializer\LogRecordsNormalizer;

class LogRecordsOutputProvider
{
public function __construct(
private readonly LogFileParserProvider $logParserProvider,
private readonly LogRecordsNormalizer $logRecordsNormalizer,
private readonly PerformanceService $performanceService,
) {
}
Expand All @@ -40,7 +42,7 @@ public function provideForFiles(array $files, LogQueryDto $logQuery): LogRecords
$logRecordCollection = new LogRecordCollection($recordIterator, null);

return new LogRecordsOutput(
$logRecordCollection->getRecords(),
$this->logRecordsNormalizer->normalize($logRecordCollection->getRecords(), $logQuery->timeZone),
$logRecordCollection->getPaginator(),
$this->performanceService->getPerformanceStats()
);
Expand All @@ -52,7 +54,7 @@ public function provide(LogFile $file, LogQueryDto $logQuery): LogRecordsOutput
$logRecordCollection = $this->logParserProvider->get($config->type)->getLogIndex($config, $file, $logQuery);

return new LogRecordsOutput(
$logRecordCollection->getRecords(),
$this->logRecordsNormalizer->normalize($logRecordCollection->getRecords(), $logQuery->timeZone),
$logRecordCollection->getPaginator(),
$this->performanceService->getPerformanceStats()
);
Expand Down
5 changes: 3 additions & 2 deletions src/Service/Parser/DateParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@
namespace FD\LogViewer\Service\Parser;

use DateTimeImmutable;
use DateTimeZone;
use Throwable;

class DateParser
{
/**
* @throws InvalidDateTimeException
*/
public function toDateTimeImmutable(string $date): DateTimeImmutable
public function toDateTimeImmutable(string $date, DateTimeZone $timeZone): DateTimeImmutable
{
try {
return new DateTimeImmutable($date);
return new DateTimeImmutable($date, $timeZone);
} catch (Throwable $exception) {
throw new InvalidDateTimeException('Invalid date ' . $date, 0, $exception);
}
Expand Down
5 changes: 3 additions & 2 deletions src/Service/Parser/ExpressionParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace FD\LogViewer\Service\Parser;

use DateTimeZone;
use FD\LogViewer\Entity\Expression\Expression;
use FD\LogViewer\Reader\String\StringReader;

Expand All @@ -27,13 +28,13 @@ public function __construct(private readonly TermParser $termParser)
/**
* @throws InvalidDateTimeException
*/
public function parse(StringReader $string): Expression
public function parse(StringReader $string, DateTimeZone $timeZone): Expression
{
$terms = [];

while ($string->eol() === false) {
$string->skipWhitespace();
$terms[] = $this->termParser->parse($string);
$terms[] = $this->termParser->parse($string, $timeZone);
$string->skipWhitespace();
}

Expand Down
8 changes: 5 additions & 3 deletions src/Service/Parser/TermParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

namespace FD\LogViewer\Service\Parser;

use DateTime;
use DateTimeZone;
use FD\LogViewer\Entity\Expression\ChannelTerm;
use FD\LogViewer\Entity\Expression\DateAfterTerm;
use FD\LogViewer\Entity\Expression\DateBeforeTerm;
Expand Down Expand Up @@ -30,16 +32,16 @@ public function __construct(
/**
* @throws InvalidDateTimeException
*/
public function parse(StringReader $string): TermInterface
public function parse(StringReader $string, DateTimeZone $timeZone): TermInterface
{
$string->skipWhitespace();

if ($string->read('before:') || $string->read('b:')) {
return new DateBeforeTerm($this->dateParser->toDateTimeImmutable($this->stringParser->parse($string)));
return new DateBeforeTerm($this->dateParser->toDateTimeImmutable($this->stringParser->parse($string), $timeZone));
}

if ($string->read('after:') || $string->read('a:')) {
return new DateAfterTerm($this->dateParser->toDateTimeImmutable($this->stringParser->parse($string)));
return new DateAfterTerm($this->dateParser->toDateTimeImmutable($this->stringParser->parse($string), $timeZone));
}

if ($string->read('severity:') || $string->read('s:')) {
Expand Down
57 changes: 57 additions & 0 deletions src/Service/Serializer/LogRecordsNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace FD\LogViewer\Service\Serializer;

use DateTime;
use DateTimeZone;
use FD\LogViewer\Entity\Index\LogRecord;
use Psr\Log\LogLevel;

class LogRecordsNormalizer
{
// bootstrap 5 text colors
private const LEVEL_CLASSES = [
LogLevel::DEBUG => 'text-info',
LogLevel::INFO => 'text-info',
LogLevel::NOTICE => 'text-info',
LogLevel::WARNING => 'text-warning',
LogLevel::ERROR => 'text-danger',
LogLevel::ALERT => 'text-danger',
LogLevel::CRITICAL => 'text-danger',
LogLevel::EMERGENCY => 'text-danger',
];

/**
* @param LogRecord[] $records
*
* @return array{
* datetime: string,
* level_name: string,
* level_class: string,
* channel: string,
* text: string,
* context: string|array<array-key, mixed>,
* extra: string|array<array-key, mixed>
* }[]
*/
public function normalize(array $records, DateTimeZone $timeZone): array
{
$date = new DateTime(timezone: $timeZone);
$result = [];
foreach ($records as $record) {
$result[] = [
'datetime' => $date->setTimestamp($record->date)->format('Y-m-d H:i:s'),
'level_name' => ucfirst($record->severity),
'level_class' => self::LEVEL_CLASSES[$record->severity] ?? 'text-info',
'channel' => $record->channel,
'text' => $record->message,
'context' => $record->context,
'extra' => $record->extra,
];
}

return $result;
}
}
27 changes: 27 additions & 0 deletions src/Util/DateUtil.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace FD\LogViewer\Util;

use DateTimeZone;
use Exception;

class DateUtil
{
/**
* @throws Exception
*/
public static function tryParseTimezone(?string $timezone, string $default): DateTimeZone
{
if ($timezone === null) {
return new DateTimeZone($default);
}

try {
return new DateTimeZone($timezone);
} catch (Exception) {
return new DateTimeZone($default);
}
}
}
Loading

0 comments on commit 6ba7367

Please sign in to comment.