Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/SeqCli/Config/Forwarder/SeqCliForwarderConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ class SeqCliForwarderConfig
public SeqCliForwarderStorageConfig Storage { get; set; } = new();
public SeqCliForwarderDiagnosticConfig Diagnostics { get; set; } = new();
public SeqCliForwarderApiConfig Api { get; set; } = new();
public bool UseApiKeyForwarding { get; set; }
}
8 changes: 4 additions & 4 deletions src/SeqCli/Config/KeyValueSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public static void Set(SeqCliConfig config, string key, string? value)

var steps = key.Split('.');
if (steps.Length < 2)
throw new ArgumentException("The format of the key is incorrect; run `seqcli config list` to view all keys.");
throw new ArgumentException("The format of the key is incorrect; run `seqcli config` to view all keys.");

object? receiver = config;
for (var i = 0; i < steps.Length - 1; ++i)
Expand All @@ -42,7 +42,7 @@ public static void Set(SeqCliConfig config, string key, string? value)
.SingleOrDefault(p => Camelize(GetUserFacingName(p)) == steps[i]);

if (nextStep == null)
throw new ArgumentException("The key could not be found; run `seqcli config list` to view all keys.");
throw new ArgumentException("The key could not be found; run `seqcli config` to view all keys.");

if (nextStep.PropertyType == typeof(Dictionary<string, SeqCliConnectionConfig>))
throw new NotSupportedException("Use `seqcli profile create` to configure connection profiles.");
Expand All @@ -57,10 +57,10 @@ public static void Set(SeqCliConfig config, string key, string? value)
// would be more robust.
var targetProperty = receiver.GetType().GetTypeInfo().DeclaredProperties
.Where(p => p is { CanRead: true, CanWrite: true } && p.GetMethod!.IsPublic && p.SetMethod!.IsPublic && !p.GetMethod.IsStatic)
.SingleOrDefault(p => Camelize(p.Name) == steps[^1]);
.SingleOrDefault(p => Camelize(GetUserFacingName(p)) == steps[^1]);

if (targetProperty == null)
throw new ArgumentException("The key could not be found; run `seqcli config list` to view all keys.");
throw new ArgumentException("The key could not be found; run `seqcli config` to view all keys.");

var targetValue = ChangeType(value, targetProperty.PropertyType);
targetProperty.SetValue(receiver, targetValue);
Expand Down
68 changes: 53 additions & 15 deletions src/SeqCli/Forwarder/Channel/ForwardingChannelMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Threading;
using System.Threading.Tasks;
using Seq.Api;
using SeqCli.Config;
using SeqCli.Forwarder.Filesystem.System;
using SeqCli.Forwarder.Storage;
using Serilog;
Expand All @@ -15,26 +16,28 @@ class ForwardingChannelMap
{
readonly string _bufferPath;
readonly SeqConnection _connection;
readonly SeqCliConfig _config;
readonly ForwardingChannel _defaultChannel;
readonly Lock _channelsSync = new();
readonly Dictionary<string, ForwardingChannel> _channels = new();
readonly CancellationTokenSource _shutdownTokenSource = new();
const string DefaultChannelName = "Default";

public ForwardingChannelMap(string bufferPath, SeqConnection connection, string? defaultApiKey)
public ForwardingChannelMap(string bufferPath, SeqConnection connection, SeqCliConfig config, string? seqCliApiKey)
{
_bufferPath = bufferPath;
_connection = connection;
_defaultChannel = OpenOrCreateChannel(defaultApiKey, "Default");
_config = config;
_defaultChannel = OpenOrCreateChannel(seqCliApiKey, DefaultChannelName);

// TODO, load other channels at start-up
ReopenApiKeyChannels();
}

ForwardingChannel OpenOrCreateChannel(string? apiKey, string name)
{
// TODO, when it's not the default, persist the API key and validate equality on reopen

var storePath = Path.Combine(_bufferPath, name);
var storePath = GetStorePath(name);
var store = new SystemStoreDirectory(storePath);

Log.Information("Opening local buffer in {StorePath}", storePath);

return new ForwardingChannel(
Expand All @@ -45,30 +48,65 @@ ForwardingChannel OpenOrCreateChannel(string? apiKey, string name)
apiKey,
_shutdownTokenSource.Token);
}

public ForwardingChannel Get(string? apiKey)
void ReopenApiKeyChannels()
{
if (string.IsNullOrWhiteSpace(apiKey))
if (_config.Forwarder.UseApiKeyForwarding)
{
return _defaultChannel;
foreach (var directoryPath in Directory.EnumerateDirectories(_bufferPath))
{
if (directoryPath.Equals(GetStorePath("Default"))) continue;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could use DefaultChannelName here?


var path = new SystemStoreDirectory(directoryPath);
var apiKey = path.ReadApiKey(_config);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the read fails, here, perhaps we should skip rather than fall through? Effects are going to be weird otherwise - e.g. the Add() call on 74 will fail on subsequent keys that also fail.


if (!string.IsNullOrEmpty(apiKey))
{
var created = OpenOrCreateChannel(apiKey, ApiKeyToName(apiKey));

lock (_channelsSync)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is only called in the constructor where there's no parallelism, it might be best to omit the lock statement here so the code doesn't give the impression of there being some.

Renaming the method to LoadApiKeyChannels or InitApiKeyChannels or OpenApiKeyChannels might communicate its role better?

{
_channels.Add(apiKey, created);
}
}
}
}

}

string GetStorePath(string name)
{
return Path.Combine(_bufferPath, name);
}

public ForwardingChannel GetSeqCliConnectionChannel()
{
return _defaultChannel;
}

public ForwardingChannel GetApiKeyChannel(string apiKey)
{
lock (_channelsSync)
{
if (_channels.TryGetValue(apiKey, out var channel))
{
return channel;
}

// Seq API keys begin with four identifying characters that aren't considered part of the
// confidential key. TODO: we could likely do better than this.
var name = apiKey[..4];
var created = OpenOrCreateChannel(apiKey, name);
var created = OpenOrCreateChannel(apiKey, ApiKeyToName(apiKey));
var store = new SystemStoreDirectory(GetStorePath(ApiKeyToName(apiKey)));
store.WriteApiKey(_config, apiKey);
_channels.Add(apiKey, created);
return created;
}
}

string ApiKeyToName(string apiKey)
{
// Seq API keys begin with four identifying characters that aren't considered part of the
// confidential key. TODO: we could likely do better than this.
return apiKey[..(Math.Min(apiKey.Length, 4))];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the next pass, if we fix/improve the directory naming scheme, we should include a min-length requirement and ignore payloads/keys below it. Seq's minimum is something like 16 chars IIRC.

}

public async Task StopAsync()
{
Log.Information("Flushing log buffers");
Expand Down
23 changes: 23 additions & 0 deletions src/SeqCli/Forwarder/Filesystem/System/SystemStoreDirectory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using SeqCli.Config;
using Serilog;

#if UNIX
using SeqCli.Forwarder.Filesystem.System.Unix;
Expand All @@ -34,6 +37,26 @@ public SystemStoreDirectory(string path)
if (!Directory.Exists(_directoryPath)) Directory.CreateDirectory(_directoryPath);
}

public void WriteApiKey(SeqCliConfig config, string apiKey)
{
File.WriteAllBytes(Path.Combine(_directoryPath, "api.key"), Encoding.UTF8.GetBytes(apiKey));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing the Encrypt() step here?

}

public string? ReadApiKey(SeqCliConfig config)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryReadApiKey?

{
string? apiKey = null;
try
{
var encrypted = File.ReadAllBytes(Path.Combine(_directoryPath, "api.key"));
apiKey = Encoding.UTF8.GetString(config.Encryption.DataProtector().Decrypt(encrypted));
}
catch (Exception exception)
{
Log.Warning(exception, "Could not read or decrypt api key");
}
return apiKey;
}

public override SystemStoreFile Create(string name)
{
var filePath = Path.Combine(_directoryPath, name);
Expand Down
2 changes: 1 addition & 1 deletion src/SeqCli/Forwarder/ForwarderModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public ForwarderModule(string bufferPath, SeqCliConfig config, SeqConnection con
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<ServerService>().SingleInstance();
builder.Register(_ => new ForwardingChannelMap(_bufferPath, _connection, _apiKey)).SingleInstance();
builder.Register(_ => new ForwardingChannelMap(_bufferPath, _connection, _config, _apiKey)).SingleInstance();

builder.RegisterType<ApiRootEndpoints>().As<IMapEndpoints>();
builder.RegisterType<IngestionEndpoints>().As<IMapEndpoints>();
Expand Down
21 changes: 19 additions & 2 deletions src/SeqCli/Forwarder/Web/Api/IngestionEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using System;
using System.Buffers;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading;
Expand All @@ -24,8 +25,10 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Net.Http.Headers;
using SeqCli.Api;
using SeqCli.Config;
using SeqCli.Forwarder.Channel;
using SeqCli.Forwarder.Diagnostics;
using Tavis.UriTemplates;
using JsonException = System.Text.Json.JsonException;

namespace SeqCli.Forwarder.Web.Api;
Expand All @@ -37,10 +40,12 @@ class IngestionEndpoints : IMapEndpoints
static readonly Encoding Utf8 = new UTF8Encoding(false);

readonly ForwardingChannelMap _forwardingChannels;
readonly SeqCliConfig _config;

public IngestionEndpoints(ForwardingChannelMap forwardingChannels)
public IngestionEndpoints(ForwardingChannelMap forwardingChannels, SeqCliConfig config)
{
_forwardingChannels = forwardingChannels;
_config = config;
}

public void MapEndpoints(WebApplication app)
Expand Down Expand Up @@ -93,10 +98,22 @@ static bool DefaultedBoolQuery(HttpRequest request, string queryParameterName)

async Task<ContentHttpResult> IngestCompactFormatAsync(HttpContext context)
{
var apiKey = GetApiKey(context.Request);
if (_config.Forwarder.UseApiKeyForwarding && string.IsNullOrEmpty(apiKey))
{
return TypedResults.Content(
"API key is required",
"text/plain",
Utf8,
StatusCodes.Status400BadRequest);
}

var cts = CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted);
cts.CancelAfter(TimeSpan.FromSeconds(5));

var log = _forwardingChannels.Get(GetApiKey(context.Request));
var log = _config.Forwarder.UseApiKeyForwarding
? _forwardingChannels.GetApiKeyChannel(apiKey!)
: _forwardingChannels.GetSeqCliConnectionChannel();

var payload = ArrayPool<byte>.Shared.Rent(1024 * 1024 * 10);
var writeHead = 0;
Expand Down