SimonCropp / SeqProxy

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

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.

About

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

License:MIT License


Languages

Language:C# 92.0%Language:JavaScript 5.8%Language:HTML 2.2%