Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion example/SeqQuery/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ static void Main(string[] args)
var server = arguments["<server>"].ToString();
var query = Normalize(arguments["<query>"]);
var apiKey = Normalize(arguments["--apikey"]);
var @from = Normalize(arguments["--from"]);
var from = Normalize(arguments["--from"]);
var to = Normalize(arguments["--to"]);

await Run(server, apiKey, query, from, to);
Expand Down
6 changes: 3 additions & 3 deletions example/SeqTail/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ static void Main(string[] args)
Console.WriteLine("Tailing, press Ctrl+C to exit.");
Console.CancelKeyPress += (s,a) => cancel.Cancel();

var run = Task.Run(() => Run(server, apiKey, filter, cancel));
var run = Task.Run(() => Run(server, apiKey, filter, cancel), cancel.Token);

run.GetAwaiter().GetResult();
}
Expand Down Expand Up @@ -78,8 +78,8 @@ static async Task Run(string server, string apiKey, string filter, CancellationT
using (var stream = await connection.Events.StreamAsync<JObject>(filter: strict))
{
var subscription = stream
.Select(jObject => LogEventReader.ReadFromJObject(jObject))
.Subscribe(evt => Log.Write(evt), () => cancel.Cancel());
.Select(LogEventReader.ReadFromJObject)
.Subscribe(Log.Write, cancel.Cancel);

cancel.Token.WaitHandle.WaitOne();
subscription.Dispose();
Expand Down
6 changes: 5 additions & 1 deletion seq-api.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AD/@EntryIndexedValue">AD</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=apikey/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=dstkey/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=permalinked/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=permalinks/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Permalinks/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Reentrant/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=srckey/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Workspaces/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>
11 changes: 5 additions & 6 deletions src/Seq.Api/Client/SeqApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ public class SeqApiClient : IDisposable

// Future versions of Seq may not completely support v1 features, however
// providing this as an Accept header will ensure what compatibility is available
// can be utilised.
// can be utilized.
const string SeqApiV7MediaType = "application/vnd.datalust.seq.v7+json";

readonly HttpClient _httpClient;
readonly CookieContainer _cookies = new CookieContainer();
readonly JsonSerializer _serializer = JsonSerializer.Create(
new JsonSerializerSettings
Expand All @@ -53,12 +52,12 @@ public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCrede
if (!baseAddress.EndsWith("/"))
baseAddress += "/";

_httpClient = new HttpClient(handler) { BaseAddress = new Uri(baseAddress) };
HttpClient = new HttpClient(handler) { BaseAddress = new Uri(baseAddress) };
}

public string ServerUrl { get; }

public HttpClient HttpClient => _httpClient;
public HttpClient HttpClient { get; }

public Task<RootEntity> GetRootAsync(CancellationToken cancellationToken = default)
{
Expand Down Expand Up @@ -183,7 +182,7 @@ async Task<Stream> HttpSendAsync(HttpRequestMessage request, CancellationToken c

request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(SeqApiV7MediaType));

var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);

if (response.IsSuccessStatusCode)
Expand Down Expand Up @@ -239,7 +238,7 @@ static string ResolveLink(ILinked entity, string link, IDictionary<string, objec

public void Dispose()
{
_httpClient.Dispose();
HttpClient.Dispose();
}
}
}
90 changes: 69 additions & 21 deletions src/Seq.Api/SeqConnection.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Seq.Api.Client;
Expand All @@ -11,16 +12,17 @@ namespace Seq.Api
{
public class SeqConnection : ISeqConnection
{
readonly ConcurrentDictionary<string, Task<ResourceGroup>> _resourceGroups = new ConcurrentDictionary<string, Task<ResourceGroup>>();
readonly Lazy<Task<RootEntity>> _root;
readonly object _sync = new object();
readonly Dictionary<string, Task<ResourceGroup>> _resourceGroups = new Dictionary<string, Task<ResourceGroup>>();
Task<RootEntity> _root;

public SeqConnection(string serverUrl, string apiKey = null, bool useDefaultCredentials = true)
{
if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl));
Client = new SeqApiClient(serverUrl, apiKey, useDefaultCredentials);

_root = new Lazy<Task<RootEntity>>(() => Client.GetRootAsync());
}

public SeqApiClient Client { get; }

public ApiKeysResourceGroup ApiKeys => new ApiKeysResourceGroup(this);

Expand Down Expand Up @@ -60,27 +62,73 @@ public SeqConnection(string serverUrl, string apiKey = null, bool useDefaultCred

public WorkspacesResourceGroup Workspaces => new WorkspacesResourceGroup(this);

public Task<ResourceGroup> LoadResourceGroupAsync(string name, CancellationToken cancellationToken = default)
public async Task EnsureConnected(TimeSpan timeout)
{
// Initially, we want to put an incomplete task into the cache so that any concurrent attempts to load the
// same resource group will wait on the same pending call.
var cached = _resourceGroups.GetOrAdd(name, s => ResourceGroupFactory(s, cancellationToken));

if (!cached.IsFaulted && !cached.IsCanceled)
return cached;

// If the cached task failed (ideally a rare situation), clobber it and return a new task (less worried about
// overlapping/concurrent calls on this path).
return _resourceGroups.AddOrUpdate(name,
s => ResourceGroupFactory(s, cancellationToken),
(s, _) => ResourceGroupFactory(s, cancellationToken));
var started = DateTime.UtcNow;
// Fractional milliseconds are lost here, but that's fine.
var wait = TimeSpan.FromMilliseconds(Math.Min(100, timeout.TotalMilliseconds));
var deadline = started.Add(timeout);
while (!await ConnectAsync(DateTime.UtcNow > deadline))
{
await Task.Delay(wait);
}
}

async Task<ResourceGroup> ResourceGroupFactory(string name, CancellationToken cancellationToken = default)
async Task<bool> ConnectAsync(bool throwOnFailure)
{
return await Client.GetAsync<ResourceGroup>(await _root.Value, name + "Resources", cancellationToken: cancellationToken).ConfigureAwait(false);
HttpStatusCode statusCode;

try
{
statusCode = (await Client.HttpClient.GetAsync("api")).StatusCode;
}
catch
{
if (throwOnFailure)
throw;

return false;
}

if (statusCode == HttpStatusCode.OK)
return true;

if (!throwOnFailure)
return false;

throw new WebException($"Could not connect to the Seq API endpoint: {(int)statusCode}/{statusCode}.");
}

public SeqApiClient Client { get; }
public async Task<ResourceGroup> LoadResourceGroupAsync(string name, CancellationToken cancellationToken = default)
{
Task<RootEntity> loadRoot;
lock (_sync)
{
if (_root == null || _root.IsFaulted || _root.IsCanceled)
_root = Client.GetRootAsync(cancellationToken);

loadRoot = _root;
}

var rootEntity = await loadRoot.ConfigureAwait(false);

Task<ResourceGroup> loadGroup;
lock (_sync)
{
// ReSharper disable once InvertIf
if (!_resourceGroups.TryGetValue(name, out loadGroup) || loadGroup.IsFaulted || loadGroup.IsCanceled)
{
loadGroup = GetResourceGroup(rootEntity, name, cancellationToken);
_resourceGroups.Add(name, loadGroup);
}
}

return await loadGroup.ConfigureAwait(false);
}

async Task<ResourceGroup> GetResourceGroup(RootEntity root, string name, CancellationToken cancellationToken = default)
{
return await Client.GetAsync<ResourceGroup>(root, name + "Resources", cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
}