Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/simoncropp/seqproxy

Enables writing seq logs by proxying requests through an ASP.NET Controller or Middleware.
https://github.com/simoncropp/seqproxy

Last synced: 8 days ago
JSON representation

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

Awesome Lists containing this project

README

        

# SeqProxy

[![Build status](https://ci.appveyor.com/api/projects/status/7996jd4uoooy5qy2/branch/main?svg=true)](https://ci.appveyor.com/project/SimonCropp/SeqProxy)
[![NuGet Status](https://img.shields.io/nuget/v/SeqProxy.svg)](https://www.nuget.org/packages/SeqProxy/)

Enables writing [Seq](https://datalust.co/seq) logs by proxying requests through an ASP.NET Controller or Middleware.

**See [Milestones](../../milestones?state=closed) for release notes.**

## Why

* Avoid exposing the Seq API to the internet.
* Leverage [Asp Authentication and Authorization](https://docs.microsoft.com/en-us/aspnet/core/security/) to verify and control incoming requests.
* Append [extra data](#extra-data) to log messages during server processing.

## NuGet package

https://nuget.org/packages/SeqProxy/

## HTTP Format/Protocol

Format: [Serilog compact](https://github.com/serilog/serilog-formatting-compact).

Protocol: [Seq raw events](https://docs.datalust.co/docs/posting-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](https://en.wikipedia.org/wiki/User_agent) as `UserAgent`.
* The [referer header](https://en.wikipedia.org/wiki/HTTP_referer) 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:


```cs
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`


```cs
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:


```cs
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](https://docs.microsoft.com/en-us/dotnet/api/system.identitymodel.claims.claimtypes.email?System_IdentityModel_Claims_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 `/`.


```cs
namespace SeqProxy;

///
/// Used for scrubbing claims when no other scrubber is defined.
///
public static class DefaultClaimTypeScrubber
{
///
/// Get the string after the last /.
///
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](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/) is done by calling `SeqWriterConfig.UseSeq` in `Startup.Configure(IApplicationBuilder builder)`:


```cs
public void Configure(IApplicationBuilder builder)
{
builder.UseSeq();
```
snippet source | anchor

##### Authorization

Authorization in the middleware can bu done by using `useAuthorizationService = true` in `UseSeq`.


```cs
public void Configure(IApplicationBuilder builder)
{
builder.UseSeq(useAuthorizationService: true);
```
snippet source | anchor

This then uses [IAuthorizationService](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased) to verify the request:


```cs
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.


```cs
namespace SeqProxy;

///
/// An implementation of that provides a http post and some basic routing.
///
[Route("/api/events/raw")]
[Route("/seq")]
[ApiController]
public abstract class BaseSeqController :
ControllerBase
{
SeqWriter writer;

///
/// Initializes a new instance of
///
protected BaseSeqController(SeqWriter writer) =>
this.writer = writer;

///
/// Handles log events via a HTTP post.
///
[HttpPost]
public virtual Task Post() =>
writer.Handle(User, Request, Response, HttpContext.RequestAborted);
}
```
snippet source | anchor

Add a new [controller](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/actions) that overrides `BaseSeqController`.


```cs
public class SeqController(SeqWriter writer) :
BaseSeqController(writer);
```
snippet source | anchor

##### Authorization/Authentication

Adding authorization and authentication can be done with an [AuthorizeAttribute](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/simple).


```cs
[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 ](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters#exception-filters).


```cs
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:


```js
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](https://github.com/structured-log/structured-log/) is a structured logging framework for JavaScript, inspired by Serilog.

In combination with [structured-log-seq-sink](https://github.com/Wedvich/structured-log-seq-sink) it can be used to write to Seq

To use this approach:

#### Include the libraries

Install both [structured-log npm](https://www.npmjs.com/package/structured-log) and [structured-log-seq-sink npm](https://www.npmjs.com/package/structured-log-seq-sink). Or include them from [jsDelivr](https://www.jsdelivr.com/):


```html

```
snippet source | anchor

#### Configure the log


```js
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


```js
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}`):


```js
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:


```js
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](https://thenounproject.com/term/robot/883226/) designed by [Maxim Kulikov](https://thenounproject.com/maxim221) from [The Noun Project](https://thenounproject.com).