migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.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\AppInfo;
use OCA\LogReader\Listener\LogListener;
use OCA\LogReader\Log\Formatter;
use OCA\LogReader\SetupChecks\LogErrors;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Log\BeforeMessageLoggedEvent;
use Psr\Container\ContainerInterface;
class Application extends App implements IBootstrap {
public function __construct(array $urlParams = []) {
parent::__construct('logreader', $urlParams);
}
public function register(IRegistrationContext $context): void {
$context->registerEventListener(BeforeMessageLoggedEvent::class, LogListener::class);
$context->registerService(Formatter::class, function (ContainerInterface $c) {
return new Formatter(\OC::$SERVERROOT);
});
$context->registerSetupCheck(LogErrors::class);
}
public function boot(IBootContext $context): void {
}
}
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.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\Command;
use OC\Core\Command\Base;
use OCA\LogReader\Log\Formatter;
use OCA\LogReader\Log\LogIteratorFactory;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Terminal;
class Tail extends Base {
public const LEVELS = ['Debug', 'Info', 'Warning', 'Error', 'Fatal'];
private $formatter;
private $logIteratorFactory;
public function __construct(Formatter $formatter, LogIteratorFactory $logIteratorFactory) {
parent::__construct();
$this->formatter = $formatter;
$this->logIteratorFactory = $logIteratorFactory;
}
protected function configure() {
$this
->setName('log:tail')
->setDescription('Tail the nextcloud logfile')
->addArgument('lines', InputArgument::OPTIONAL, 'The number of log entries to print', "10")
->addOption('follow', 'f', InputOption::VALUE_NONE, 'Output new log entries as they appear')
->addOption('raw', 'r', InputOption::VALUE_NONE, 'Output raw log json instead of formatted log item');
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$raw = $input->getOption('raw');
$count = (int)$input->getArgument('lines');
$io = new SymfonyStyle($input, $output);
$logIterator = $this->logIteratorFactory->getLogIterator(Watch::ALL_LEVELS);
$logIterator = new \LimitIterator($logIterator, 0, $count);
$logItems = iterator_to_array($logIterator);
$logItems = array_reverse($logItems);
if ($raw) {
foreach ($logItems as $logItem) {
$output->writeln(json_encode($logItem));
}
} else {
$terminal = new Terminal();
$totalWidth = $terminal->getWidth();
// 8 level, 18 for app, 26 for time, 6 for formatting
$messageWidth = $totalWidth - 8 - 18 - 26 - 6;
$tableItems = array_map(function (array $logItem) use ($messageWidth) {
return [
self::LEVELS[$logItem['level']],
wordwrap($logItem['app'], 18),
$this->formatter->formatMessage($logItem, $messageWidth) . "\n",
$logItem['time'],
];
}, $logItems);
$io->table([
'Level',
'App',
'Message',
'Time',
], $tableItems);
}
if ($input->getOption('follow')) {
$watch = new Watch($this->formatter, $this->logIteratorFactory);
$watch->watch($raw, $output);
}
return 0;
}
}
@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.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\Command;
use OC\Core\Command\Base;
use OC\Core\Command\InterruptedException;
use OCA\LogReader\Log\Formatter;
use OCA\LogReader\Log\LogIteratorFactory;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Terminal;
class Watch extends Base {
public const LEVELS = ['Debug', 'Info', 'Warning', 'Error', 'Fatal'];
public const ALL_LEVELS = [0, 1, 2, 3, 4];
private $formatter;
private $logIteratorFactory;
public function __construct(Formatter $formatter, LogIteratorFactory $logIteratorFactory) {
parent::__construct();
$this->formatter = $formatter;
$this->logIteratorFactory = $logIteratorFactory;
}
protected function configure() {
$this
->setName('log:watch')
->setDescription('Watch the nextcloud logfile')
->addOption('raw', 'r', InputOption::VALUE_NONE, 'Output raw log json instead of formatted log item');
parent::configure();
}
private function getLastLogId() {
$logIterator = $this->logIteratorFactory->getLogIterator(self::ALL_LEVELS);
$logIterator->next();
if ($logIterator->current() !== null) {
return $logIterator->current()['reqId'];
}
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$raw = $input->getOption('raw');
return $this->watch($raw, $output);
}
public function watch(bool $raw, OutputInterface $output): int {
$terminal = new Terminal();
$totalWidth = $terminal->getWidth();
// 8 level, 18 for app, 26 for time, 6 for formatting
$messageWidth = $totalWidth - 8 - 18 - 26 - 6;
$lastId = $this->getLastLogId();
while (true) {
usleep(100 * 1000);
try {
$this->abortIfInterrupted();
} catch (InterruptedException $e) {
break;
}
$id = $this->getLastLogId();
if ($id !== $lastId) {
$iterator = $this->logIteratorFactory->getLogIterator(self::ALL_LEVELS);
$iterator->next();
$lines = [];
while ($iterator->valid() && count($lines) < 10) {
$line = $iterator->current();
if ($line['reqId'] === $lastId) {
break;
}
if (!is_null($line)) {
$lines[] = $line;
}
$iterator->next();
}
array_reverse($lines);
foreach ($lines as $line) {
if ($raw) {
$output->writeln(json_encode($line));
} else {
$this->printItem($line, $output, $messageWidth);
$output->writeln("");
}
}
$lastId = $id;
}
}
return 0;
}
private function printItem(array $logItem, OutputInterface $output, int $messageWidth) {
$widths = [8, 18, $messageWidth, 26];
$parts = [
self::LEVELS[$logItem['level']],
wordwrap($logItem['app'], 18),
$this->formatter->formatMessage($logItem, $messageWidth),
$logItem['time'],
];
$partLines = array_map(function ($part) {
return explode("\n", $part);
}, $parts);
$lineCount = array_reduce($partLines, function (int $count, array $lines) {
return max($count, count($lines));
}, 0);
$partLines = array_map(function (array $lines) use ($lineCount) {
return array_pad($lines, $lineCount, '');
}, $partLines);
$partLines = array_map(function (array $lines, int $width) {
return array_map(function (string $line) use ($width) {
return str_pad($line, $width);
}, $lines);
}, $partLines, $widths);
$lines = array_map(function (int $lineNumber) use ($partLines) {
$partsForLine = array_map(function (array $lines) use ($lineNumber) {
return $lines[$lineNumber];
}, $partLines);
return implode(' ', $partsForLine);
}, range(0, $lineCount - 1));
foreach ($lines as $line) {
$output->writeln(' ' . $line);
}
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Nextcloud GmbH
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0-or-later
*
* 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;
// !! Keep in sync with src/constants.ts
class Constants {
/**
* Used AppConfig Keys
*/
public const CONFIG_KEY_SHOWNLEVELS = 'shownLevels';
public const CONFIG_KEY_DATETIMEFORMAT = 'dateTimeFormat';
public const CONFIG_KEY_RELATIVEDATES = 'relativedates';
public const CONFIG_KEY_LIVELOG = 'liveLog';
public const CONFIG_KEYS = [
self::CONFIG_KEY_SHOWNLEVELS,
self::CONFIG_KEY_DATETIMEFORMAT,
self::CONFIG_KEY_RELATIVEDATES,
self::CONFIG_KEY_LIVELOG
];
public const LOGGING_LEVELS = [0, 1, 2, 3, 4];
public const LOGGING_LEVEL_NAMES = [
'debug',
'info',
'warn',
'error',
'fatal',
];
}
@@ -0,0 +1,156 @@
<?php
/**
* @author Robin Appelman <icewind@owncloud.com>
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\LogReader\Controller;
use OCA\LogReader\Log\LogIteratorFactory;
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;
/**
* Class LogController
*
* @package OCA\LogReader\Controller
*/
class LogController extends Controller {
public function __construct($appName,
IRequest $request,
private LogIteratorFactory $logIteratorFactory,
private SettingsService $settingsService,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
/**
* @AuthorizedAdminSetting(settings=OCA\LogReader\Settings\Admin)
* @param string $query
* @param int $count
* @param int $offset
* @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 !== '') {
$iterator = new \LimitIterator($iterator, 0, 100000); // limit the number of message we search to avoid huge search times
$iterator->rewind();
$iterator = new SearchFilter($iterator, $query);
$iterator->rewind();
return $this->responseFromIterator($iterator, $count, $offset);
}
return $this->responseFromIterator($iterator, $count, $offset);
}
/**
* @brief Gets the last item in the log, bypassing any cache.
* @return mixed
*/
private function getLastItem() {
$iterator = $this->logIteratorFactory->getLogIterator($this->settingsService->getShownLevels());
$iterator->next();
return $iterator->current();
}
/**
* @AuthorizedAdminSetting(settings=OCA\LogReader\Settings\Admin)
* @brief Use to poll for new log messages since $lastReqId.
*
* @note There is a possible race condition, when the user loads the
* logging page when a request isn't finished and this specific request
* is the last request in the log, then new messages of this request
* won't be polled. This is because there is no reliable way to identify
* a log message, so we have to use the reqid:
* - the key of the iterator will change when a new message is saved
* - a combination of reqid and counting the messages for that specific reqid
* will work in some cases but not when there are more than 50 messages of that
* 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([]);
}
$iterator = $this->logIteratorFactory->getLogIterator($this->settingsService->getShownLevels());
$iterator->next();
$data = [];
while ($iterator->valid()) {
$line = $iterator->current();
if ($line['reqId'] === $lastReqId) {
break;
}
if (!is_null($line)) {
$line['id'] = uniqid();
$data[] = $line;
}
$iterator->next();
}
return new JSONResponse($data);
}
protected function responseFromIterator(\Iterator $iterator, $count, $offset): JSONResponse {
$iterator->rewind();
for ($i = 0; $i < $offset; $i++) {
$iterator->next();
}
$data = [];
for ($i = 0; $i < $count && $iterator->valid(); $i++) {
$line = $iterator->current();
if (!is_null($line)) {
$line["id"] = uniqid();
$data[] = $line;
}
$iterator->next();
}
return new JSONResponse([
'data' => $data,
'remain' => $iterator->valid()
]);
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @author Robin Appelman <icewind@owncloud.com>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\LogReader\Controller;
use OCA\LogReader\Service\SettingsService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\Util;
/**
* Class PageController
*
* @package OCA\LogReader\Controller
*/
class PageController extends Controller {
public function __construct(
private IInitialState $initialState,
private SettingsService $settingsService,
) {
}
/**
* @NoCSRFRequired
*
* @return TemplateResponse
*/
public function index() {
Util::addScript($this->appName, 'logreader-main');
Util::addStyle($this->appName, 'logreader-main');
$this->initialState->provideInitialState('settings', $this->settingsService->getAppSettings());
return new TemplateResponse($this->appName, 'index');
}
}
@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Nextcloud GmbH
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0-or-later
*
* 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\Controller;
use OCA\LogReader\Constants;
use OCA\LogReader\Service\SettingsService;
use OCP\AppFramework\ApiController;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IConfig;
use OCP\IRequest;
use Psr\Log\LoggerInterface;
class SettingsController extends ApiController {
public function __construct(
string $appName,
IRequest $request,
private SettingsService $settingsService,
private IConfig $config,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}
/**
* Get the current app config
*
* @AuthorizedAdminSetting(settings=OCA\LogReader\Settings\Admin)
*/
public function getAppConfig(): JSONResponse {
return new JSONResponse($this->settingsService->getAppSettings());
}
/**
* Update values on the app config.
*
* @param string $settingsKey AppConfig Key to store
* @param mixed $settingsValues Corresponding AppConfig Value
*
* @AuthorizedAdminSetting(settings=OCA\LogReader\Settings\Admin)
*/
public function updateAppConfig(string $settingsKey, $settingsValue): JSONResponse {
$this->logger->debug('Updating AppConfig: {settingsKey} => {settingsValue}', [
'settingsKey' => $settingsKey,
'settingsValue' => $settingsValue
]);
// Check for allowed keys
if (!in_array($settingsKey, Constants::CONFIG_KEYS)) {
$this->logger->debug('Unknown appConfig key: ' . $settingsKey);
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
}
// Check type of value
if (gettype($settingsValue) !== gettype($this->settingsService->getAppSettings()[$settingsKey])) {
// Invalid type
$this->logger->debug('Incorrect value type for appConfig key', ['settingsKey' => $settingsKey, "valueType" => gettype($settingsValue)]);
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
}
if ($settingsKey === Constants::CONFIG_KEY_SHOWNLEVELS) {
foreach ($settingsValue as $value) {
if (!is_integer($value) || !in_array($value, Constants::LOGGING_LEVELS)) {
$this->logger->debug('Invalid logging level given', ['value' => $value ]);
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
}
}
}
// Set on DB
$this->config->setAppValue($this->appName, $settingsKey, json_encode($settingsValue));
return new JSONResponse();
}
}
@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.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\Listener;
use OC\SystemConfig;
use OCA\LogReader\Log\Console;
use OCA\LogReader\Log\Formatter;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Log\BeforeMessageLoggedEvent;
use Symfony\Component\Console\Terminal;
/**
* @template-implements IEventListener<BeforeMessageLoggedEvent>
*/
class LogListener implements IEventListener {
private ?Console $console;
public function __construct(Formatter $formatter, SystemConfig $config) {
if (defined('OC_CONSOLE') && \OC_CONSOLE) {
$level = getenv('OCC_LOG');
if ($level) {
$terminal = new Terminal();
$this->console = new Console($formatter, $config, $level, $terminal->getWidth());
} else {
$this->console = null;
}
} else {
$this->console = null;
}
}
public function handle(Event $event): void {
if (!$event instanceof BeforeMessageLoggedEvent) {
return;
}
if ($this->console) {
$this->console->log($event->getLevel(), $event->getApp(), $event->getMessage());
}
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <robin@icewind.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\Log;
use OC\Log\LogDetails;
use OC\SystemConfig;
use OCA\LogReader\Command\Tail;
/**
* Utility to write log messages to the console as they are emitted
*/
class Console extends LogDetails {
private int $level;
private int $terminalWidth;
private Formatter $formatter;
public function __construct(Formatter $formatter, SystemConfig $config, string $level, int $terminalWidth) {
parent::__construct($config);
$this->formatter = $formatter;
$this->level = self::parseLogLevel($level);
$this->terminalWidth = $terminalWidth;
}
public function log(int $level, string $app, array $entry) {
if ($level >= $this->level) {
$messageWidth = $this->terminalWidth - 8 - 18 - 6;
$entry = $this->logDetails($app, $entry, $level);
$lines = explode("\n", $this->formatter->formatMessage($entry, $messageWidth));
$lines[0] = str_pad(Tail::LEVELS[$level], 8) . ' ' .
str_pad(wordwrap($app, 18), 18) . ' ' .
str_pad($lines[0], $messageWidth);
for ($i = 1; $i < count($lines); $i++) {
$lines[$i] = str_repeat(' ', 8 + 18 + 2) . $lines[$i];
}
foreach ($lines as $line) {
fwrite(STDERR, $line . "\n");
}
fwrite(STDERR, "\n");
}
}
private static function parseLogLevel(string $level): int {
if (is_numeric($level)) {
return (int)$level;
}
switch (strtoupper($level)) {
case "DEBUG":
return 0;
case "INFO":
return 1;
case "WARN":
return 2;
case "ERROR":
return 3;
case "FATAL":
return 4;
default:
throw new \Exception("Unknown log level $level");
}
}
}
@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.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\Log;
class Formatter {
private $root;
public function __construct(string $root) {
$this->root = $root;
}
public function formatMessage($logItem, int $width) {
if (isset($logItem['exception'])) {
return $this->formatException($logItem['exception'], $width);
}
if (is_array($logItem['message']) && isset($logItem['message']['Exception'])) {
return $this->formatException($logItem['message'], $width);
}
return wordwrap($logItem['message'], $width);
}
private function formatException(array $e, int $width) {
$largestIndex = count($e['Trace']) - 1;
$largestIndexWidth = strlen((string)$largestIndex);
$message = wordwrap($e['Exception'] . ': ' . $e['Message'] . ' at ' . $this->getFileAndLine($e), $width) . "\n\n";
$message .= implode("\n", array_map(function ($index, $trace) use ($largestIndexWidth, $width) {
return $this->formatTraceLine($index, $trace, $largestIndexWidth, $width);
}, array_keys($e['Trace']), array_values($e['Trace'])));
if (isset($e['Previous'])) {
$message .= "\n\n" . 'Caused by ' . $this->formatException($e['Previous'], $width);
}
return $message;
}
private function formatTraceLine(int $index, array $trace, int $largestIndexWidth, int $width): string {
$whiteSpace = str_repeat(' ', $largestIndexWidth - strlen((string)$index));
$method = ($trace['class'] ?? '') . ($trace['type'] ?? '') . $trace['function'];
$argumentWidth = $width - $largestIndexWidth - strlen($method) - 5;
$arguments = array_map(function ($arg) use ($argumentWidth) {
$base = str_replace("\n", '', $this->formatArgument($arg, 0));
$showInline = strlen($base) < $argumentWidth;
return $showInline ? $base : substr($base, 0, $argumentWidth - 8) . ' ... ' . substr($base, -2);
}, $trace['args'] ?? []);
$argumentsString = implode(', ', $arguments);
$argumentWhiteSpace = str_repeat(' ', $largestIndexWidth + 2);
if ($argumentsString && strlen($argumentsString) < $argumentWidth) {
return $whiteSpace . $index . '. ' . $this->getFileAndLine($trace, $argumentWidth) . "\n" .
$argumentWhiteSpace . $method . '(' .
$argumentsString . ')';
} else {
return $whiteSpace . $index . '. ' . $this->getFileAndLine($trace, $argumentWidth) . "\n" .
$argumentWhiteSpace . $method . "(\n" .
implode(",\n", array_map(function ($argumentLine) use ($argumentWhiteSpace) {
return $argumentWhiteSpace . ' ' . trim($argumentLine);
}, $arguments)) . "\n" .
$argumentWhiteSpace . ")";
}
}
private function formatArgument($argument, int $whiteSpace, int $depth = 0, bool $forceObject = false): string {
$leadingSpace = str_repeat(' ', $whiteSpace * $depth);
$glue = ($whiteSpace) ? ",\n" : ',';
if (is_array($argument) && isset($argument['__class__'])) {
$className = $argument['__class__'];
unset($argument['__class__']);
return $leadingSpace . $className . ' ' . trim($this->formatArgument($argument, $whiteSpace, $depth, true));
} elseif (is_array($argument)) {
if (count($argument) === 0) {
return $leadingSpace . ($forceObject ? '{}' : '[]');
}
$isObject = $forceObject || array_keys($argument) !== range(0, count($argument) - 1);
if ($isObject) {
$keyWhitespace = str_repeat(' ', $whiteSpace * ($depth + 1));
return $leadingSpace . "{\n" .
implode($glue, array_map(function ($key, $value) use ($whiteSpace, $depth, $keyWhitespace) {
return $keyWhitespace . $key . ':' . trim($this->formatArgument($value, $whiteSpace, $depth + 1));
}, array_keys($argument), array_values($argument))) . ($whiteSpace ? "\n" : '') . $leadingSpace . '}';
} else {
return $leadingSpace . "[\n" .
implode($glue, array_map(function ($value) use ($whiteSpace, $depth) {
return $this->formatArgument($value, $whiteSpace, $depth + 1);
}, $argument)) . ($whiteSpace ? "\n" : '') . $leadingSpace . ']';
}
} else {
$value = json_encode($argument, $whiteSpace ? JSON_PRETTY_PRINT : 0);
return $leadingSpace . $value;
}
}
private function getFileAndLine(array $item, int $width = 64): string {
$file = $item['file'] ?? $item['File'] ?? null;
$line = $item['line'] ?? $item['Line'] ?? null;
if ($file && $line) {
if (substr($file, 0, strlen($this->root) + 1) === $this->root . '/') {
$file = substr($file, strlen($this->root) + 1);
}
$postFix = ' line ' . $line;
$fileWidth = $width - strlen($postFix) - 4;
$prefix = '';
$count = 0;
while (strlen($file) > $fileWidth && strpos($file, '/') !== false && $count < 20) {
$file = substr($file, strpos($file, '/') + 1);
$prefix = '.../';
$count++;
}
return $prefix . $file . $postFix;
} else {
return '<<closure>>';
}
}
}
@@ -0,0 +1,144 @@
<?php
/**
* @author Robin Appelman <icewind@owncloud.com>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\LogReader\Log;
/**
* @template-implements \Iterator<int,array>
*/
class LogIterator implements \Iterator {
/**
* @var resource
*/
private $handle;
/**
* @var int
*/
private $position = 0;
/**
* @var string
*/
private $lastLine;
/**
* @var string
*/
private $currentLine = '';
private $currentKey = -1;
/**
* @var string
*/
private $dateFormat;
private $timezone;
public const CHUNK_SIZE = 100; // how many chars do we try at once to find a new line
/**
* @param resource $handle
* @param string $dateFormat
* @param string $timezone
*/
public function __construct($handle, $dateFormat, $timezone) {
$this->handle = $handle;
$this->dateFormat = $dateFormat;
$this->timezone = new \DateTimeZone($timezone);
$this->rewind();
$this->next();
}
public function rewind(): void {
fseek($this->handle, 0, SEEK_END);
$this->position = ftell($this->handle) - self::CHUNK_SIZE;
$this->currentKey = 0;
}
/**
* @return array
*/
#[\ReturnTypeWillChange]
public function current() {
$entry = json_decode($this->lastLine, true);
if ($this->dateFormat !== \DateTime::ATOM) {
if (isset($entry['time'])) {
$time = \DateTime::createFromFormat($this->dateFormat, $entry['time'], $this->timezone);
if ($time) {
$entry['time'] = $time->format(\DateTime::ATOM);
}
}
}
return $entry;
}
public function key(): int {
return $this->currentKey;
}
public function next(): void {
$this->currentLine = '';
// Loop through each character of the file looking for new lines
while ($this->position > 0) {
fseek($this->handle, $this->position);
$chars = fread($this->handle, self::CHUNK_SIZE);
$newlinePos = strrpos($chars, "\n");
if ($newlinePos !== false) {
$this->currentLine = substr($chars, $newlinePos + 1) . $this->currentLine;
$this->lastLine = $this->currentLine;
$this->currentKey++;
$this->position -= (self::CHUNK_SIZE - $newlinePos);
return;
} else {
$this->currentLine = $chars . $this->currentLine;
if ($this->position >= self::CHUNK_SIZE) {
$this->position -= self::CHUNK_SIZE;
} else {
$remaining = $this->position;
fseek($this->handle, 0);
$chars = fread($this->handle, $remaining);
$this->currentLine = $chars . $this->currentLine;
$this->lastLine = $this->currentLine;
$this->position = 0;
}
}
}
}
public function valid(): bool {
if (!is_resource($this->handle)) {
return false;
}
if ($this->position > 0) {
return true;
}
if ($this->currentLine === '') {
return false;
}
return true;
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.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\Log;
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;
}
/**
* @return \Iterator
* @param int[] $levels Array of levels to show
* @throws \Exception
*/
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);
});
} else {
throw new \Exception("Error while opening " . $log->getLogFilePath());
}
}
throw new \Exception('Can\'t find log class');
}
}
@@ -0,0 +1,71 @@
<?php
/**
* @author Robin Appelman <icewind@owncloud.com>
*
* @copyright Copyright (c) 2015, ownCloud, Inc.
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\LogReader\Log;
/**
* @template-extends \FilterIterator<int,array,\Iterator<int,array>>
*/
class SearchFilter extends \FilterIterator {
/**
* @var string
*/
private $query;
/**
* @var string[]
*/
private $levels;
public function __construct(\Iterator $iterator, string $query) {
parent::__construct($iterator);
$this->rewind();
$this->query = strtolower($query);
$this->levels = ['Debug', 'Info', 'Warning', 'Error', 'Fatal'];
}
private function formatLevel(int $level): string {
return isset($this->levels[$level]) ? $this->levels[$level] : 'Unknown';
}
public function accept(): bool {
if (!$this->query) {
return true;
}
$value = $this->current();
return $this->inMessage($value['message'] ?? '', $this->query)
|| stripos($value['app'] ?? '', $this->query) !== false
|| stripos($value['reqId'] ?? '', $this->query) !== false
|| stripos($value['user'] ?? '', $this->query) !== false
|| stripos($value['url'] ?? '', $this->query) !== false
|| stripos($this->formatLevel($value['level'] ?? -1), $this->query) !== false;
}
private function inMessage($message, string $query): bool {
if (is_string($message)) {
return stripos($message, $query) !== false;
} elseif (isset($message['Exception'])) {
return stripos($message['Exception'], $query) !== false
|| stripos($message['Message'] ?? '', $query) !== false;
}
return false;
}
}
@@ -0,0 +1,103 @@
<?php
/**
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @copyright Copyright (c) 2023, Nextcloud GmbH
* @license AGPL-3.0-or-later
*
* 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\Service;
use OCA\LogReader\Constants;
use OCP\IConfig;
class SettingsService {
public function __construct(
private IConfig $config,
) {
$this->config = $config;
}
/**
* Load shown levels from app config
*/
public function getShownLevels(): array {
return json_decode($this->config->getAppValue('logreader', Constants::CONFIG_KEY_SHOWNLEVELS, '[0,1,2,3,4]'), flags: JSON_THROW_ON_ERROR);
}
/**
* Load date time format to use for user from app config
*/
public function getDateTimeFormat(): string {
return json_decode($this->config->getAppValue('logreader', Constants::CONFIG_KEY_DATETIMEFORMAT, '"local"'), flags: JSON_THROW_ON_ERROR);
}
/**
* Load app config if dates should be displayed as relative dates
*/
public function getRelativeDates(): bool {
return json_decode($this->config->getAppValue('logreader', Constants::CONFIG_KEY_RELATIVEDATES, 'false') ?: 'false', flags: JSON_THROW_ON_ERROR);
}
/**
* Load app config if log should be updated automatically
*/
public function getLiveLog(): bool {
return json_decode($this->config->getAppValue('logreader', Constants::CONFIG_KEY_LIVELOG, 'true'), flags: JSON_THROW_ON_ERROR);
}
/**
* Get all app settings for displaying the logfiles
*/
public function getAppSettings(): array {
return [
Constants::CONFIG_KEY_SHOWNLEVELS => $this->getShownLevels(),
Constants::CONFIG_KEY_DATETIMEFORMAT => $this->getDateTimeFormat(),
Constants::CONFIG_KEY_RELATIVEDATES => $this->getRelativeDates(),
Constants::CONFIG_KEY_LIVELOG => $this->getLiveLog(),
'enabled' => $this->getLoggingType() === 'file',
];
}
/**
* Get system setting of the logging type
*/
public function getLoggingType(): string {
return $this->config->getSystemValueString('log_type', 'file');
}
/**
* Get system setting of the log file name
*/
public function getLoggingFile(): string {
return $this->config->getSystemValueString('logile', '');
}
/**
* Get system setting for the log date format
*/
public function getLoggingDateFormat(): string {
// see default: https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/logging_configuration.html#file
return $this->config->getSystemValueString('logdateformat', 'c');
}
/**
* Get system setting for the log timezone
*/
public function getLoggingTimezone(): string {
return $this->config->getSystemValueString('logtimezone', 'UTC');
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.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\Settings;
use OCA\LogReader\Service\SettingsService;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\Settings\IDelegatedSettings;
use OCP\Util;
class Admin implements IDelegatedSettings {
public function __construct(
private string $appName,
private IInitialState $initialState,
private SettingsService $settingsService,
) {
}
/**
* @return TemplateResponse
*/
public function getForm() {
Util::addScript($this->appName, 'logreader-main');
Util::addStyle($this->appName, 'logreader-main');
$this->initialState->provideInitialState('settings', $this->settingsService->getAppSettings());
return new TemplateResponse($this->appName, 'index');
}
/**
* @return string the section ID, e.g. 'sharing'
*/
public function getSection() {
return 'logging';
}
/**
* @return int whether the form should be rather on the top or bottom of
* the admin section. The forms are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
*/
public function getPriority() {
return 90;
}
public function getName(): ?string {
return null;
}
public function getAuthorizedAppConfig(): array {
return [];
}
}
@@ -0,0 +1,75 @@
<?php
/**
* @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.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\Settings;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class Section implements IIconSection {
public function __construct(
private IL10N $l,
private IURLGenerator $url,
) {
$this->l = $l;
$this->url = $url;
}
/**
* returns the ID of the section. It is supposed to be a lower case string,
* e.g. 'ldap'
*
* @returns string
*/
public function getID() {
return 'logging';
}
/**
* returns the translated name as it should be displayed, e.g. 'LDAP / AD
* integration'. Use the L10N service to translate it.
*
* @return string
*/
public function getName() {
return $this->l->t('Logging');
}
/**
* @return int whether the form should be rather on the top or bottom of
* the settings navigation. The sections are arranged in ascending order of
* the priority values. It is required to return a value between 0 and 99.
*
* E.g.: 70
*/
public function getPriority() {
return 90;
}
/**
* {@inheritdoc}
*/
public function getIcon() {
return $this->url->imagePath('logreader', 'app-dark.svg');
}
}
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Côme Chilliet <come.chilliet@nextcloud.com>
*
* @author Côme Chilliet <come.chilliet@nextcloud.com>
*
* @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\SetupChecks;
use OCA\LogReader\Log\LogIteratorFactory;
use OCP\IConfig;
use OCP\IDateTimeFormatter;
use OCP\IL10N;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
class LogErrors implements ISetupCheck {
private const LEVEL_WARNING = 2;
private const LEVEL_ERROR = 3;
private const LEVEL_FATAL = 4;
public function __construct(
private IL10N $l10n,
private IConfig $config,
private IDateTimeFormatter $dateFormatter,
private LogIteratorFactory $logIteratorFactory,
) {
}
public function getName(): string {
return $this->l10n->t('Errors in the log');
}
public function getCategory(): string {
return 'system';
}
public function run(): SetupResult {
$logIterator = $this->logIteratorFactory->getLogIterator([self::LEVEL_WARNING,self::LEVEL_ERROR,self::LEVEL_FATAL]);
$count = [
self::LEVEL_WARNING => 0,
self::LEVEL_ERROR => 0,
self::LEVEL_FATAL => 0,
];
$limit = new \DateTime('7 days ago');
foreach ($logIterator as $logItem) {
if (!isset($logItem['time'])) {
continue;
}
$time = \DateTime::createFromFormat(\DateTime::ATOM, $logItem['time']);
if ($time < $limit) {
break;
}
$count[$logItem['level']]++;
}
if (array_sum($count) === 0) {
return SetupResult::success($this->l10n->t('No errors in the logs since %s', $this->dateFormatter->formatDate($limit)));
} elseif ($count[self::LEVEL_ERROR] + $count[self::LEVEL_FATAL] > 0) {
return SetupResult::warning(
$this->l10n->n(
'%n error in the logs since %s',
'%n errors in the logs since %s',
$count[self::LEVEL_ERROR] + $count[self::LEVEL_FATAL],
[$this->dateFormatter->formatDate($limit)],
)
);
} else {
return SetupResult::info(
$this->l10n->n(
'%n warning in the logs since %s',
'%n warnings in the logs since %s'.json_encode($count),
$count[self::LEVEL_WARNING],
[$this->dateFormatter->formatDate($limit)],
)
);
}
}
}