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)..];
});
}applicationdefaults toAssembly.GetCallingAssembly().GetName().Name.applicationVersiondefaults toAssembly.GetCallingAssembly().GetName().Version.scrubClaimTypeis 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 valueemailaddressis sufficient. Defaults toDefaultClaimTypeScrubber.Scrubto 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)
{
Ensure.NotEmpty(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.

