SeqProxy
SeqProxy copied to clipboard
Enables writing seq logs by proxying requests through an ASP.NET Controller or Middleware.
SeqProxy
Enables writing Seq logs by proxying requests through an ASP.NET Controller or Middleware.
See Milestones for release notes.
Why
- 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.
NuGet package
https://nuget.org/packages/SeqProxy/
HTTP Format/Protocol
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.
Extra data
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
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");
snippet source | anchor
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'
Usage
Enable in Startup
Enable in Startup.ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(option => option.EnableEndpointRouting = false);
services.AddSeqWriter(seqUrl: "http://localhost:5341");
}
snippet source | anchor
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)..];
});
}
snippet source | anchor
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)
{
Guard.AgainstEmpty(claimType, nameof(claimType));
var lastIndexOf = claimType.LastIndexOf('/');
if (lastIndexOf == -1)
{
return claimType;
}
return claimType[(lastIndexOf + 1)..];
}
}
snippet source | anchor
Add HTTP handling
There are two approaches to handling the HTTP containing log events. Using a Middleware and using a Controller.
Using a Middleware
Using a Middleware is done by calling SeqWriterConfig.UseSeq in Startup.Configure(IApplicationBuilder builder):
public void Configure(IApplicationBuilder builder)
{
builder.UseSeq();
snippet source | anchor
Authorization
Authorization in the middleware can bu done by using useAuthorizationService = true in UseSeq.
public void Configure(IApplicationBuilder builder)
{
builder.UseSeq(useAuthorizationService: true);
snippet source | anchor
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);
}
snippet source | anchor
Using a Controller
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);
}
snippet source | anchor
Add a new controller that overrides BaseSeqController.
public class SeqController(SeqWriter writer) :
BaseSeqController(writer);
snippet source | anchor
Authorization/Authentication
Adding authorization and authentication can be done with an AuthorizeAttribute.
[Authorize]
public class SeqController(SeqWriter writer) :
BaseSeqController(writer)
snippet source | anchor
Method level attributes
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();
snippet source | anchor
Client Side Usage
Using raw JavaScript
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);
}
snippet source | anchor
Using Structured-Log
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:
Include the libraries
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>
snippet source | anchor
Configure the log
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();
snippet source | anchor
Write a log message
function LogStructured(text) {
log.info('StructuredLog input: {Text}', text);
}
snippet source | anchor
Including data but omitting from the message template
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();
snippet source | anchor
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
});
}
snippet source | anchor
Then a destructured property will be written to Seq.
Icon
Robot designed by Maxim Kulikov from The Noun Project.