Enables writing Seq logs by proxying requests through an ASP.NET Controller or Middleware.
See Milestones for release notes.
- Avoid exposing the Seq API to the internet.
- Leverage Asp Authentication and Authorization to verify and control incoming requests.
- Append extra data to log messages during server processing.
https://nuget.org/packages/SeqProxy/
Format: Serilog compact.
Protocol: Seq raw events.
Note that timestamp (@t
) is optional when using this project. If it is not supplied the server timestamp will be used.
For every log entry written the following information is appended:
- The current application name (as
Application
) defined in code at startup. - The current application version (as
ApplicationVersion
) defined in code at startup. - The server name (as
Server
) usingEnvironment.MachineName
. - All claims for the current User from
ControllerBase.User.Claims
. - The user-agent header as
UserAgent
. - The referer header as
Referrer
.
SeqProxyId is a tick based timestamp to help correlating a front-end error with a Seq log entry.
It is appended to every Seq log entry and returned as a header to HTTP response.
The id is generated using the following:
var startOfYear = new DateTime(utcNow.Year, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var ticks = utcNow.Ticks - startOfYear.Ticks;
var id = ticks.ToString("x");
Which generates a string of the form 8e434f861302
. The current year is trimmed to shorten the id and under the assumption that retention policy is not longer than 12 months. There is a small chance of collisions, but given the use-case (error correlation), this should not impact the ability to find the correct error. This string can then be given to a user as a error correlation id.
Then the log entry can be accessed using a Seq filter.
http://seqServer/#/events?filter=SeqProxyId%3D'39f616eeb2e3'
Enable in Startup.ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(option => option.EnableEndpointRouting = false);
services.AddSeqWriter(seqUrl: "http://localhost:5341");
}
There are several optional parameters:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore();
services.AddSeqWriter(
seqUrl: "http://localhost:5341",
apiKey: "TheApiKey",
application: "MyAppName",
appVersion: new(1, 2),
scrubClaimType: claimType =>
{
var lastIndexOf = claimType.LastIndexOf('/');
if (lastIndexOf == -1)
{
return claimType;
}
return claimType[(lastIndexOf + 1)..];
});
}
application
defaults toAssembly.GetCallingAssembly().GetName().Name
.applicationVersion
defaults toAssembly.GetCallingAssembly().GetName().Version
.scrubClaimType
is used to clean up claimtype strings. For example ClaimTypes.Email ishttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
, but when recording to Seq the valueemailaddress
is sufficient. Defaults toDefaultClaimTypeScrubber.Scrub
to get the string after the last/
.
namespace SeqProxy;
/// <summary>
/// Used for scrubbing claims when no other scrubber is defined.
/// </summary>
public static class DefaultClaimTypeScrubber
{
/// <summary>
/// Get the string after the last /.
/// </summary>
public static CharSpan Scrub(CharSpan claimType)
{
Guard.AgainstEmpty(claimType, nameof(claimType));
var lastIndexOf = claimType.LastIndexOf('/');
if (lastIndexOf == -1)
{
return claimType;
}
return claimType[(lastIndexOf + 1)..];
}
}
There are two approaches to handling the HTTP containing log events. Using a Middleware and using a Controller.
Using a Middleware is done by calling SeqWriterConfig.UseSeq
in Startup.Configure(IApplicationBuilder builder)
:
public void Configure(IApplicationBuilder builder)
{
builder.UseSeq();
Authorization in the middleware can bu done by using useAuthorizationService = true
in UseSeq
.
public void Configure(IApplicationBuilder builder)
{
builder.UseSeq(useAuthorizationService: true);
This then uses IAuthorizationService to verify the request:
async Task HandleWithAuth(HttpContext context)
{
var user = context.User;
var authResult = await authService.AuthorizeAsync(user, null, "SeqLog");
if (!authResult.Succeeded)
{
await context.ChallengeAsync();
return;
}
await writer.Handle(
user,
context.Request,
context.Response,
context.RequestAborted);
}
BaseSeqController
is an implementation of ControllerBase
that provides a HTTP post and some basic routing.
namespace SeqProxy;
/// <summary>
/// An implementation of <see cref="ControllerBase"/> that provides a http post and some basic routing.
/// </summary>
[Route("/api/events/raw")]
[Route("/seq")]
[ApiController]
public abstract class BaseSeqController :
ControllerBase
{
SeqWriter writer;
/// <summary>
/// Initializes a new instance of <see cref="BaseSeqController"/>
/// </summary>
protected BaseSeqController(SeqWriter writer) =>
this.writer = writer;
/// <summary>
/// Handles log events via a HTTP post.
/// </summary>
[HttpPost]
public virtual Task Post() =>
writer.Handle(User, Request, Response, HttpContext.RequestAborted);
}
Add a new controller that overrides BaseSeqController
.
public class SeqController(SeqWriter writer) :
BaseSeqController(writer);
Adding authorization and authentication can be done with an AuthorizeAttribute.
[Authorize]
public class SeqController(SeqWriter writer) :
BaseSeqController(writer)
Method level Asp attributes can by applied by overriding BaseSeqController.Post
.
For example adding an exception filter .
public class SeqController(SeqWriter writer) :
BaseSeqController(writer)
{
[CustomExceptionFilter]
public override Task Post() =>
base.Post();
Writing to Seq can be done using a HTTP post:
function LogRawJs(text) {
const postSettings = {
method: 'POST',
credentials: 'include',
body: `{'@mt':'RawJs input: {Text}','Text':'${text}'}`
};
return fetch('/api/events/raw', postSettings);
}
structured-log is a structured logging framework for JavaScript, inspired by Serilog.
In combination with structured-log-seq-sink it can be used to write to Seq
To use this approach:
Install both structured-log npm and structured-log-seq-sink npm. Or include them from jsDelivr:
<script src='https://cdn.jsdelivr.net/npm/structured-log/dist/structured-log.js'>
</script>
<script src='https://cdn.jsdelivr.net/npm/structured-log-seq-sink/dist/structured-log-seq-sink.js'>
</script>
var levelSwitch = new structuredLog.DynamicLevelSwitch('info');
const log = structuredLog.configure()
.writeTo(new structuredLog.ConsoleSink())
.minLevel(levelSwitch)
.writeTo(SeqSink({
url: `${location.protocol}//${location.host}`,
compact: true,
levelSwitch: levelSwitch
}))
.create();
function LogStructured(text) {
log.info('StructuredLog input: {Text}', text);
}
When using structured-log, data not included in the message template will be named with a convention of a+counter
. So for example if the following is logged:
log.info('The text: {Text}', text, "OtherData");
Then OtherData
would be written to Seq with the property name a1
.
To work around this:
Include a filter that replaces a known token name (in this case {@Properties}
):
const logWithExtraProps = structuredLog.configure()
.filter(logEvent => {
const template = logEvent.messageTemplate;
template.raw = template.raw.replace('{@Properties}','');
return true;
})
.writeTo(SeqSink({
url: `${location.protocol}//${location.host}`,
compact: true,
levelSwitch: levelSwitch
}))
.create();
Include that token name in the message template, and then include an object at the same position in the log parameters:
function LogStructuredWithExtraProps(text) {
logWithExtraProps.info(
'StructuredLog input: {Text} {@Properties}',
text,
{
Timezone: new Date().getTimezoneOffset(),
Language: navigator.language
});
}
Then a destructured property will be written to Seq.
Robot designed by Maxim Kulikov from The Noun Project.