Skip to content

Commit 3ac862a

Browse files
committed
Update logging with new abstraction/configuration
1 parent 84daace commit 3ac862a

File tree

3 files changed

+90
-55
lines changed

3 files changed

+90
-55
lines changed

16/umbraco-cms/fundamentals/backoffice/logviewer.md

Lines changed: 83 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,13 @@ Here are some example queries to help you get started. For more details on the s
3030

3131
If you frequently use a custom query, you can save it for quick access. Type your query in the search box and click the heart icon to save it with a friendly name. Saved queries are stored in the `umbracoLogViewerQuery` table in the database.
3232

33-
## Implementing Your Own Log Viewer
33+
## Implementing Your Own Log Viewer Source
3434

35-
Umbraco allows you to implement a customn `ILogViewer` to fetch logs from alternative sources, such as **Azure Table Storage**.
35+
Umbraco allows you to implement a customn `ILogViewerRepository` to fetch logs from alternative sources, such as **Azure Table Storage**.
3636

37-
### Creating a Custom Log Viewer
37+
### Creating a Custom Log Viewer Repository
3838

39-
To fetch logs from Azure Table Storage, implement the `SerilogLogViewerSourceBase` class from `Umbraco.Cms.Core.Logging.Viewer`.
39+
To fetch logs from Azure Table Storage, extend the `LogViewerRepositoryBase` class from `Umbraco.Cms.Infrastructure.Services.Implement`.
4040

4141
{% hint style="info" %}
4242
This implementation requires the `Azure.Data.Tables` NuGet package.
@@ -47,90 +47,118 @@ using Azure;
4747
using Azure.Data.Tables;
4848
using Serilog.Events;
4949
using Serilog.Formatting.Compact.Reader;
50-
using Serilog.Sinks.AzureTableStorage;
50+
using Umbraco.Cms.Core.Composing;
5151
using Umbraco.Cms.Core.Logging.Viewer;
52-
using ITableEntity = Azure.Data.Tables.ITableEntity;
52+
using Umbraco.Cms.Core.Serialization;
53+
using Umbraco.Cms.Core.Services;
54+
using Umbraco.Cms.Infrastructure.Logging.Serilog;
55+
using Umbraco.Cms.Infrastructure.Services.Implement;
5356

5457
namespace My.Website;
5558

56-
public class AzureTableLogViewer : SerilogLogViewerSourceBase
59+
public class AzureTableLogsRepository : LogViewerRepositoryBase
5760
{
58-
public AzureTableLogViewer(ILogViewerConfig logViewerConfig, Serilog.ILogger serilogLog, ILogLevelLoader logLevelLoader)
59-
: base(logViewerConfig, logLevelLoader, serilogLog)
61+
private readonly IJsonSerializer _jsonSerializer;
62+
63+
public AzureTableLogsRepository(UmbracoFileConfiguration umbracoFileConfig, IJsonSerializer jsonSerializer) : base(
64+
umbracoFileConfig)
6065
{
66+
_jsonSerializer = jsonSerializer;
6167
}
6268

63-
public override bool CanHandleLargeLogs => true;
64-
65-
// This method will not be called - as we have indicated that this 'CanHandleLargeLogs'
66-
public override bool CheckCanOpenLogs(LogTimePeriod logTimePeriod) => throw new NotImplementedException();
67-
68-
protected override IReadOnlyList<LogEvent> GetLogs(LogTimePeriod logTimePeriod, ILogFilter filter, int skip, int take)
69+
protected override IEnumerable<ILogEntry> GetLogs(LogTimePeriod logTimePeriod, ILogFilter logFilter)
6970
{
70-
//Replace ACCOUNT_NAME and KEY with your actual Azure Storage Account details. The "Logs" parameter refers to the table name where logs will be stored and retrieved from.
71+
// This example uses a connetionstring compatible with the Azurite emulator
72+
// https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite
7173
var client =
7274
new TableClient(
73-
"DefaultEndpointsProtocol=https;AccountName=ACCOUNT_NAME;AccountKey=KEY;EndpointSuffix=core.windows.net",
74-
"Logs");
75-
76-
// Table storage does not support skip, only take, so the best we can do is to not fetch more entities than we need in total.
77-
// See: https://learn.microsoft.com/en-us/rest/api/storageservices/writing-linq-queries-against-the-table-service#returning-the-top-n-entities for more info.
78-
var requiredEntities = skip + take;
79-
IEnumerable<AzureTableLogEntity> results = client.Query<AzureTableLogEntity>().Take(requiredEntities);
80-
81-
return results
82-
.Skip(skip)
83-
.Take(take)
84-
.Select(x => LogEventReader.ReadFromString(x.Data))
85-
// Filter by timestamp to avoid retrieving all logs from the table, preventing memory and performance issues
86-
.Where(evt => evt.Timestamp >= logTimePeriod.StartTime.Date &&
87-
evt.Timestamp <= logTimePeriod.EndTime.Date.AddDays(1).AddSeconds(-1))
88-
.Where(filter.TakeLogEvent)
89-
.ToList();
75+
"UseDevelopmentStorage=true",
76+
"LogEventEntity");
77+
78+
// Filter by timestamp to avoid retrieving all logs from the table, preventing memory and performance issues
79+
IEnumerable<AzureTableLogEntity> results = client.Query<AzureTableLogEntity>(
80+
entity => entity.Timestamp >= logTimePeriod.StartTime.Date &&
81+
entity.Timestamp <= logTimePeriod.EndTime.Date.AddDays(1).AddSeconds(-1));
82+
83+
// Read the data and apply logfilters
84+
IEnumerable<LogEvent> filteredData = results.Select(x => LogEventReader.ReadFromString(x.Data))
85+
.Where(logFilter.TakeLogEvent);
86+
87+
return filteredData.Select(x => new LogEntry
88+
{
89+
Timestamp = x.Timestamp,
90+
Level = Enum.Parse<Core.Logging.LogLevel>(x.Level.ToString()),
91+
MessageTemplateText = x.MessageTemplate.Text,
92+
Exception = x.Exception?.ToString(),
93+
Properties = MapLogMessageProperties(x.Properties),
94+
RenderedMessage = x.RenderMessage(),
95+
});
9096
}
9197

92-
public override IReadOnlyList<SavedLogSearch>? GetSavedSearches()
98+
private IReadOnlyDictionary<string, string?> MapLogMessageProperties(
99+
IReadOnlyDictionary<string, LogEventPropertyValue>? properties)
93100
{
94-
//This method is optional. If you store saved searches in Azure Table Storage, implement fetching logic here.
95-
return base.GetSavedSearches();
101+
var result = new Dictionary<string, string?>();
102+
103+
if (properties is not null)
104+
{
105+
foreach (KeyValuePair<string, LogEventPropertyValue> property in properties)
106+
{
107+
string? value;
108+
109+
if (property.Value is ScalarValue scalarValue)
110+
{
111+
value = scalarValue.Value?.ToString();
112+
}
113+
else if (property.Value is StructureValue structureValue)
114+
{
115+
var textWriter = new StringWriter();
116+
structureValue.Render(textWriter);
117+
value = textWriter.ToString();
118+
}
119+
else
120+
{
121+
value = _jsonSerializer.Serialize(property.Value);
122+
}
123+
124+
result.Add(property.Key, value);
125+
}
126+
}
127+
128+
return result;
96129
}
97130

98-
public override IReadOnlyList<SavedLogSearch>? AddSavedSearch(string? name, string? query)
131+
public class AzureTableLogEntity : ITableEntity
99132
{
100-
//This method is optional. If you store saved searches in Azure Table Storage, implement adding logic here.
101-
return base.AddSavedSearch(name, query);
102-
}
133+
public required string Data { get; set; }
103134

104-
public override IReadOnlyList<SavedLogSearch>? DeleteSavedSearch(string? name, string? query)
105-
{
106-
//This method is optional. If you store saved searches in Azure Table Storage, implement deleting logic here.
107-
return base.DeleteSavedSearch(name, query);
108-
}
109-
}
135+
public required string PartitionKey { get; set; }
110136

111-
public class AzureTableLogEntity : LogEventEntity, ITableEntity
112-
{
113-
public DateTimeOffset? Timestamp { get; set; }
137+
public required string RowKey { get; set; }
114138

115-
public ETag ETag { get; set; }
139+
public DateTimeOffset? Timestamp { get; set; }
140+
141+
public ETag ETag { get; set; }
142+
}
116143
}
117144
```
118145

119-
Azure Table Storage requires entities to implement the `ITableEntity` interface. Since Umbraco’s default log entity does not implement this, a custom entity (`AzureTableLogEntity`) must be created to ensure logs are correctly fetched and stored.
146+
Azure Table Storage requires entities to implement the `ITableEntity` interface. Since Umbraco’s default log entity does not implement this, a custom entity (`AzureTableLogEntity`) must be created to ensure logs are correctly fetched.
120147

121148
### Register implementation
122149

123-
Umbraco needs to be made aware that there is a new implementation of an `ILogViewer` to register. We also need to replace the default JSON LogViewer that we ship in the core of Umbraco.
150+
Umbraco needs to be made aware that there is a new implementation of an `ILogViewerRepository` to register. We also need to replace the default JSON LogViewer that is shipped in the core of Umbraco.
124151

125152
```csharp
126153
using Umbraco.Cms.Core.Composing;
127154
using Umbraco.Cms.Infrastructure.DependencyInjection;
128155

129156
namespace My.Website;
130157

131-
public class LogViewerSavedSearches : IComposer
132-
{
133-
public void Compose(IUmbracoBuilder builder) => builder.SetLogViewer<AzureTableLogViewer>();
158+
public class AzureTableLogsComposer : IComposer
159+
{
160+
public void Compose(IUmbracoBuilder builder) => builder.Services.AddUnique<ILogViewerRepository, AzureTableLogsRepository>();
161+
}
134162
}
135163
```
136164

16/umbraco-cms/fundamentals/code/debugging/logging.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ Serilog uses levels as the primary means for assigning importance to log events.
117117

118118
Serilog can be configured and extended by using the .NET Core configuration such as the AppSetting.json files or environment variables. For more information, see the [Serilog config](../../../reference/configuration/serilog.md) article.
119119

120+
## The UmbracoFile Sink
121+
122+
Serilog uses the concept of Sinks to output the log messages to different places. Umbraco ships with a custom sink configuration called UmbracoFile that uses the [Serilog.Sinks.File](https://github.com/serilog/serilog-sinks-file) sink to save the logs to a rolling file on disk in the Umbraco. You can disable this sink by setting its Enabled configuration flag to false, see [Serilog config](../../../reference/configuration/serilog.md) for more information.
123+
120124
## The logviewer dashboard
121125

122126
Learn more about the [logviewer dashboard](../../backoffice/logviewer.md) in the backoffice and how it can be extended.

16/umbraco-cms/reference/configuration/serilog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ By default, Umbraco uses a special Serilog 'sink' that is optimized for performa
105105
{
106106
"Name": "UmbracoFile",
107107
"Args": {
108+
"Enabled": "True",
108109
"RestrictedToMinimumLevel": "Warning",
109110
"FileSizeLimitBytes": 1073741824,
110111
"RollingInterval" : "Day",
@@ -117,6 +118,8 @@ By default, Umbraco uses a special Serilog 'sink' that is optimized for performa
117118
}
118119
```
119120

121+
You can also disable this sink if you do not wish to write files to disk.
122+
120123
## Adding a custom log property to all log items
121124

122125
You may wish to add a log property to all log messages. A good example could be a log property for the `environment` to determine if the log message came from `development` or `production`.

0 commit comments

Comments
 (0)