Skip to content

Commit b50c969

Browse files
authored
Merge pull request #14187 from nextcloud/jtr/docs-dev-logging-v34
2 parents 195bd11 + 497337a commit b50c969

1 file changed

Lines changed: 279 additions & 12 deletions

File tree

developer_manual/basics/logging.rst

Lines changed: 279 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ Logging
44

55
.. sectionauthor:: Bernhard Posselt <dev@bernhard-posselt.com>
66

7-
The logger is present by default in the container. The app that is logging is
8-
set automatically. Nextcloud uses a :ref:`PSR3 <psr3>` logger.
7+
Nextcloud uses a :ref:`PSR-3 <psr3>` logger (``Psr\Log\LoggerInterface``).
8+
The recommended way to use it is via :ref:`dependency injection <dependency-injection>`.
99

10-
The logger can be used in the following way:
10+
Basic usage
11+
-----------
12+
13+
Inject ``Psr\Log\LoggerInterface`` into your class. When the logger is resolved
14+
from an app container, Nextcloud automatically wraps it so that every log
15+
message is attributed to your app -- you do **not** need to pass an app name
16+
yourself:
1117

1218
.. code-block:: php
1319
@@ -17,19 +23,21 @@ The logger can be used in the following way:
1723
use Psr\Log\LoggerInterface;
1824
1925
class AuthorService {
20-
private LoggerInterface $logger;
21-
private string $appName;
22-
23-
public function __construct(LoggerInterface $logger, string $appName){
24-
$this->logger = $logger;
25-
$this->appName = $appName;
26+
public function __construct(
27+
private LoggerInterface $logger,
28+
) {
2629
}
2730
28-
public function log($message) {
29-
$this->logger->error($message, ['extra_context' => 'my extra context']);
31+
public function doSomething(): void {
32+
$this->logger->error('Something went wrong');
3033
}
3134
}
3235
36+
.. note::
37+
Behind the scenes, each app's DI container registers a ``ScopedPsrLogger``
38+
that prepends ``['app' => '<your-app-id>']`` to the context of every call.
39+
You do not need to set this manually.
40+
3341
In cases where you can not :ref:`inject <dependency-injection>` a logger into a class, you can use
3442
the ``\OCP\Log\logger`` function to acquire a logger instance. As a first argument you need to pass
3543
the app ID.
@@ -40,7 +48,253 @@ the app ID.
4048
4149
use function OCP\Log\logger;
4250
43-
logger('calendar')->warning('look, no dependency injection');
51+
logger('my_app')->warning('look, no dependency injection');
52+
53+
54+
Available log methods
55+
---------------------
56+
57+
The logger exposes the full set of PSR-3 methods:
58+
59+
- ``emergency()`` -- System is unusable
60+
- ``alert()`` -- Action must be taken immediately
61+
- ``critical()`` -- Critical conditions
62+
- ``error()`` -- Runtime errors
63+
- ``warning()`` -- Exceptional occurrences that are not errors
64+
- ``notice()`` -- Normal but significant events
65+
- ``info()`` -- Interesting events (e.g. user logs in)
66+
- ``debug()`` -- Detailed debug information
67+
68+
Each method has the signature ``(string|\Stringable $message, array $context = []): void``.
69+
70+
71+
Log level mapping
72+
-----------------
73+
74+
PSR-3 defines eight severity levels, but Nextcloud internally uses five numeric
75+
levels (set via the ``loglevel`` setting in ``config.php``). The mapping is:
76+
77+
.. list-table::
78+
:header-rows: 1
79+
80+
* - PSR-3 level
81+
- Nextcloud level
82+
- Numeric value
83+
* - ``emergency``
84+
- FATAL
85+
- 4
86+
* - ``alert``, ``critical``, ``error``
87+
- ERROR
88+
- 3
89+
* - ``warning``
90+
- WARN
91+
- 2
92+
* - ``notice``, ``info``
93+
- INFO
94+
- 1
95+
* - ``debug``
96+
- DEBUG
97+
- 0
98+
99+
A message is written to the log only when its numeric level is **** the
100+
configured ``loglevel`` (default: ``2`` / WARN).
101+
102+
103+
Message interpolation
104+
---------------------
105+
106+
Nextcloud supports PSR-3 message interpolation. Context values whose keys
107+
appear as ``{placeholder}`` tokens in the message string are substituted
108+
automatically:
109+
110+
.. code-block:: php
111+
112+
$this->logger->warning('User {user} failed to log in', [
113+
'user' => $userId,
114+
]);
115+
116+
This produces a log message like ``User jane failed to log in``. The ``user``
117+
key is consumed by the interpolation and will not appear separately in the
118+
stored log entry.
119+
120+
121+
Context array
122+
-------------
123+
124+
The second argument to every log method is a context array. Nextcloud
125+
recognises several special keys:
126+
127+
``exception``
128+
If the context contains an ``exception`` key whose value is a
129+
``\Throwable``, Nextcloud will serialize the full exception (class name,
130+
message, code, file, line, stack trace and any previous exceptions). **This
131+
is the recommended way to log exceptions:**
132+
133+
.. code-block:: php
134+
135+
try {
136+
$this->doRiskyThing();
137+
} catch (\Exception $e) {
138+
$this->logger->error('Operation failed', ['exception' => $e]);
139+
}
140+
141+
You can provide a custom message as the first argument; if you omit it, the
142+
exception's own message is used.
143+
144+
``app``
145+
Identifies the app that produced the message. This is **set automatically**
146+
when you obtain the logger via dependency injection or the ``logger()``
147+
helper. You can override it explicitly if needed:
148+
149+
.. code-block:: php
150+
151+
$this->logger->info('Cross-app note', ['app' => 'other_app']);
152+
153+
Any other keys you add to the context array are included in the log entry as
154+
additional data:
155+
156+
.. code-block:: php
157+
158+
$this->logger->info('Cron job finished', [
159+
'duration' => $seconds,
160+
'items_processed' => $count,
161+
]);
162+
163+
164+
Logging exceptions
165+
------------------
166+
167+
As shown above, passing an ``exception`` key in the context triggers detailed
168+
exception serialization. This is preferred over manually calling
169+
``$e->getMessage()`` because Nextcloud will capture the full stack trace and
170+
chain of previous exceptions.
171+
172+
.. code-block:: php
173+
174+
try {
175+
$this->service->process($data);
176+
} catch (\OCP\DB\Exception $e) {
177+
$this->logger->error('Database operation failed for item {id}', [
178+
'id' => $itemId,
179+
'exception' => $e,
180+
]);
181+
}
182+
183+
You can combine message interpolation with exception logging -- Nextcloud
184+
handles both.
185+
186+
187+
Custom log file
188+
---------------
189+
190+
If your app needs to write to a **separate log file** (for example, to keep
191+
verbose debug output out of the main log), you can use
192+
``OCP\Log\ILogFactory::getCustomPsrLogger()``:
193+
194+
.. code-block:: php
195+
196+
<?php
197+
namespace OCA\MyApp\Service;
198+
199+
use OCP\Log\ILogFactory;
200+
use Psr\Log\LoggerInterface;
201+
202+
class ImportService {
203+
private LoggerInterface $importLogger;
204+
205+
public function __construct(ILogFactory $logFactory) {
206+
$this->importLogger = $logFactory->getCustomPsrLogger(
207+
'/var/log/nextcloud/import.log'
208+
);
209+
}
210+
211+
public function run(): void {
212+
$this->importLogger->info('Import started');
213+
}
214+
}
215+
216+
The method signature is:
217+
218+
.. code-block:: php
219+
220+
public function getCustomPsrLogger(
221+
string $path,
222+
string $type = 'file',
223+
string $tag = 'Nextcloud'
224+
): LoggerInterface;
225+
226+
``$type`` can be ``file`` (default), ``errorlog``, ``syslog``, or ``systemd``.
227+
228+
.. note::
229+
``getCustomPsrLogger`` has been available since Nextcloud 22.
230+
The ``$type`` and ``$tag`` parameters were added in Nextcloud 24.
231+
232+
233+
Structured data logging
234+
-----------------------
235+
236+
For advanced use cases, the logger also implements ``OCP\Log\IDataLogger``
237+
(since Nextcloud 18.0.1), which provides a ``logData()`` method for logging
238+
arbitrary structured data:
239+
240+
.. code-block:: php
241+
242+
use OCP\Log\IDataLogger;
243+
244+
/** @var IDataLogger $logger */
245+
$logger = \OCP\Server::get(\Psr\Log\LoggerInterface::class);
246+
$logger->logData('Sync completed', [
247+
'provider' => 'caldav',
248+
'items' => 42,
249+
], [
250+
'app' => 'my_app',
251+
'level' => \OCP\ILogger::INFO,
252+
]);
253+
254+
The first argument is a message string, the second is the structured data array,
255+
and the third is an optional context array (which accepts the ``app`` and
256+
``level`` keys described above).
257+
258+
259+
Listening to log events
260+
-----------------------
261+
262+
If your app needs to react when log messages are written (e.g. to forward them
263+
to an external monitoring service), you can listen for the
264+
``OCP\Log\BeforeMessageLoggedEvent`` event (since Nextcloud 28):
265+
266+
.. code-block:: php
267+
268+
<?php
269+
namespace OCA\MyApp\Listener;
270+
271+
use OCP\EventDispatcher\Event;
272+
use OCP\EventDispatcher\IEventListener;
273+
use OCP\Log\BeforeMessageLoggedEvent;
274+
275+
/** @template-implements IEventListener<BeforeMessageLoggedEvent> */
276+
class LogEventListener implements IEventListener {
277+
public function handle(Event $event): void {
278+
if (!$event instanceof BeforeMessageLoggedEvent) {
279+
return;
280+
}
281+
282+
$app = $event->getApp();
283+
$level = $event->getLevel();
284+
$message = $event->getMessage(); // array with the log entry
285+
// ... forward to external service
286+
}
287+
}
288+
289+
Register the listener in your ``Application::register()`` method:
290+
291+
.. code-block:: php
292+
293+
$context->registerEventListener(
294+
BeforeMessageLoggedEvent::class,
295+
LogEventListener::class
296+
);
297+
44298
45299
Admin audit logging
46300
-------------------
@@ -63,3 +317,16 @@ You can easily add a log by simply emitting an ``OCP\Log\Audit\CriticalActionPer
63317
['name' => 'My App ID']
64318
);
65319
$dispatcher->dispatchTyped($event);
320+
321+
The constructor also accepts an optional third parameter
322+
``bool $obfuscateParameters = false``. Set it to ``true`` when the parameters
323+
may contain sensitive information (passwords, tokens, etc.) that should not be
324+
written in clear text to the audit log:
325+
326+
.. code-block:: php
327+
328+
$event = new \OCP\Log\Audit\CriticalActionPerformedEvent(
329+
'Password changed for user %s',
330+
['name' => $userId],
331+
true // obfuscate parameters in the audit log
332+
);

0 commit comments

Comments
 (0)