Skip to content
Merged
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 docs-website/docs/admin-panel/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Each user has one role. They build up on each other:
|---|---|
| **Viewer** | See the servers, accounts and the configuration |
| **Operator** | Everything above, plus operating the servers and editing accounts |
| **Administrator** | Everything above, plus the setup, plugins, configuration updates, log files and the user management |
| **Administrator** | Everything above, plus the setup, plugins, configuration updates, log files, the logging configuration and the user management |

Give each administrator their own user, so you can remove one without changing
everybody else's password.
Expand Down
32 changes: 29 additions & 3 deletions docs-website/docs/admin-panel/logs-and-monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,36 @@ file, so a very large log stays fast.

### Configuring what is logged

**Navigation:** *Logging Configuration* — route `/logging-configuration`
(administrators only)

Logging is configured in `appsettings.json` with
[Serilog](https://github.com/serilog/serilog-settings-configuration). The server
makes good use of log scopes, so you can configure it to log only the actions of
certain players, of a certain server, or of a certain logger. See
[Serilog](https://github.com/serilog/serilog-settings-configuration). In the
all-in-one deployment, you can edit it in the admin panel instead of editing the
file. The page shows the path of the file it edits, and offers two tabs:

| Tab | Effect |
|---|---|
| **Log Levels** | The default minimum level, and a table of levels per source. A source is the name of a logger — usually the full name of a class, like `MUnique.OpenMU.ConnectServer.ClientListener` — or a namespace like `MUnique.OpenMU.GameLogic`, which covers all loggers in it. The most specific source wins. |
| **Advanced (JSON)** | The whole `Serilog` section of the file, for everything the table doesn't cover, like sinks, output templates or filters. Invalid JSON is rejected. |

![Log levels of the logging configuration](/img/admin-panel/logging-configuration-levels.png)

![Advanced JSON editor of the logging configuration](/img/admin-panel/logging-configuration-json.png)

**Save** writes the file; the rest of `appsettings.json` stays as it is (comments
in it are lost, though). The server watches the file, so a changed level of the
default or of an existing source applies at once — no restart needed. A newly
added or removed source and everything else in the `Serilog` section take effect
after the next restart. Settings from environment variables or an
`appsettings.<Environment>.json` file override the file, so changing them here
has no effect.

In a docker container, the file lives inside the container: mount it as a
volume, if a change should survive recreating the container.

The server makes good use of log scopes, so you can configure it to log only the
actions of certain players, of a certain server, or of a certain logger. See
[Startup parameters](../deployment/startup-parameters.md#logging).

### Grafana (all-in-one, optional)
Expand Down
4 changes: 3 additions & 1 deletion docs-website/docs/deployment/startup-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ Logging can be configured in the `appsettings.json` file. By default, not a lot
configured. To extend the configuration, have a look at the
[Serilog documentation](https://github.com/serilog/serilog-settings-configuration).
The server makes good use of scopes, so you can configure it to log only the
actions of certain players, for example.
actions of certain players, for example. In the all-in-one deployment, the admin
panel can edit the logging configuration, too — see
[Logs and monitoring](../admin-panel/logs-and-monitoring.md#configuring-what-is-logged).

## Settings priority

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/Web/AdminPanel/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@
<span class="oi oi-list" aria-hidden="true"></span> @Resources.LogFiles
</NavLink>
</li>
<AuthorizeView Policy="@AdminPolicies.Administrator">
<li class="nav-item px-3">
<NavLink class="nav-link" href="logging-configuration" @onclick="() => NavigationHistory.Clear()">
<span class="oi oi-wrench" aria-hidden="true"></span> @Resources.LoggingConfiguration
</NavLink>
</li>
</AuthorizeView>
}
@if (AdminPanelEnvironment.IsGrafanaAvailable)
{
Expand Down
109 changes: 109 additions & 0 deletions src/Web/AdminPanel/Pages/LoggingConfiguration.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
@page "/logging-configuration"
@attribute [Authorize(Policy = AdminPolicies.Administrator)]

@using MUnique.OpenMU.Web.AdminPanel.Properties
@using MUnique.OpenMU.Web.AdminPanel.Services

<PageTitle>OpenMU: @Resources.LoggingConfiguration</PageTitle>
<Breadcrumb IsFirstFromRoot="true" Caption="@Resources.LoggingConfiguration"/>
<h1>@Resources.LoggingConfiguration</h1>

@if (!AdminPanelEnvironment.IsHostingEmbedded)
{
<div class="alert alert-info" role="alert">@Resources.LoggingConfigurationNotAvailable</div>
return;
}

<p class="text-muted">@string.Format(Resources.LoggingConfigurationDescription, this.LoggingConfigurationService.FilePath)</p>

@if (this._loadError is { } loadError)
{
<div class="alert alert-danger" role="alert">@string.Format(Resources.LoggingConfigurationLoadError, loadError)</div>
<button type="button" class="btn btn-secondary" @onclick="this.LoadAsync">
<span class="oi oi-reload" aria-hidden="true"></span> @Resources.Reload
</button>
return;
}

@if (this._settings is not { } settings)
{
<p>@Resources.Loading</p>
return;
}

<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<button type="button" class="nav-link @(this._showJson ? null : "active")" @onclick="() => this._showJson = false">@Resources.LogLevels</button>
</li>
<li class="nav-item">
<button type="button" class="nav-link @(this._showJson ? "active" : null)" @onclick="() => this._showJson = true">@Resources.AdvancedJson</button>
</li>
</ul>

@if (this._showJson)
{
<p class="text-muted">
@Resources.AdvancedLoggingJsonDescription
<a href="https://github.com/serilog/serilog-settings-configuration" target="_blank" rel="noopener noreferrer">Serilog.Settings.Configuration</a>
</p>
<textarea class="form-control font-monospace mb-3" rows="25" spellcheck="false" aria-label="@Resources.AdvancedJson" @bind="this._sectionJson"></textarea>
}
else
{
<div class="mb-3 col-md-4">
<label class="form-label" for="defaultLogLevel">@Resources.DefaultLogLevel</label>
<select id="defaultLogLevel" class="form-select" @bind="settings.DefaultLevel">
@foreach (var level in LoggingConfigurationService.Levels)
{
<option value="@level">@level</option>
}
</select>
</div>

<h5>@Resources.LogLevelOverrides</h5>
<p class="text-muted">@Resources.LogLevelOverridesDescription</p>
<table class="table table-striped table-hover">
<thead>
<tr>
<th class="col-7">@Resources.LogSource</th>
<th class="col-3">@Resources.Level</th>
<th class="col-2">@Resources.Actions</th>
</tr>
</thead>
<tbody>
@foreach (var levelOverride in settings.Overrides)
{
<tr>
<td>
<input type="text" class="form-control font-monospace" aria-label="@Resources.LogSource" @bind="levelOverride.Source"/>
</td>
<td>
<select class="form-select" aria-label="@Resources.Level" @bind="levelOverride.Level">
@foreach (var level in LoggingConfigurationService.Levels)
{
<option value="@level">@level</option>
}
</select>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger" @onclick="() => settings.Overrides.Remove(levelOverride)">
<span class="oi oi-trash" aria-hidden="true"></span> @Resources.Remove
</button>
</td>
</tr>
}
</tbody>
</table>
<button type="button" class="btn btn-secondary mb-3" @onclick="() => settings.Overrides.Add(new LogLevelOverride())">
<span class="oi oi-plus" aria-hidden="true"></span> @Resources.AddNew
</button>
}

<div>
<button type="button" class="btn btn-primary" disabled="@this._isSaving" @onclick="this.SaveAsync">
<span class="oi oi-check" aria-hidden="true"></span> @Resources.Save
</button>
<button type="button" class="btn btn-secondary" disabled="@this._isSaving" @onclick="this.LoadAsync">
<span class="oi oi-reload" aria-hidden="true"></span> @Resources.Reload
</button>
</div>
103 changes: 103 additions & 0 deletions src/Web/AdminPanel/Pages/LoggingConfiguration.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// <copyright file="LoggingConfiguration.razor.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Web.AdminPanel.Pages;

using System.IO;
using System.Text.Json;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Web.AdminPanel.Properties;
using MUnique.OpenMU.Web.AdminPanel.Services;
using MUnique.OpenMU.Web.Shared.Components.Toast;

/// <summary>
/// The page which edits the logging configuration of the server.
/// </summary>
public partial class LoggingConfiguration
{
private LoggingSettings? _settings;
private string _sectionJson = string.Empty;
private string? _loadError;
private bool _showJson;
private bool _isSaving;

[Inject]
private LoggingConfigurationService LoggingConfigurationService { get; set; } = null!;

[Inject]
private IToastService ToastService { get; set; } = null!;

[Inject]
private ILogger<LoggingConfiguration> Logger { get; set; } = null!;

/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync().ConfigureAwait(true);
if (AdminPanelEnvironment.IsHostingEmbedded)
{
await this.LoadAsync().ConfigureAwait(true);
}
}

private async Task LoadAsync()
{
try
{
this._settings = await this.LoggingConfigurationService.LoadAsync().ConfigureAwait(true);
this._sectionJson = this._settings.SectionJson;
this._loadError = null;
}
catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException)
{
this.Logger.LogError(ex, "Couldn't load the logging configuration from {filePath}.", this.LoggingConfigurationService.FilePath);
this._settings = null;
this._loadError = ex.Message;
}
}

private async Task SaveAsync()
{
if (this._settings is not { } settings)
{
return;
}

this._isSaving = true;
try
{
if (this._showJson)
{
await this.LoggingConfigurationService.SaveSectionAsync(this._sectionJson).ConfigureAwait(true);
}
else if (settings.Overrides.Any(o => string.IsNullOrWhiteSpace(o.Source))
|| settings.Overrides.GroupBy(o => o.Source.Trim()).Any(g => g.Count() > 1))
{
this.ToastService.ShowError(Resources.LogLevelOverridesInvalid);
return;
}
else
{
await this.LoggingConfigurationService.SaveMinimumLevelsAsync(settings.DefaultLevel, settings.Overrides).ConfigureAwait(true);
}

this.ToastService.ShowSuccess(Resources.LoggingConfigurationSaved);
await this.LoadAsync().ConfigureAwait(true);
}
catch (JsonException ex) when (this._showJson)
{
this.ToastService.ShowError(string.Format(Resources.LoggingConfigurationInvalidJson, ex.Message));
}
catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException)
{
this.Logger.LogError(ex, "Couldn't save the logging configuration to {filePath}.", this.LoggingConfigurationService.FilePath);
this.ToastService.ShowError(string.Format(Resources.LoggingConfigurationSaveError, ex.Message));
}
finally
{
this._isSaving = false;
}
}
}
Loading
Loading