Move log pages to Vue components.
This commit is contained in:
parent
7084860515
commit
852d2e4de1
|
@ -268,7 +268,7 @@ return function (App\Event\BuildStationMenu $e) {
|
|||
'help' => [
|
||||
'label' => __('Help'),
|
||||
'icon' => 'support',
|
||||
'url' => (string)$router->fromHere('stations:logs:index'),
|
||||
'url' => (string)$router->fromHere('stations:help'),
|
||||
'permission' => StationPermissions::Logs,
|
||||
],
|
||||
]
|
||||
|
|
|
@ -91,17 +91,9 @@ return static function (RouteCollectorProxy $app) {
|
|||
->setName('admin:custom_fields:index')
|
||||
->add(new Middleware\Permissions(GlobalPermissions::CustomFields));
|
||||
|
||||
$group->group(
|
||||
'/logs',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('', Controller\Admin\LogsController::class)
|
||||
->setName('admin:logs:index');
|
||||
|
||||
$group->get('/view/{station_id}/{log}', Controller\Admin\LogsController::class . ':viewAction')
|
||||
->setName('admin:logs:view')
|
||||
->add(Middleware\GetStation::class);
|
||||
}
|
||||
)->add(new Middleware\Permissions(GlobalPermissions::Logs));
|
||||
$group->get('/logs', Controller\Admin\LogsAction::class)
|
||||
->setName('admin:logs:index')
|
||||
->add(new Middleware\Permissions(GlobalPermissions::Logs));
|
||||
|
||||
$group->get('/permissions', Controller\Admin\PermissionsAction::class)
|
||||
->setName('admin:permissions:index')
|
||||
|
|
|
@ -188,6 +188,17 @@ return static function (RouteCollectorProxy $group) {
|
|||
Controller\Api\Admin\Stations\StorageLocationsAction::class
|
||||
)->setName('api:admin:stations:storage-locations')
|
||||
->add(new Middleware\Permissions(GlobalPermissions::Stations));
|
||||
|
||||
$group->group(
|
||||
'',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('/logs', Controller\Api\Admin\LogsAction::class)
|
||||
->setName('api:admin:logs');
|
||||
|
||||
$group->get('/log/{log}', Controller\Api\Admin\LogsAction::class)
|
||||
->setName('api:admin:log');
|
||||
}
|
||||
)->add(new Middleware\Permissions(GlobalPermissions::Logs));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -640,6 +640,17 @@ return static function (RouteCollectorProxy $group) {
|
|||
)->setName('api:stations:webhook:test-log');
|
||||
}
|
||||
)->add(new Middleware\Permissions(StationPermissions::WebHooks, true));
|
||||
|
||||
$group->group(
|
||||
'',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('/logs', Controller\Api\Stations\LogsAction::class)
|
||||
->setName('api:stations:logs');
|
||||
|
||||
$group->get('/log/{log}', Controller\Api\Stations\LogsAction::class)
|
||||
->setName('api:stations:log');
|
||||
}
|
||||
)->add(new Middleware\Permissions(StationPermissions::Logs, true));
|
||||
}
|
||||
)->add(Middleware\RequireStation::class)
|
||||
->add(Middleware\GetStation::class);
|
||||
|
|
|
@ -44,16 +44,9 @@ return static function (RouteCollectorProxy $app) {
|
|||
->setName('stations:stereo_tool_config')
|
||||
->add(new Middleware\Permissions(StationPermissions::Broadcasting, true));
|
||||
|
||||
$group->group(
|
||||
'/logs',
|
||||
function (RouteCollectorProxy $group) {
|
||||
$group->get('', Controller\Stations\LogsController::class)
|
||||
->setName('stations:logs:index');
|
||||
|
||||
$group->get('/view/{log}', Controller\Stations\LogsController::class . ':viewAction')
|
||||
->setName('stations:logs:view');
|
||||
}
|
||||
)->add(new Middleware\Permissions(StationPermissions::Logs, true));
|
||||
$group->get('/help', Controller\Stations\HelpAction::class)
|
||||
->setName('stations:help')
|
||||
->add(new Middleware\Permissions(StationPermissions::Logs, true));
|
||||
|
||||
$group->get('/playlists', Controller\Stations\PlaylistsAction::class)
|
||||
->setName('stations:playlists:index')
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title">
|
||||
<translate key="hdr_system_logs">System Logs</translate>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<log-list :url="systemLogsUrl" @view="viewLog"></log-list>
|
||||
</div>
|
||||
|
||||
<div class="card" v-if="stationLogs.length > 0">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title">
|
||||
<translate key="hdr_logs_by_station">Logs by Station</translate>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<b-tabs pills lazy nav-class="card-header-pills" nav-wrapper-class="card-header">
|
||||
<b-tab v-for="row in stationLogs" :key="row.id" :title="row.name">
|
||||
<log-list :url="row.url" @view="viewLog"></log-list>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</div>
|
||||
|
||||
<streaming-log-modal ref="modal"></streaming-log-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LogList from "~/components/Common/LogList";
|
||||
import StreamingLogModal from "~/components/Common/StreamingLogModal";
|
||||
|
||||
export default {
|
||||
name: 'AdminLogs',
|
||||
components: {StreamingLogModal, LogList},
|
||||
props: {
|
||||
systemLogsUrl: String,
|
||||
stationLogs: Array
|
||||
},
|
||||
methods: {
|
||||
viewLog(url) {
|
||||
this.$refs.modal.show(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
|
||||
</template>
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<div class="list-group list-group-flush">
|
||||
<a v-for="log in logs" :key="log.key" class="list-group-item list-group-item-action log-item"
|
||||
href="#" @click.prevent="viewLog(log.links.self)">
|
||||
<span class="log-name">{{ log.name }}</span><br>
|
||||
<small class="text-secondary">{{ log.path }}</small>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LogList',
|
||||
emits: ['view'],
|
||||
props: {
|
||||
url: String,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
logs: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.relist();
|
||||
},
|
||||
methods: {
|
||||
relist() {
|
||||
this.loading = true;
|
||||
this.$wrapWithLoading(
|
||||
this.axios.get(this.url)
|
||||
).then((resp) => {
|
||||
this.logs = resp.data.logs;
|
||||
}).finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
viewLog(url) {
|
||||
this.$emit('view', url);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -22,7 +22,7 @@ export default {
|
|||
components: {StreamingLogView},
|
||||
data() {
|
||||
return {
|
||||
logUrl: null,
|
||||
logUrl: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -1,14 +1,23 @@
|
|||
<template>
|
||||
<b-overlay variant="card" :show="loading">
|
||||
<textarea class="form-control log-viewer" id="log-view-contents" spellcheck="false"
|
||||
<b-form-group label-for="modal_scroll_to_bottom">
|
||||
<b-form-checkbox id="modal_scroll_to_bottom" v-model="scrollToBottom">
|
||||
<translate key="scroll_to_bottom">Automatically Scroll to Bottom</translate>
|
||||
</b-form-checkbox>
|
||||
</b-form-group>
|
||||
|
||||
<textarea class="form-control log-viewer" ref="textarea" id="log-view-contents" spellcheck="false"
|
||||
readonly>{{ logs }}</textarea>
|
||||
</b-overlay>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import BWrappedFormCheckbox from "~/components/Form/BWrappedFormCheckbox";
|
||||
|
||||
export default {
|
||||
name: 'StreamingLogView',
|
||||
components: {BWrappedFormCheckbox},
|
||||
props: {
|
||||
logUrl: {
|
||||
type: String,
|
||||
|
@ -21,6 +30,7 @@ export default {
|
|||
logs: '',
|
||||
currentLogPosition: null,
|
||||
timeoutUpdateLog: null,
|
||||
scrollToBottom: true,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -32,6 +42,7 @@ export default {
|
|||
}).then((resp) => {
|
||||
if (resp.data.contents !== '') {
|
||||
this.logs = resp.data.contents + "\n";
|
||||
this.scrollTextarea();
|
||||
} else {
|
||||
this.logs = '';
|
||||
}
|
||||
|
@ -59,6 +70,7 @@ export default {
|
|||
}).then((resp) => {
|
||||
if (resp.data.contents !== '') {
|
||||
this.logs = this.logs + resp.data.contents + "\n";
|
||||
this.scrollTextarea();
|
||||
}
|
||||
this.currentLogPosition = resp.data.position;
|
||||
|
||||
|
@ -70,6 +82,14 @@ export default {
|
|||
getContents() {
|
||||
return this.logs;
|
||||
},
|
||||
scrollTextarea() {
|
||||
if (this.scrollToBottom) {
|
||||
this.$nextTick(() => {
|
||||
const textarea = this.$refs.textarea;
|
||||
textarea.scrollTop = textarea.scrollHeight;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title">
|
||||
<translate key="hdr_logs">Available Logs</translate>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<log-list :url="logsUrl" @view="viewLog"></log-list>
|
||||
</div>
|
||||
|
||||
<streaming-log-modal ref="modal"></streaming-log-modal>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title">
|
||||
<translate key="hdr_need_help">Need Help?</translate>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
<translate key="help_section_1">You can find answers for many common questions in our support documents.</translate>
|
||||
</p>
|
||||
<p class="card-text">
|
||||
<a href="https://docs.azuracast.com/en/user-guide/troubleshooting" target="_blank">
|
||||
<translate key="help_link_support_docs">Support Documents</translate>
|
||||
</a>
|
||||
</p>
|
||||
<p class="card-text">
|
||||
<translate key="help_section_2">If you're experiencing a bug or error, you can submit a GitHub issue using the link below.</translate>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-outline-primary" role="button"
|
||||
href="https://github.com/AzuraCast/AzuraCast/issues/new/choose" target="_blank">
|
||||
<icon icon="contact_support"></icon>
|
||||
<translate key="btn_add_github_issue">Add New GitHub Issue</translate>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from "~/components/Common/Icon";
|
||||
import StreamingLogModal from "~/components/Common/StreamingLogModal";
|
||||
import LogList from "~/components/Common/LogList";
|
||||
|
||||
export default {
|
||||
name: 'StationsHelp',
|
||||
components: {LogList, StreamingLogModal, Icon},
|
||||
props: {
|
||||
logsUrl: String,
|
||||
},
|
||||
methods: {
|
||||
viewLog(url) {
|
||||
this.$refs.modal.show(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,7 @@
|
|||
import initBase from '~/base.js';
|
||||
|
||||
import '~/vendor/bootstrapVue.js';
|
||||
|
||||
import AdminLogs from '~/components/Admin/Logs.vue';
|
||||
|
||||
export default initBase(AdminLogs);
|
|
@ -0,0 +1,7 @@
|
|||
import initBase from '~/base.js';
|
||||
|
||||
import '~/vendor/bootstrapVue.js';
|
||||
|
||||
import Help from '~/components/Stations/Help.vue';
|
||||
|
||||
export default initBase(Help);
|
|
@ -15,6 +15,7 @@ module.exports = {
|
|||
AdminCustomFields: '~/pages/Admin/CustomFields.js',
|
||||
AdminGeoLite: '~/pages/Admin/GeoLite.js',
|
||||
AdminIndex: '~/pages/Admin/Index.js',
|
||||
AdminLogs: '~/pages/Admin/Logs.js',
|
||||
AdminPermissions: '~/pages/Admin/Permissions.js',
|
||||
AdminSettings: '~/pages/Admin/Settings.js',
|
||||
AdminShoutcast: '~/pages/Admin/Shoutcast.js',
|
||||
|
@ -35,6 +36,7 @@ module.exports = {
|
|||
SetupStation: '~/pages/Setup/Station.js',
|
||||
StationsBulkMedia: '~/pages/Stations/BulkMedia.js',
|
||||
StationsFallback: '~/pages/Stations/Fallback.js',
|
||||
StationsHelp: '~/pages/Stations/Help.js',
|
||||
StationsHlsStreams: '~/pages/Stations/HlsStreams.js',
|
||||
StationsLiquidsoapConfig: '~/pages/Stations/LiquidsoapConfig.js',
|
||||
StationsMedia: '~/pages/Stations/Media.js',
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
|
||||
use App\Entity\Repository\StationRepository;
|
||||
use App\Enums\StationPermissions;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class LogsAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly StationRepository $stationRepo
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response
|
||||
): ResponseInterface {
|
||||
$router = $request->getRouter();
|
||||
|
||||
$acl = $request->getAcl();
|
||||
$stationLogs = [];
|
||||
foreach ($this->stationRepo->iterateEnabledStations() as $station) {
|
||||
if ($acl->isAllowed(StationPermissions::Logs, $station)) {
|
||||
$stationLogs[] = [
|
||||
'id' => $station->getIdRequired(),
|
||||
'name' => $station->getName(),
|
||||
'url' => (string)$router->named('api:stations:logs', [
|
||||
'station_id' => $station->getIdRequired(),
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $request->getView()->renderVuePage(
|
||||
response: $response,
|
||||
component: 'Vue_AdminLogs',
|
||||
id: 'admin-logs',
|
||||
title: __('System Logs'),
|
||||
props: [
|
||||
'systemLogsUrl' => (string)$router->fromHere('api:admin:logs'),
|
||||
'stationLogs' => $stationLogs,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,47 +2,64 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Admin;
|
||||
namespace App\Controller\Api\Admin;
|
||||
|
||||
use App\Controller\AbstractLogViewerController;
|
||||
use App\Entity;
|
||||
use App\Controller\Api\Traits\HasLogViewer;
|
||||
use App\Environment;
|
||||
use App\Exception;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class LogsController extends AbstractLogViewerController
|
||||
final class LogsAction
|
||||
{
|
||||
use HasLogViewer;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly Environment $environment
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response
|
||||
Response $response,
|
||||
?string $log = null
|
||||
): ResponseInterface {
|
||||
$stations = $this->em->getRepository(Entity\Station::class)->findAll();
|
||||
$station_logs = [];
|
||||
$logPaths = $this->getGlobalLogs();
|
||||
|
||||
foreach ($stations as $station) {
|
||||
/** @var Entity\Station $station */
|
||||
$station_logs[$station->getId()] = [
|
||||
'name' => $station->getName(),
|
||||
'logs' => $this->getStationLogs($station),
|
||||
];
|
||||
if (null === $log) {
|
||||
$router = $request->getRouter();
|
||||
return $response->withJson(
|
||||
[
|
||||
'logs' => array_map(
|
||||
function (string $key, array $row) use ($router) {
|
||||
$row['key'] = $key;
|
||||
$row['links'] = [
|
||||
'self' => (string)$router->named(
|
||||
'api:admin:log',
|
||||
[
|
||||
'log' => $key,
|
||||
]
|
||||
),
|
||||
];
|
||||
return $row;
|
||||
},
|
||||
array_keys($logPaths),
|
||||
array_values($logPaths)
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $request->getView()->renderToResponse(
|
||||
if (!isset($logPaths[$log])) {
|
||||
throw new Exception('Invalid log file specified.');
|
||||
}
|
||||
|
||||
return $this->streamLogToResponse(
|
||||
$request,
|
||||
$response,
|
||||
'admin/logs/index',
|
||||
[
|
||||
'global_logs' => $this->getGlobalLogs(),
|
||||
'station_logs' => $station_logs,
|
||||
]
|
||||
$logPaths[$log]['path'],
|
||||
$logPaths[$log]['tail'] ?? true
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -85,24 +102,4 @@ final class LogsController extends AbstractLogViewerController
|
|||
|
||||
return $logPaths;
|
||||
}
|
||||
|
||||
public function viewAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
string $station_id,
|
||||
string $log
|
||||
): ResponseInterface {
|
||||
if ('global' === $station_id) {
|
||||
$log_areas = $this->getGlobalLogs();
|
||||
} else {
|
||||
$log_areas = $this->getStationLogs($request->getStation());
|
||||
}
|
||||
|
||||
if (!isset($log_areas[$log])) {
|
||||
throw new Exception('Invalid log file specified.');
|
||||
}
|
||||
|
||||
$logArea = $log_areas[$log];
|
||||
return $this->streamLogToResponse($request, $response, $logArea['path'], $logArea['tail'] ?? true);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Api\Stations;
|
||||
|
||||
use App\Controller\Api\Traits\HasLogViewer;
|
||||
use App\Entity\Station;
|
||||
use App\Exception;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use App\Radio\Enums\BackendAdapters;
|
||||
use App\Radio\Enums\FrontendAdapters;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class LogsAction
|
||||
{
|
||||
use HasLogViewer;
|
||||
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
string $station_id,
|
||||
?string $log = null
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
$logPaths = $this->getStationLogs($station);
|
||||
|
||||
if (null === $log) {
|
||||
$router = $request->getRouter();
|
||||
return $response->withJson(
|
||||
[
|
||||
'logs' => array_map(
|
||||
function (string $key, array $row) use ($router, $station_id) {
|
||||
$row['key'] = $key;
|
||||
$row['links'] = [
|
||||
'self' => (string)$router->named(
|
||||
'api:stations:log',
|
||||
[
|
||||
'station_id' => $station_id,
|
||||
'log' => $key,
|
||||
]
|
||||
),
|
||||
];
|
||||
return $row;
|
||||
},
|
||||
array_keys($logPaths),
|
||||
array_values($logPaths)
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (!isset($logPaths[$log])) {
|
||||
throw new Exception('Invalid log file specified.');
|
||||
}
|
||||
|
||||
$frontendConfig = $station->getFrontendConfig();
|
||||
$filteredTerms = [
|
||||
$station->getAdapterApiKey(),
|
||||
$frontendConfig->getAdminPassword(),
|
||||
$frontendConfig->getRelayPassword(),
|
||||
$frontendConfig->getSourcePassword(),
|
||||
$frontendConfig->getStreamerPassword(),
|
||||
];
|
||||
|
||||
return $this->streamLogToResponse(
|
||||
$request,
|
||||
$response,
|
||||
$logPaths[$log]['path'],
|
||||
$logPaths[$log]['tail'] ?? true,
|
||||
$filteredTerms
|
||||
);
|
||||
}
|
||||
|
||||
private function getStationLogs(Station $station): array
|
||||
{
|
||||
$logPaths = [];
|
||||
$stationConfigDir = $station->getRadioConfigDir();
|
||||
|
||||
$logPaths['station_nginx'] = [
|
||||
'name' => __('Station Nginx Configuration'),
|
||||
'path' => $stationConfigDir . '/nginx.conf',
|
||||
'tail' => false,
|
||||
];
|
||||
|
||||
if (BackendAdapters::Liquidsoap === $station->getBackendTypeEnum()) {
|
||||
$logPaths['liquidsoap_log'] = [
|
||||
'name' => __('Liquidsoap Log'),
|
||||
'path' => $stationConfigDir . '/liquidsoap.log',
|
||||
'tail' => true,
|
||||
];
|
||||
$logPaths['liquidsoap_liq'] = [
|
||||
'name' => __('Liquidsoap Configuration'),
|
||||
'path' => $stationConfigDir . '/liquidsoap.liq',
|
||||
'tail' => false,
|
||||
];
|
||||
}
|
||||
|
||||
switch ($station->getFrontendTypeEnum()) {
|
||||
case FrontendAdapters::Icecast:
|
||||
$logPaths['icecast_access_log'] = [
|
||||
'name' => __('Icecast Access Log'),
|
||||
'path' => $stationConfigDir . '/icecast_access.log',
|
||||
'tail' => true,
|
||||
];
|
||||
$logPaths['icecast_error_log'] = [
|
||||
'name' => __('Icecast Error Log'),
|
||||
'path' => $stationConfigDir . '/icecast.log',
|
||||
'tail' => true,
|
||||
];
|
||||
$logPaths['icecast_xml'] = [
|
||||
'name' => __('Icecast Configuration'),
|
||||
'path' => $stationConfigDir . '/icecast.xml',
|
||||
'tail' => false,
|
||||
];
|
||||
break;
|
||||
|
||||
case FrontendAdapters::Shoutcast:
|
||||
$logPaths['shoutcast_log'] = [
|
||||
'name' => __('SHOUTcast Log'),
|
||||
'path' => $stationConfigDir . '/shoutcast.log',
|
||||
'tail' => true,
|
||||
];
|
||||
$logPaths['shoutcast_conf'] = [
|
||||
'name' => __('SHOUTcast Configuration'),
|
||||
'path' => $stationConfigDir . '/sc_serv.conf',
|
||||
'tail' => false,
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
return $logPaths;
|
||||
}
|
||||
}
|
|
@ -18,7 +18,8 @@ trait HasLogViewer
|
|||
ServerRequest $request,
|
||||
Response $response,
|
||||
string $log_path,
|
||||
bool $tail_file = true
|
||||
bool $tail_file = true,
|
||||
array $filteredTerms = []
|
||||
): ResponseInterface {
|
||||
clearstatcache();
|
||||
|
||||
|
@ -28,7 +29,10 @@ trait HasLogViewer
|
|||
|
||||
if (!$tail_file) {
|
||||
$log = file_get_contents($log_path) ?: '';
|
||||
$log_contents = $this->processLog($request, $log);
|
||||
$log_contents = $this->processLog(
|
||||
rawLog: $log,
|
||||
filteredTerms: $filteredTerms
|
||||
);
|
||||
|
||||
return $response->withJson(
|
||||
[
|
||||
|
@ -66,7 +70,12 @@ trait HasLogViewer
|
|||
$log_contents_raw = fread($fp, $log_visible_size) ?: '';
|
||||
fclose($fp);
|
||||
|
||||
$log_contents = $this->processLog($request, $log_contents_raw, $cut_first_line, true);
|
||||
$log_contents = $this->processLog(
|
||||
rawLog: $log_contents_raw,
|
||||
cutFirstLine: $cut_first_line,
|
||||
cutEmptyLastLine: true,
|
||||
filteredTerms: $filteredTerms
|
||||
);
|
||||
}
|
||||
|
||||
return $response->withJson(
|
||||
|
@ -78,11 +87,11 @@ trait HasLogViewer
|
|||
);
|
||||
}
|
||||
|
||||
protected function processLog(
|
||||
ServerRequest $request,
|
||||
private function processLog(
|
||||
string $rawLog,
|
||||
bool $cutFirstLine = false,
|
||||
bool $cutEmptyLastLine = false
|
||||
bool $cutEmptyLastLine = false,
|
||||
array $filteredTerms = []
|
||||
): string {
|
||||
$logParts = explode("\n", $rawLog);
|
||||
|
||||
|
@ -96,6 +105,8 @@ trait HasLogViewer
|
|||
$logParts = str_replace(['>', '<'], ['>', '<'], $logParts);
|
||||
|
||||
$log = implode("\n", $logParts);
|
||||
return mb_convert_encoding($log, 'UTF-8', 'UTF-8');
|
||||
$log = mb_convert_encoding($log, 'UTF-8', 'UTF-8');
|
||||
|
||||
return str_replace($filteredTerms, '(PASSWORD)', $log);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Stations;
|
||||
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class HelpAction
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
string $station_id
|
||||
): ResponseInterface {
|
||||
$router = $request->getRouter();
|
||||
|
||||
return $request->getView()->renderVuePage(
|
||||
response: $response,
|
||||
component: 'Vue_StationsHelp',
|
||||
id: 'stations-help',
|
||||
title: __('Help'),
|
||||
props: [
|
||||
'logsUrl' => (string)$router->fromHere('api:stations:logs'),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Stations;
|
||||
|
||||
use App\Controller\AbstractLogViewerController;
|
||||
use App\Exception;
|
||||
use App\Http\Response;
|
||||
use App\Http\ServerRequest;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
final class LogsController extends AbstractLogViewerController
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
string $station_id
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
|
||||
return $request->getView()->renderToResponse($response, 'stations/logs/index', [
|
||||
'logs' => $this->getStationLogs($station),
|
||||
]);
|
||||
}
|
||||
|
||||
public function viewAction(
|
||||
ServerRequest $request,
|
||||
Response $response,
|
||||
string $station_id,
|
||||
string $log,
|
||||
): ResponseInterface {
|
||||
$station = $request->getStation();
|
||||
$log_areas = $this->getStationLogs($station);
|
||||
|
||||
if (!isset($log_areas[$log])) {
|
||||
throw new Exception('Invalid log file specified.');
|
||||
}
|
||||
|
||||
$logArea = $log_areas[$log];
|
||||
return $this->streamLogToResponse($request, $response, $logArea['path'], $logArea['tail'] ?? true);
|
||||
}
|
||||
|
||||
protected function processLog(
|
||||
ServerRequest $request,
|
||||
string $rawLog,
|
||||
bool $cutFirstLine = false,
|
||||
bool $cutEmptyLastLine = false
|
||||
): string {
|
||||
$log = parent::processLog($request, $rawLog, $cutFirstLine, $cutEmptyLastLine);
|
||||
|
||||
// Filter out passwords, API keys, etc.
|
||||
$station = $request->getStation();
|
||||
|
||||
$frontendConfig = $station->getFrontendConfig();
|
||||
|
||||
$passwords = [
|
||||
$station->getAdapterApiKey(),
|
||||
$frontendConfig->getAdminPassword(),
|
||||
$frontendConfig->getRelayPassword(),
|
||||
$frontendConfig->getSourcePassword(),
|
||||
$frontendConfig->getStreamerPassword(),
|
||||
];
|
||||
|
||||
return str_replace($passwords, '(PASSWORD)', $log);
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @var App\Environment $environment
|
||||
*/
|
||||
|
||||
$this->layout('main', [
|
||||
'title' => __('Log Viewer'),
|
||||
'manual' => true,
|
||||
]);
|
||||
?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title"><?=__('System Logs')?></h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($environment->isDocker()): ?>
|
||||
<p><?= sprintf(
|
||||
__(
|
||||
'Because you are running Docker, some system logs can only be accessed from a shell session on the host computer. You can run <code>%s</code> to access container logs from the terminal.'
|
||||
),
|
||||
'docker-compose logs -f (nginx|web|stations|...)'
|
||||
) ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<?php foreach ($global_logs as $log_key => $log_info): ?>
|
||||
<a class="list-group-item list-group-item-action log-item" href="<?=$router->fromHere('admin:logs:view',
|
||||
['station_id' => 'global', 'log' => $log_key])?>">
|
||||
<span class="log-name"><?=$log_info['name']?></span><br>
|
||||
<small class="text-secondary"><?=$log_info['path']?></small>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title"><?=__('Logs by Station')?></h2>
|
||||
</div>
|
||||
<div class="card-body pb-0">
|
||||
<ul class="nav nav-pills nav-pills-scrollable card-header-pills">
|
||||
<?php foreach ($station_logs as $station_id => $station_row): ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" role="tab" data-toggle="tab" aria-expanded="true" aria-controls="logs_station_<?=$station_id?>" href="#logs_station_<?=$station_id?>"><?=$this->e($station_row['name'])?></a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<?php foreach ($station_logs as $station_id => $station_row): ?>
|
||||
<div class="list-group list-group-flush tab-pane" id="logs_station_<?=$station_id?>">
|
||||
<?php foreach ($station_row['logs'] as $log_key => $log_info): ?>
|
||||
<a class="list-group-item list-group-item-action log-item" href="<?=$router->fromHere('admin:logs:view',
|
||||
['station_id' => $station_id, 'log' => $log_key])?>">
|
||||
<span class="log-name"><?=$log_info['name']?></span><br>
|
||||
<small class="text-secondary"><?=$log_info['path']?></small>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<?=$this->fetch('partials/log_help_card')?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?=$this->fetch('partials/log_viewer')?>
|
|
@ -1,24 +0,0 @@
|
|||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title"><?=__('Need Help?')?></h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><?=sprintf(
|
||||
__('You can find answers for many common questions in our <a href="%s" target="_blank">support documents</a>.'),
|
||||
'https://docs.azuracast.com/en/user-guide/troubleshooting'
|
||||
)?></p>
|
||||
|
||||
<p><?=__('If you\'re experiencing a bug or error, you can submit a GitHub issue using the link below.')?></p>
|
||||
|
||||
<p><?=sprintf(
|
||||
__('Your current installation type is <b>%s</b>. Be sure to include this when creating a new issue.'),
|
||||
($environment->isDocker() ? 'Docker' : 'Ansible')
|
||||
)?></p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-outline-primary" role="button" href="https://github.com/AzuraCast/AzuraCast/issues/new/choose" target="_blank">
|
||||
<i class="material-icons" aria-hidden="true">contact_support</i>
|
||||
<?=__('Add New GitHub Issue')?>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
|
@ -1,35 +0,0 @@
|
|||
$(function () {
|
||||
var logUrl = $('#log-view').data('url');
|
||||
var logContents = $('#log-view-contents');
|
||||
var currentLogPosition, timeoutUpdateLog;
|
||||
|
||||
function updateLogView () {
|
||||
$.getJSON(logUrl, {
|
||||
position: currentLogPosition
|
||||
}, function (logData) {
|
||||
|
||||
if (currentLogPosition == 0) {
|
||||
logContents.text('');
|
||||
}
|
||||
|
||||
if (logData.contents != '') {
|
||||
logContents.append(logData.contents + '\n');
|
||||
|
||||
// Timeout to allow height to adjust to appended contents.
|
||||
setTimeout(function () {
|
||||
logContents.animate({ scrollTop: logContents.prop('scrollHeight') }, 'fast');
|
||||
}, 500);
|
||||
}
|
||||
|
||||
currentLogPosition = logData.position;
|
||||
|
||||
if (!logData.eof) {
|
||||
timeoutUpdateLog = setTimeout(updateLogView, 15000);
|
||||
}
|
||||
}).fail(function (xhr) {
|
||||
logContents.text('Error: ' + xhr.responseJSON.message);
|
||||
});
|
||||
}
|
||||
|
||||
timeoutUpdateLog = setTimeout(updateLogView, 1000);
|
||||
});
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
/** @var \App\Assets $assets */
|
||||
$assets
|
||||
->load('clipboard')
|
||||
->addInlineJs($this->fetch('partials/log_inline.js'), 99);
|
||||
?>
|
||||
|
||||
<div id="log-view" data-url="<?=$url?>">
|
||||
<textarea class="form-control log-viewer" id="log-view-contents" spellcheck="false" readonly>Loading...</textarea>
|
||||
<div class="buttons pt-2">
|
||||
<button class="btn btn-copy btn-primary btn-sm" data-clipboard-target="#log-view-contents">
|
||||
<i class="material-icons">file_copy</i> <?=__('Copy to Clipboard')?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -1,65 +0,0 @@
|
|||
$(function() {
|
||||
var log_modal = $('#modal-log-view'),
|
||||
log_modal_contents = $('#modal-log-view-contents');
|
||||
|
||||
var current_log_url,
|
||||
current_log_position,
|
||||
timeout_update_log;
|
||||
|
||||
log_modal.modal({
|
||||
focus: false,
|
||||
show: false
|
||||
});
|
||||
|
||||
function updateLogView() {
|
||||
$.getJSON(current_log_url, {
|
||||
position: current_log_position
|
||||
}, function(log_data) {
|
||||
|
||||
if (current_log_position == 0) {
|
||||
log_modal_contents.text('');
|
||||
}
|
||||
|
||||
if (log_data.contents != '') {
|
||||
log_modal_contents.append(log_data.contents+"\n");
|
||||
|
||||
if (log_modal.find('#modal-log-autoscroll').is(':checked')) {
|
||||
// Timeout to allow height to adjust to appended contents.
|
||||
setTimeout(function() {
|
||||
log_modal_contents.animate({scrollTop: log_modal_contents.prop("scrollHeight")}, "fast");
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
current_log_position = log_data.position;
|
||||
|
||||
if (!log_data.eof) {
|
||||
timeout_update_log = setTimeout(updateLogView, 15000);
|
||||
}
|
||||
}).fail(function(xhr) {
|
||||
log_modal_contents.text('Error: '+xhr.responseJSON.message);
|
||||
});
|
||||
}
|
||||
|
||||
log_modal.on('hide.bs.modal', function (event) {
|
||||
current_log_url = null;
|
||||
current_log_position = 0;
|
||||
|
||||
clearTimeout(timeout_update_log);
|
||||
});
|
||||
|
||||
$('a.log-item').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
current_log_url = $(this).attr('href');
|
||||
current_log_position = 0;
|
||||
|
||||
log_modal.find('.modal-title').text($(this).find('.log-name').text());
|
||||
log_modal.find('#modal-log-view-contents').text('Loading...');
|
||||
|
||||
log_modal.modal('show');
|
||||
|
||||
updateLogView();
|
||||
return false;
|
||||
});
|
||||
});
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
/** @var \App\Assets $assets */
|
||||
$assets
|
||||
->load('clipboard')
|
||||
->addInlineJs($this->fetch('partials/log_viewer.js'), 99);
|
||||
?>
|
||||
|
||||
<div class="modal fade" id="modal-log-view" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modal-log-view-label"><?= __('Log Viewer') ?></h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="custom-control custom-switch pb-3">
|
||||
<input class="custom-control-input" id="modal-log-autoscroll" type="checkbox" checked>
|
||||
<span class="custom-control-track"></span>
|
||||
<label class="custom-control-label" for="modal-log-autoscroll"><?= __(
|
||||
'Automatically scroll to the bottom of the log'
|
||||
) ?></label>
|
||||
</div>
|
||||
|
||||
<textarea class="form-control log-viewer" id="modal-log-view-contents" spellcheck="false" readonly>Loading...</textarea>
|
||||
<div class="buttons pt-2">
|
||||
<button class="btn btn-copy btn-primary btn-sm" data-clipboard-target="#modal-log-view-contents">
|
||||
<i class="material-icons">file_copy</i> <?=__('Copy to Clipboard')?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
$this->layout('main', [
|
||||
'title' => __('Log Viewer'),
|
||||
'manual' => true
|
||||
]);
|
||||
?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary-dark">
|
||||
<h2 class="card-title"><?=__('Available Logs') ?></h2>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<?php foreach($logs as $log_key => $log_info): ?>
|
||||
<a class="list-group-item list-group-item-action log-item" href="<?=$router->fromHere('stations:logs:view', ['log' => $log_key]) ?>">
|
||||
<span class="log-name"><?=$log_info['name'] ?></span><br>
|
||||
<small class="text-secondary"><?=$log_info['path'] ?></small>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<?=$this->fetch('partials/log_help_card') ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?=$this->fetch('partials/log_viewer') ?>
|
Loading…
Reference in New Issue