Skip to content

Commit fb50fd4

Browse files
authored
Merge pull request #5 from KaririCode-Framework/develop
Develop
2 parents 69bce09 + 4d78d84 commit fb50fd4

15 files changed

+2871
-262
lines changed

src/Formatter/AbstractFormatter.php

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,79 @@
66

77
use KaririCode\Contract\ImmutableValue;
88
use KaririCode\Contract\Logging\LogFormatter;
9-
use KaririCode\Contract\Logging\Structural\FormatterAware;
109

11-
abstract class AbstractFormatter implements LogFormatter, FormatterAware, ImmutableValue
10+
abstract class AbstractFormatter implements LogFormatter
1211
{
13-
protected ImmutableValue $formatter;
14-
12+
/**
13+
* @param string $dateFormat The date format pattern for timestamps
14+
* @param bool $includeContext Whether to include context in formatted output
15+
* @param bool $includeExtra Whether to include extra data in formatted output
16+
*/
1517
public function __construct(
16-
protected string $dateFormat = 'Y-m-d H:i:s'
18+
public readonly string $dateFormat = 'Y-m-d H:i:s',
19+
public readonly bool $includeContext = true,
20+
public readonly bool $includeExtra = false
1721
) {
18-
$this->formatter = $this;
1922
}
2023

24+
/**
25+
* Format a single log record.
26+
*/
2127
abstract public function format(ImmutableValue $record): string;
2228

29+
/**
30+
* Format multiple log records.
31+
*/
2332
public function formatBatch(array $records): string
2433
{
25-
return implode("\n", array_map([$this, 'format'], $records));
34+
return implode(PHP_EOL, array_map([$this, 'format'], $records));
35+
}
36+
37+
/**
38+
* Create a new formatter with different date format.
39+
*/
40+
public function withDateFormat(string $dateFormat): static
41+
{
42+
return new static(
43+
$dateFormat,
44+
$this->includeContext,
45+
$this->includeExtra
46+
);
2647
}
2748

28-
public function setFormatter(ImmutableValue $formatter): AbstractFormatter
49+
/**
50+
* Create a new formatter with context inclusion setting.
51+
*/
52+
public function withContextInclusion(bool $include): static
2953
{
30-
$this->formatter = $formatter;
54+
return new static(
55+
$this->dateFormat,
56+
$include,
57+
$this->includeExtra
58+
);
59+
}
3160

32-
return $this;
61+
/**
62+
* Format the timestamp according to the configured format.
63+
*/
64+
protected function formatTimestamp(\DateTimeImmutable $datetime): string
65+
{
66+
return $datetime->format($this->dateFormat);
3367
}
3468

35-
public function getFormatter(): ImmutableValue
69+
/**
70+
* Check if the formatter should include context.
71+
*/
72+
protected function shouldIncludeContext(array $context): bool
3673
{
37-
return $this->formatter;
74+
return $this->includeContext && !empty($context);
3875
}
3976

40-
public function toArray(): array
77+
/**
78+
* Check if the formatter should include extra data.
79+
*/
80+
protected function shouldIncludeExtra(array $extra): bool
4181
{
42-
return [
43-
'dateFormat' => $this->dateFormat,
44-
'formatter' => $this->formatter->toArray() ?? null,
45-
];
82+
return $this->includeExtra && !empty($extra);
4683
}
4784
}

src/Formatter/JsonFormatter.php

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,145 @@
55
namespace KaririCode\Logging\Formatter;
66

77
use KaririCode\Contract\ImmutableValue;
8+
use KaririCode\Logging\LogRecord;
89

9-
class JsonFormatter extends AbstractFormatter
10+
/**
11+
* JSON formatter optimized for direct property access.
12+
*/
13+
final class JsonFormatter extends AbstractFormatter
1014
{
11-
private const JSON_OPTIONS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE;
15+
private const JSON_OPTIONS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR;
1216

17+
/**
18+
* @param string $dateFormat Date format pattern
19+
* @param bool $includeContext Include context in output
20+
* @param bool $includeExtra Include extra data in output
21+
* @param bool $prettyPrint Pretty print JSON output
22+
* @param bool $includeStacktraces Include stack traces for exceptions
23+
*/
24+
public function __construct(
25+
string $dateFormat = 'Y-m-d H:i:s',
26+
bool $includeContext = true,
27+
bool $includeExtra = false,
28+
public readonly bool $prettyPrint = false,
29+
public readonly bool $includeStacktraces = false
30+
) {
31+
parent::__construct($dateFormat, $includeContext, $includeExtra);
32+
}
33+
34+
/**
35+
* Format log record to JSON using direct property access.
36+
*/
1337
public function format(ImmutableValue $record): string
1438
{
15-
$data = $this->prepareData($record);
39+
if (!$record instanceof LogRecord) {
40+
throw new \InvalidArgumentException('Record must be an instance of LogRecord');
41+
}
42+
43+
// Direct property access - no toArray() needed
44+
$data = [
45+
'datetime' => $this->formatTimestamp($record->datetime),
46+
'level' => $record->level->value,
47+
'message' => $record->getMessageAsString(),
48+
];
49+
50+
// Conditionally add context and extra
51+
if ($this->shouldIncludeContext($record->context)) {
52+
$data['context'] = $this->processContext($record->context);
53+
}
54+
55+
if ($this->shouldIncludeExtra($record->extra)) {
56+
$data['extra'] = $record->extra;
57+
}
1658

1759
return $this->encodeJson($data);
1860
}
1961

62+
/**
63+
* Format batch of records.
64+
*/
2065
public function formatBatch(array $records): string
2166
{
22-
$formattedRecords = array_map([$this, 'prepareData'], $records);
67+
$formattedRecords = array_map(
68+
fn ($record) => $this->prepareData($record),
69+
$records
70+
);
2371

2472
return $this->encodeJson($formattedRecords);
2573
}
2674

75+
/**
76+
* Prepare data from record using direct property access.
77+
*/
2778
private function prepareData(ImmutableValue $record): array
2879
{
29-
$data = [
30-
'datetime' => $record->datetime->format($this->dateFormat),
80+
if (!$record instanceof LogRecord) {
81+
throw new \InvalidArgumentException('Record must be an instance of LogRecord');
82+
}
83+
84+
return [
85+
'datetime' => $this->formatTimestamp($record->datetime),
3186
'level' => $record->level->value,
32-
'message' => $record->message,
87+
'message' => $record->getMessageAsString(),
88+
'context' => $record->hasContext() ? $this->processContext($record->context) : null,
89+
'extra' => $record->hasExtra() ? $record->extra : null,
90+
];
91+
}
92+
93+
/**
94+
* Process context to handle exceptions if needed.
95+
*/
96+
private function processContext(array $context): array
97+
{
98+
if (!$this->includeStacktraces) {
99+
return $context;
100+
}
101+
102+
// Handle exceptions in context
103+
foreach ($context as $key => $value) {
104+
if ($value instanceof \Throwable) {
105+
$context[$key] = $this->formatException($value);
106+
}
107+
}
108+
109+
return $context;
110+
}
111+
112+
/**
113+
* Format exception for JSON output.
114+
*/
115+
private function formatException(\Throwable $exception): array
116+
{
117+
$formatted = [
118+
'class' => get_class($exception),
119+
'message' => $exception->getMessage(),
120+
'code' => $exception->getCode(),
121+
'file' => $exception->getFile(),
122+
'line' => $exception->getLine(),
33123
];
34124

35-
if (!empty($record->context)) {
36-
$data['context'] = $record->context;
125+
if ($this->includeStacktraces) {
126+
$formatted['trace'] = $exception->getTraceAsString();
127+
}
128+
129+
if ($exception->getPrevious()) {
130+
$formatted['previous'] = $this->formatException($exception->getPrevious());
37131
}
38132

39-
return $data;
133+
return $formatted;
40134
}
41135

42-
private function encodeJson($data): string
136+
/**
137+
* Encode data to JSON with configured options.
138+
*/
139+
private function encodeJson(mixed $data): string
43140
{
44-
return json_encode($data, self::JSON_OPTIONS | JSON_THROW_ON_ERROR);
141+
$options = self::JSON_OPTIONS;
142+
143+
if ($this->prettyPrint) {
144+
$options |= JSON_PRETTY_PRINT;
145+
}
146+
147+
return json_encode($data, $options);
45148
}
46149
}

src/LogRecord.php

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@
77
use KaririCode\Contract\ImmutableValue;
88
use KaririCode\Contract\Logging\LogLevel;
99

10-
class LogRecord implements ImmutableValue
10+
final class LogRecord implements ImmutableValue
1111
{
12+
/**
13+
* @param LogLevel $level The severity level of the log
14+
* @param string|\Stringable $message The log message
15+
* @param array<string, mixed> $context Additional contextual data
16+
* @param \DateTimeImmutable $datetime Timestamp of the log record
17+
* @param array<string, mixed> $extra Extra metadata
18+
*/
1219
public function __construct(
1320
public readonly LogLevel $level,
1421
public readonly string|\Stringable $message,
@@ -18,14 +25,56 @@ public function __construct(
1825
) {
1926
}
2027

21-
public function toArray(): array
28+
/**
29+
* Create a new instance with modified context
30+
* Following the immutability pattern.
31+
*/
32+
public function withContext(array $context): self
2233
{
23-
return [
24-
'level' => $this->level->value,
25-
'message' => $this->message,
26-
'context' => $this->context,
27-
'datetime' => $this->datetime,
28-
'extra' => $this->extra,
29-
];
34+
return new self(
35+
$this->level,
36+
$this->message,
37+
array_merge($this->context, $context),
38+
$this->datetime,
39+
$this->extra
40+
);
41+
}
42+
43+
/**
44+
* Create a new instance with modified extra data.
45+
*/
46+
public function withExtra(array $extra): self
47+
{
48+
return new self(
49+
$this->level,
50+
$this->message,
51+
$this->context,
52+
$this->datetime,
53+
array_merge($this->extra, $extra)
54+
);
55+
}
56+
57+
/**
58+
* Get the string representation of the message.
59+
*/
60+
public function getMessageAsString(): string
61+
{
62+
return (string) $this->message;
63+
}
64+
65+
/**
66+
* Check if the record has context data.
67+
*/
68+
public function hasContext(): bool
69+
{
70+
return !empty($this->context);
71+
}
72+
73+
/**
74+
* Check if the record has extra data.
75+
*/
76+
public function hasExtra(): bool
77+
{
78+
return !empty($this->extra);
3079
}
3180
}

src/LoggerBuilder.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
use KaririCode\Contract\Logging\LogFormatter;
88
use KaririCode\Contract\Logging\Logger;
9-
use KaririCode\Contract\Logging\Structural\FormatterAware;
109
use KaririCode\Contract\Logging\Structural\HandlerAware;
1110
use KaririCode\Contract\Logging\Structural\ProcessorAware;
1211
use KaririCode\Logging\Formatter\LineFormatter;
@@ -38,7 +37,7 @@ public function withProcessor(ProcessorAware $processor): self
3837
return $this;
3938
}
4039

41-
public function withFormatter(FormatterAware $formatter): self
40+
public function withFormatter(LogFormatter $formatter): self
4241
{
4342
$this->formatter = $formatter;
4443

0 commit comments

Comments
 (0)