Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow to show logs of all logging types by proxying messages #1041

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion css/logreader-main.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/logreader-main.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/logreader-main.mjs.map

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions lib/BackgroundJobs/Rotate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2018 Arthur Schiwon <blizzz@arthur-schiwon.de>
*
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\LogReader\BackgroundJobs;

use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\IConfig;

class Rotate extends TimedJob {

public function __construct(
ITimeFactory $time,
private IConfig $config,
private IAppData $appData,
) {
parent::__construct($time);

$this->setInterval(60 * 60 * 3);
}

protected function run($argument): void {
try {
$folder = $this->appData->getFolder('logreader');
$file = $folder->getFile('nextcloud.log');
} catch (NotFoundException $e) {
// Nothing to do if there is no log
return;
}

$maxSize = $this->config->getSystemValue('log_rotate_size', 100 * 1024 * 1024);
if ($file->getSize() < $maxSize) {
// nothing to do
return;
}

if ($folder->fileExists('nextcloud.1.log')) {
$rotatedFile = $folder->getFile('nextcloud.1.log');
} else {
$rotatedFile = $folder->newFile('nextcloud.1.log');
}
$rotatedFile->putContent($file->getContent());
$file->putContent('');
}
}
15 changes: 0 additions & 15 deletions lib/Controller/LogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
use OCA\LogReader\Log\SearchFilter;
use OCA\LogReader\Service\SettingsService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -55,13 +54,6 @@ public function __construct($appName,
* @return JSONResponse
*/
public function get($query = '', $count = 50, $offset = 0): JSONResponse {
$logType = $this->settingsService->getLoggingType();
// we only support web access when `log_type` is set to `file` (the default)
if ($logType !== 'file') {
$this->logger->debug('File-based logging must be enabled to access logs from the Web UI.');
return new JSONResponse([], Http::STATUS_FAILED_DEPENDENCY);
}

$iterator = $this->logIteratorFactory->getLogIterator($this->settingsService->getShownLevels());

if ($query !== '') {
Expand Down Expand Up @@ -100,13 +92,6 @@ private function getLastItem() {
* request.
*/
public function poll(string $lastReqId): JSONResponse {
$logType = $this->settingsService->getLoggingType();
// we only support web access when `log_type` is set to `file` (the default)
if ($logType !== 'file') {
$this->logger->debug('File-based logging must be enabled to access logs from the Web UI.');
return new JSONResponse([], Http::STATUS_FAILED_DEPENDENCY);
}

$lastItem = $this->getLastItem();
if ($lastItem === null || $lastItem['reqId'] === $lastReqId) {
return new JSONResponse([]);
Expand Down
20 changes: 16 additions & 4 deletions lib/Listener/LogListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use OC\SystemConfig;
use OCA\LogReader\Log\Console;
use OCA\LogReader\Log\Formatter;
use OCA\LogReader\Log\LogProxy;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Log\BeforeMessageLoggedEvent;
Expand All @@ -35,9 +36,14 @@
* @template-implements IEventListener<BeforeMessageLoggedEvent>
*/
class LogListener implements IEventListener {
private ?Console $console;
private ?Console $console = null;
private ?LogProxy $proxy = null;

public function __construct(Formatter $formatter, SystemConfig $config) {
public function __construct(
Formatter $formatter,
SystemConfig $config,
LogProxy $proxy,
) {
if (defined('OC_CONSOLE') && \OC_CONSOLE) {
$level = getenv('OCC_LOG');
if ($level) {
Expand All @@ -46,8 +52,11 @@ public function __construct(Formatter $formatter, SystemConfig $config) {
} else {
$this->console = null;
}
} else {
$this->console = null;
}

// Only create a proxy file if log type is not file
if ($config->getValue('log_type', 'file') !== 'file') {
$this->proxy = $proxy;
}
}

Expand All @@ -60,5 +69,8 @@ public function handle(Event $event): void {
if ($this->console) {
$this->console->log($event->getLevel(), $event->getApp(), $event->getMessage());
}
if ($this->proxy) {
$this->proxy->log($event->getLevel(), $event->getApp(), $event->getMessage());
}
}
}
53 changes: 38 additions & 15 deletions lib/Log/LogIteratorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@

namespace OCA\LogReader\Log;

use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\IConfig;
use OCP\Log\IFileBased;
use OCP\Log\ILogFactory;

class LogIteratorFactory {
private $config;
private $logFactory;

public function __construct(IConfig $config, ILogFactory $logFactory) {
$this->config = $config;
$this->logFactory = $logFactory;
public function __construct(
private IConfig $config,
private ILogFactory $logFactory,
private IAppData $appData,
) {
}

/**
Expand All @@ -44,18 +46,39 @@ public function __construct(IConfig $config, ILogFactory $logFactory) {
public function getLogIterator(array $levels) {
$dateFormat = $this->config->getSystemValue('logdateformat', \DateTime::ATOM);
$timezone = $this->config->getSystemValue('logtimezone', 'UTC');
$log = $this->logFactory->get('file');
if ($log instanceof IFileBased) {
$handle = fopen($log->getLogFilePath(), 'rb');
if ($handle) {
$iterator = new LogIterator($handle, $dateFormat, $timezone);
return new \CallbackFilterIterator($iterator, function ($logItem) use ($levels) {
return $logItem && in_array($logItem['level'], $levels);
});

if ($this->config->getSystemValue('log_type', 'file') !== 'file') {
try {
$folder = $this->appData->getFolder('logreader');
} catch (NotFoundException $e) {
$folder = $this->appData->newFolder('logreader');
}

try {
$logfile = $folder->getFile('nextcloud.log');
} catch (NotFoundException $e) {
$logfile = $folder->newFile('nextcloud.log');
}

$handle = $logfile->read();
if (!$handle) {
throw new \Exception('Error while opening internal logfile');
}
} else {
$logfile = $this->logFactory->get('file');
if ($logfile instanceof IFileBased) {
$handle = fopen($logfile->getLogFilePath(), 'rb');
if (!$handle) {
throw new \Exception("Error while opening " . $logfile->getLogFilePath());
}
} else {
throw new \Exception("Error while opening " . $log->getLogFilePath());
throw new \Exception('Can\'t find log class');
}
}
throw new \Exception('Can\'t find log class');

$iterator = new LogIterator($handle, $dateFormat, $timezone);
return new \CallbackFilterIterator($iterator, function ($logItem) use ($levels) {
return $logItem && in_array($logItem['level'], $levels);
});
}
}
82 changes: 82 additions & 0 deletions lib/Log/LogProxy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\LogReader\Log;

use OC\Log\LogDetails;
use OC\SystemConfig;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFile;

/**
* Create a logfile even for syslog etc loggers
*/
class LogProxy extends LogDetails {
private ISimpleFile $logfile;

public function __construct(IAppData $appData, private SystemConfig $config) {
parent::__construct($config);

try {
$folder = $appData->getFolder('logreader');
} catch (NotFoundException $e) {
$folder = $appData->newFolder('logreader');
}

try {
$this->logfile = $folder->getFile('nextcloud.log');
} catch (NotFoundException $e) {
$this->logfile = $folder->newFile('nextcloud.log');
}
}

public function log(int $level, string $app, array $entry) {
if ($level >= $this->config->getValue('loglevel', 1)) {
$hasBacktrace = isset($entry['exception']);
$logBacktrace = $this->config->getValue('log.backtrace', false);
if (!$hasBacktrace && $logBacktrace) {
$entry['backtrace'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
}

$entry = $this->logDetailsAsJSON($app, $entry, $level);

$handle = $this->logfile->read();
if ($handle) {
$content = stream_get_contents($handle);
fclose($handle);
}

$handle = $this->logfile->write();
if ($handle) {
if (isset($content) && $content !== false) {
fwrite($handle, $content);
}
fwrite($handle, $entry."\n");
fclose($handle);
} else {
throw new \Error('Could not open internal log file.');
}
}
}
}
1 change: 0 additions & 1 deletion lib/Service/SettingsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ public function getAppSettings(): array {
Constants::CONFIG_KEY_DATETIMEFORMAT => $this->getDateTimeFormat(),
Constants::CONFIG_KEY_RELATIVEDATES => $this->getRelativeDates(),
Constants::CONFIG_KEY_LIVELOG => $this->getLiveLog(),
'enabled' => $this->getLoggingType() === 'file',
];
}

Expand Down
4 changes: 3 additions & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
<directory name="vendor/nextcloud/ocp"/>
</extraFiles>
<stubs>
<file name="tests/stubs/stub.phpstub" preloadClasses="true"/>
<file name="tests/stubs/oc_core_command.php" preloadClasses="true"/>
<file name="tests/stubs/oc_log_LogDetails.php"/>
<file name="tests/stubs/oc_SystemConfig.php" preloadClasses="true"/>
</stubs>
<issueHandlers>
<UndefinedClass>
Expand Down
31 changes: 7 additions & 24 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</NcButton>
</div>
<!-- Show information / warning message -->
<NcNoteCard v-if="settingsStore.localFile" type="info" class="info-note">
<NcNoteCard v-if="!settingsStore.isServerLogShown" type="info" class="info-note">
<div class="info-note__content">
<p>{{ t('logreader', 'Currently the log file {file} is shown', { file: settingsStore.localFileName }) }}</p>
<NcButton type="secondary" @click="onShowServerLog">
Expand All @@ -30,17 +30,13 @@
<p>{{ t('logreader', 'Live view is disabled') }}</p>
</NcNoteCard>
<!-- Show the log file table -->
<LogTable v-if="settingsStore.enabled" :rows="entries" />
<NcEmptyContent v-else :name="t('logreader', 'No log file')">
<LogTable v-if="hasEntries" :rows="entries" />
<NcEmptyContent v-else
:name="t('logreader', 'No log entries')"
:description="t('logreader', 'The log is currently empty. Try to select a different logging level.')">
<template #icon>
<IconFormatList :size="20" />
</template>
<template #description>
{{ t('logreader', 'File-based logging must be enabled to access logs from the Web UI.') }}
<br>
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="noLogDescription" />
</template>
</NcEmptyContent>
<!-- App settings dialog will be mounted on page body -->
<AppSettingsDialog :open.sync="areSettingsShown" />
Expand Down Expand Up @@ -74,6 +70,8 @@ const loggingStore = useLogStore()

const entries = computed(() => loggingStore.entries)

const hasEntries = computed(() => loggingStore.allEntries.length > 0)

const onShowServerLog = () => {
settingsStore.localFile = undefined
// remove local entries
Expand All @@ -99,21 +97,6 @@ onMounted(() => {
onUnmounted(() => {
loggingStore.stopPolling()
})

/** Translated description what to check in case no log can be loaded */
const noLogDescription = t(
'logreader',
'If you feel this is an error, please verify {setting} in your {config} and check the Nextcloud Administration Manual.',
{
setting: '<code>log_type</code>',
config: '<code>config.php</code>',
},
0,
{
sanitize: false,
escape: false,
},
)
</script>

<style lang="scss" scoped>
Expand Down
Loading