/SeqProxy

Enables writing seq logs by proxying requests through an ASP.NET Controller or Middleware.

Primary LanguageC#MIT LicenseMIT

SeqProxy

Build status NuGet Status

Enables writing Seq logs by proxying requests through an ASP.NET Controller or Middleware.

See Milestones for release notes.

Why

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) using Environment.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

  • application defaults to Assembly.GetCallingAssembly().GetName().Name.
  • applicationVersion defaults to Assembly.GetCallingAssembly().GetName().Version.
  • scrubClaimType is used to clean up claimtype strings. For example ClaimTypes.Email is http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, but when recording to Seq the value emailaddress is sufficient. Defaults to DefaultClaimTypeScrubber.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)..];
    }
}

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.