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
1 change: 0 additions & 1 deletion example/SeqTail/SeqTail.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
<PackageReference Include="Serilog.Formatting.Compact.Reader" Version="1.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="System.Reactive" Version="3.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net46' ">
Expand Down
7 changes: 3 additions & 4 deletions example/SignalCopy/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
using Seq.Api.Model.Signals;
using System.Collections.Generic;

namespace SeqQuery
namespace SignalCopy
{
class Program
{
Expand Down Expand Up @@ -76,10 +76,9 @@ static async Task Run(string src, string srcKey, string dst, string dstKey)

foreach (var signal in await srcConnection.Signals.ListAsync())
{
SignalEntity target;
if (dstSignals.TryGetValue(signal.Title, out target))
if (dstSignals.TryGetValue(signal.Title, out var target))
{
if (target.IsRestricted)
if (target.IsProtected)
{
Console.WriteLine($"Skipping restricted signal '{signal.Title}' ({target.Id})");
continue;
Expand Down
26 changes: 18 additions & 8 deletions src/Seq.Api/Client/SeqApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ 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.
const string SeqApiV5MediaType = "application/vnd.datalust.seq.v5+json";
const string SeqApiV6MediaType = "application/vnd.datalust.seq.v6+json";

readonly HttpClient _httpClient;
readonly CookieContainer _cookies = new CookieContainer();
Expand All @@ -36,14 +36,18 @@ public class SeqApiClient : IDisposable
Converters = { new StringEnumConverter(), new LinkCollectionConverter() }
});

public SeqApiClient(string serverUrl, string apiKey = null)
public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCredentials = true)
{
ServerUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl));

if (!string.IsNullOrEmpty(apiKey))
_apiKey = apiKey;

var handler = new HttpClientHandler { CookieContainer = _cookies, UseDefaultCredentials = true };
var handler = new HttpClientHandler
{
CookieContainer = _cookies,
UseDefaultCredentials = useDefaultCredentials
};

var baseAddress = serverUrl;
if (!baseAddress.EndsWith("/"))
Expand Down Expand Up @@ -103,6 +107,13 @@ public async Task<string> PostReadStringAsync<TEntity>(ILinked entity, string li
return await new StreamReader(stream).ReadToEndAsync();
}

public async Task<Stream> PostReadStreamAsync<TEntity>(ILinked entity, string link, TEntity content, IDictionary<string, object> parameters = null)
{
var linkUri = ResolveLink(entity, link, parameters);
var request = new HttpRequestMessage(HttpMethod.Post, linkUri) { Content = MakeJsonContent(content) };
return await HttpSendAsync(request).ConfigureAwait(false);
}

public async Task PutAsync<TEntity>(ILinked entity, string link, TEntity content, IDictionary<string, object> parameters = null)
{
var linkUri = ResolveLink(entity, link, parameters);
Expand Down Expand Up @@ -170,7 +181,7 @@ async Task<Stream> HttpSendAsync(HttpRequestMessage request)
if (_apiKey != null)
request.Headers.Add("X-Seq-ApiKey", _apiKey);

request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(SeqApiV5MediaType));
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(SeqApiV6MediaType));

var response = await _httpClient.SendAsync(request).ConfigureAwait(false);
var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
Expand All @@ -186,11 +197,10 @@ async Task<Stream> HttpSendAsync(HttpRequestMessage request)
// ReSharper disable once EmptyGeneralCatchClause
catch { }

object error;
if (payload != null && payload.TryGetValue("Error", out error) && error != null)
throw new SeqApiException($"{(int)response.StatusCode} - {error}");
if (payload != null && payload.TryGetValue("Error", out var error) && error != null)
throw new SeqApiException($"{(int)response.StatusCode} - {error}", response.StatusCode);

throw new SeqApiException($"The Seq request failed ({(int)response.StatusCode}).");
throw new SeqApiException($"The Seq request failed ({(int)response.StatusCode}).", response.StatusCode);
}

HttpContent MakeJsonContent(object content)
Expand Down
17 changes: 16 additions & 1 deletion src/Seq.Api/Client/SeqApiException.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
using System;
using System.Net;

namespace Seq.Api.Client
{
/// <summary>
/// Thrown when an action cannot be performed.
/// </summary>
public class SeqApiException : Exception
{
public SeqApiException(string message)
/// <summary>
/// Construct a <see cref="SeqApiException"/> with the given message and status code.
/// </summary>
/// <param name="message">A message describing the error.</param>
/// <param name="statusCode">The corresponding status code returned from Seq, if available.</param>
public SeqApiException(string message, HttpStatusCode? statusCode)
: base(message)
{
StatusCode = statusCode;
}

/// <summary>
/// The status code returned from Seq, if available.
/// </summary>
public HttpStatusCode? StatusCode { get; }
}
}
18 changes: 7 additions & 11 deletions src/Seq.Api/Model/Inputs/ApiKeyEntity.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
using System.Collections.Generic;
using Seq.Api.Model.LogEvents;
using Seq.Api.Model.Security;
using Seq.Api.Model.Signals;

namespace Seq.Api.Model.Inputs
{
public class ApiKeyEntity : Entity
{
public ApiKeyEntity()
{
AppliedProperties = new List<InputAppliedPropertyPart>();
InputFilter = new SignalFilterPart();
Metrics = new ApiKeyMetricsPart();
}

public string Title { get; set; }
public string Token { get; set; }
public List<InputAppliedPropertyPart> AppliedProperties { get; set; }
public SignalFilterPart InputFilter { get; set; }
public bool CanActAsPrincipal { get; set; }
public string TokenPrefix { get; set; }
public List<InputAppliedPropertyPart> AppliedProperties { get; set; } = new List<InputAppliedPropertyPart>();
public SignalFilterPart InputFilter { get; set; } = new SignalFilterPart();
public LogEventLevel? MinimumLevel { get; set; }
public bool UseServerTimestamps { get; set; }
public int InfluxPerMinute { get; set; }
public int ArrivalsPerMinute { get; set; }
public ApiKeyMetricsPart Metrics { get; set; }
public ApiKeyMetricsPart Metrics { get; set; } = new ApiKeyMetricsPart();
public bool IsDefault { get; set; }
public string OwnerId { get; set; }
public HashSet<Permission> AssignedPermissions { get; set; } = new HashSet<Permission>();
}
}
6 changes: 6 additions & 0 deletions src/Seq.Api/Model/Permalinks/PermalinkEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ namespace Seq.Api.Model.Permalinks
{
public class PermalinkEntity : Entity
{
/// <summary>
/// When retrieving an event that _may_ be permalinked (backwards compatibility),
/// this hint is given by specifiying `permalinkId=unknown` in the API call.
/// </summary>
public const string UnknownId = "unknown";

public string OwnerId { get; set; }
public string EventId { get; set; }
public DateTime CreatedUtc { get; set; }
Expand Down
41 changes: 41 additions & 0 deletions src/Seq.Api/Model/Security/Permission.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Seq.Api.Model.Security
{
/// <summary>
/// A permission is an access right 1) held by a principal, and 2) demanded by an endpoint. Permissions
/// may be broad, such as the permission to modify administrative settings, or narrow (e.g. currently the
/// permission to ingest events).
/// </summary>
public enum Permission
{
/// <summary>
/// A sentinel value to detect uninitialized permissions.
/// </summary>
Undefined,

/// <summary>
/// Access to publicly-visible assets - the API root/metadata, HTML, JavaScript, CSS, information necessary
/// to initiate the login process, and so-on.
/// </summary>
Public,

/// <summary>
/// Add events to the event store.
/// </summary>
Ingest,

/// <summary>
/// Query events, dashboards, signals, app instances.
/// </summary>
Read,

/// <summary>
/// Write-access to signals, alerts, preferences etc.
/// </summary>
Write,

/// <summary>
/// Access to administrative features of Seq, management of other users, app installation, backups.
/// </summary>
Setup,
}
}
10 changes: 10 additions & 0 deletions src/Seq.Api/Model/Security/RoleEntity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Collections.Generic;

namespace Seq.Api.Model.Security
{
public class RoleEntity : Entity
{
public string Title { get; set; }
public HashSet<Permission> Permissions { get; set; } = new HashSet<Permission>();
}
}
8 changes: 8 additions & 0 deletions src/Seq.Api/Model/Security/WellKnownRole.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Seq.Api.Model.Security
{
public static class WellKnownRole
{
public const string AdministratorRoleId = "role-administrator";
public const string UserRoleId = "role-user";
}
}
7 changes: 7 additions & 0 deletions src/Seq.Api/Model/Shared/ErrorPart.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Seq.Api.Model.Shared
{
public class ErrorPart
{
public string Error { get; set; }
}
}
9 changes: 7 additions & 2 deletions src/Seq.Api/Model/Signals/SignalEntity.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;

namespace Seq.Api.Model.Signals
{
Expand All @@ -21,7 +22,11 @@ public SignalEntity()

public bool IsWatched { get; set; }

public bool IsRestricted { get; set; }
[Obsolete("This member has been renamed `IsProtected` to better reflect its purpose.")]
// ReSharper disable once UnusedMember.Global
public bool? IsRestricted { get; set; }

public bool IsProtected { get; set; }

public SignalGrouping Grouping { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion src/Seq.Api/Model/Users/UserEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ public class UserEntity : Entity
public string DisplayName { get; set; }
public string EmailAddress { get; set; }
public Dictionary<string, object> Preferences { get; set; }
public bool IsAdministrator { get; set; }
public string NewPassword { get; set; }
public SignalFilterPart ViewFilter { get; set; }
public bool MustChangePassword { get; set; }
public HashSet<string> RoleIds { get; set; } = new HashSet<string>();
}
}
7 changes: 4 additions & 3 deletions src/Seq.Api/ResourceGroups/ApiKeysResourceGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ public async Task<ApiKeyEntity> FindAsync(string id)
return await GroupGetAsync<ApiKeyEntity>("Item", new Dictionary<string, object> { { "id", id } }).ConfigureAwait(false);
}

public async Task<List<ApiKeyEntity>> ListAsync()
public async Task<List<ApiKeyEntity>> ListAsync(string ownerId = null)
{
return await GroupListAsync<ApiKeyEntity>("Items").ConfigureAwait(false);
var parameters = new Dictionary<string, object> { { "ownerId", ownerId } };
return await GroupListAsync<ApiKeyEntity>("Items", parameters).ConfigureAwait(false);
}

public async Task<ApiKeyEntity> TemplateAsync()
Expand All @@ -30,7 +31,7 @@ public async Task<ApiKeyEntity> TemplateAsync()

public async Task<ApiKeyEntity> AddAsync(ApiKeyEntity entity)
{
return await Client.PostAsync<ApiKeyEntity, ApiKeyEntity>(entity, "Create", entity).ConfigureAwait(false);
return await GroupCreateAsync<ApiKeyEntity, ApiKeyEntity>(entity).ConfigureAwait(false);
}

public async Task RemoveAsync(ApiKeyEntity entity)
Expand Down
27 changes: 27 additions & 0 deletions src/Seq.Api/ResourceGroups/ApiResourceGroup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Seq.Api.Client;
using Seq.Api.Model;
Expand Down Expand Up @@ -55,6 +56,12 @@ protected async Task<string> GroupPostReadStringAsync<TEntity>(string link, TEnt
return await Client.PostReadStringAsync(group, link, content, parameters).ConfigureAwait(false);
}

protected async Task<Stream> GroupPostReadBytesAsync<TEntity>(string link, TEntity content, IDictionary<string, object> parameters = null)
{
var group = await LoadGroupAsync().ConfigureAwait(false);
return await Client.PostReadStreamAsync(group, link, content, parameters).ConfigureAwait(false);
}

protected async Task<TResponse> GroupPostAsync<TEntity, TResponse>(string link, TEntity content, IDictionary<string, object> parameters = null)
{
var group = await LoadGroupAsync().ConfigureAwait(false);
Expand Down Expand Up @@ -83,5 +90,25 @@ protected string GetLink<TEntity>(TEntity entity, string link, string orElse) wh
{
return entity.Links.ContainsKey(link) ? link : orElse;
}

protected async Task<TResponse> GroupCreateAsync<TEntity, TResponse>(TEntity entity,
IDictionary<string, object> parameters = null) where TEntity : ILinked
{
ILinked resource;
string link;

if (entity.Links.ContainsKey("Create"))
{
resource = entity;
link = "Create";
}
else
{
resource = await LoadGroupAsync().ConfigureAwait(false);
link = "Items";
}

return await Client.PostAsync<TEntity, TResponse>(resource, link, entity, parameters).ConfigureAwait(false);
}
}
}
6 changes: 6 additions & 0 deletions src/Seq.Api/ResourceGroups/BackupsResourceGroup.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Seq.Api.Model.Backups;

Expand All @@ -22,5 +23,10 @@ public async Task<List<BackupEntity>> ListAsync()
{
return await GroupListAsync<BackupEntity>("Items").ConfigureAwait(false);
}

public async Task<Stream> DownloadImmediateAsync()
{
return await GroupPostReadBytesAsync("Immediate", new object()).ConfigureAwait(false);
}
}
}
11 changes: 3 additions & 8 deletions src/Seq.Api/ResourceGroups/DataResourceGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,16 @@ static void MakeParameters(
};

if (rangeStartUtc != null)
{
parameters.Add(nameof(rangeStartUtc), rangeStartUtc);
}

if (rangeEndUtc != null)
{
parameters.Add(nameof(rangeEndUtc), rangeEndUtc.Value);
}

if (signal != null)
{
parameters.Add(nameof(signal), signal.ToString());
}

if (timeout != null)
{
parameters.Add("timeoutMS", timeout.Value.TotalMilliseconds.ToString("0"));
}

body = unsavedSignal ?? new SignalEntity();
}
Expand Down
Loading