From 8f1d74ff831da9c24ebe7fbd1cff101f616f5470 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 14 Feb 2025 11:54:15 +1000 Subject: [PATCH] Update to the Seq 2025.1 API --- src/Seq.Api/Client/SeqApiClient.cs | 17 +-- .../Model/Alerting/AlertOccurrencePart.cs | 1 - .../Alerting/AlertOccurrenceRangePart.cs | 3 +- .../Model/Alerting/AlertStateEntity.cs | 22 +--- .../Model/Apps/AppPackageIdentityPart.cs | 37 ++++++ src/Seq.Api/Model/Apps/AppPackagePart.cs | 17 +-- .../Model/Cluster/ClusterNodeEntity.cs | 28 +++-- .../{NodeRole.cs => HealthCheckResultPart.cs} | 19 ++-- src/Seq.Api/Model/Cluster/HealthStatus.cs | 39 +++++++ src/Seq.Api/Model/Dashboarding/ChartPart.cs | 9 +- .../MeasurementDisplayStylePart.cs | 5 + .../Dashboarding/MeasurementDisplayType.cs | 7 +- src/Seq.Api/Model/Data/ColumnMetadataPart.cs | 22 ++++ src/Seq.Api/Model/Data/QueryResultPart.cs | 11 ++ .../Storage/StorageConsumptionPart.cs | 7 +- src/Seq.Api/Model/Inputs/InputSettingsPart.cs | 1 - src/Seq.Api/Model/License/LicenseEntity.cs | 11 +- src/Seq.Api/Model/Settings/SettingName.cs | 12 ++ src/Seq.Api/Model/Users/UserEntity.cs | 1 - .../ResourceGroups/AlertStateResourceGroup.cs | 1 - .../ResourceGroups/AppsResourceGroup.cs | 22 ++-- .../ResourceGroups/BackupsResourceGroup.cs | 2 +- .../ClusterNodesResourceGroup.cs | 70 ------------ .../ResourceGroups/ClusterResourceGroup.cs | 107 ++++++++++++++++++ .../DiagnosticsResourceGroup.cs | 16 +-- .../ResourceGroups/EventsResourceGroup.cs | 20 +++- .../RunningTasksResourceGroup.cs | 1 - .../ResourceGroups/UsersResourceGroup.cs | 11 ++ src/Seq.Api/Seq.Api.csproj | 2 +- src/Seq.Api/SeqConnection.cs | 82 ++++++++------ 30 files changed, 401 insertions(+), 202 deletions(-) create mode 100644 src/Seq.Api/Model/Apps/AppPackageIdentityPart.cs rename src/Seq.Api/Model/Cluster/{NodeRole.cs => HealthCheckResultPart.cs} (66%) create mode 100644 src/Seq.Api/Model/Cluster/HealthStatus.cs create mode 100644 src/Seq.Api/Model/Data/ColumnMetadataPart.cs delete mode 100644 src/Seq.Api/ResourceGroups/ClusterNodesResourceGroup.cs create mode 100644 src/Seq.Api/ResourceGroups/ClusterResourceGroup.cs diff --git a/src/Seq.Api/Client/SeqApiClient.cs b/src/Seq.Api/Client/SeqApiClient.cs index c39cc7a..b2c4d1b 100644 --- a/src/Seq.Api/Client/SeqApiClient.cs +++ b/src/Seq.Api/Client/SeqApiClient.cs @@ -29,6 +29,7 @@ using System.Threading; using Seq.Api.Streams; using System.Net.WebSockets; +using Seq.Api.Model.Shared; namespace Seq.Api.Client { @@ -42,7 +43,7 @@ public sealed class SeqApiClient : IDisposable // Future versions of Seq may not completely support vN-1 features, however // providing this as an Accept header will ensure what compatibility is available // can be utilized. - const string SeqApiV10MediaType = "application/vnd.datalust.seq.v10+json"; + const string SeqApiV11MediaType = "application/vnd.datalust.seq.v11+json"; readonly CookieContainer _cookies = new(); readonly JsonSerializer _serializer = JsonSerializer.Create( @@ -111,7 +112,7 @@ public SeqApiClient(string serverUrl, string apiKey = null, Func HttpGetStringAsync(string url, CancellationToken cancellation async Task HttpSendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + // ReSharper disable once MethodSupportsCancellation var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); if (response.IsSuccessStatusCode) return stream; - Dictionary payload = null; + ErrorPart error = null; try { - payload = _serializer.Deserialize>(new JsonTextReader(new StreamReader(stream))); + error = _serializer.Deserialize(new JsonTextReader(new StreamReader(stream))); } // ReSharper disable once EmptyGeneralCatchClause catch { } - if (payload != null && payload.TryGetValue("Error", out var error) && error != null) - throw new SeqApiException($"{(int)response.StatusCode} - {error}", response.StatusCode); + var exceptionMessage = $"The Seq request failed ({(int)response.StatusCode}/{response.StatusCode})."; + if (error?.Error != null) + exceptionMessage += $" {error.Error}"; - throw new SeqApiException($"The Seq request failed ({(int)response.StatusCode}/{response.StatusCode}).", response.StatusCode); + throw new SeqApiException(exceptionMessage, response.StatusCode); } HttpContent MakeJsonContent(object content) diff --git a/src/Seq.Api/Model/Alerting/AlertOccurrencePart.cs b/src/Seq.Api/Model/Alerting/AlertOccurrencePart.cs index 7adfa5a..8104e3d 100644 --- a/src/Seq.Api/Model/Alerting/AlertOccurrencePart.cs +++ b/src/Seq.Api/Model/Alerting/AlertOccurrencePart.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Seq.Api.Model.AppInstances; using Seq.Api.Model.LogEvents; using Seq.Api.Model.Shared; diff --git a/src/Seq.Api/Model/Alerting/AlertOccurrenceRangePart.cs b/src/Seq.Api/Model/Alerting/AlertOccurrenceRangePart.cs index 7a9116c..27bb4e6 100644 --- a/src/Seq.Api/Model/Alerting/AlertOccurrenceRangePart.cs +++ b/src/Seq.Api/Model/Alerting/AlertOccurrenceRangePart.cs @@ -1,5 +1,4 @@ -using Seq.Api.Model.LogEvents; -using Seq.Api.Model.Shared; +using Seq.Api.Model.Shared; namespace Seq.Api.Model.Alerting { diff --git a/src/Seq.Api/Model/Alerting/AlertStateEntity.cs b/src/Seq.Api/Model/Alerting/AlertStateEntity.cs index b788d9f..a186962 100644 --- a/src/Seq.Api/Model/Alerting/AlertStateEntity.cs +++ b/src/Seq.Api/Model/Alerting/AlertStateEntity.cs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Collections.Generic; +using Seq.Api.Model.LogEvents; namespace Seq.Api.Model.Alerting { @@ -41,25 +41,15 @@ public class AlertStateEntity : Entity /// The ids of app instances that receive notifications when the alert is triggered. /// public List NotificationAppInstanceIds { get; set; } - - /// - /// The time at which the alert was last checked. Not preserved across server restarts. - /// - public DateTime? LastCheck { get; set; } - - /// - /// The time at which the alert last triggered a notification. Not preserved across server restarts. - /// - public DateTime? LastNotification { get; set; } /// - /// The time until which no further notifications will be sent by the alert. + /// A level indicating the severity or priority of the alert. /// - public DateTime? SuppressedUntil { get; set; } - + public LogEventLevel NotificationLevel { get; set; } + /// - /// true if the alert is in the failing state; otherwise, false. + /// Any recent activity for the alert. /// - public bool IsFailing { get; set; } + public AlertActivityPart Activity { get; set; } } } diff --git a/src/Seq.Api/Model/Apps/AppPackageIdentityPart.cs b/src/Seq.Api/Model/Apps/AppPackageIdentityPart.cs new file mode 100644 index 0000000..25077aa --- /dev/null +++ b/src/Seq.Api/Model/Apps/AppPackageIdentityPart.cs @@ -0,0 +1,37 @@ +// Copyright © Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Seq.Api.Model.Apps +{ + /// + /// The data required to identify a NuGet package version. + /// + public class AppPackageIdentityPart + { + /// + /// The id of the from which the package was installed. + /// + public string NuGetFeedId { get; set; } + + /// + /// The package id, for example Seq.Input.HealthCheck. + /// + public string PackageId { get; set; } + + /// + /// The version of the package. + /// + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/Seq.Api/Model/Apps/AppPackagePart.cs b/src/Seq.Api/Model/Apps/AppPackagePart.cs index 8cd87ee..1b68097 100644 --- a/src/Seq.Api/Model/Apps/AppPackagePart.cs +++ b/src/Seq.Api/Model/Apps/AppPackagePart.cs @@ -19,23 +19,8 @@ namespace Seq.Api.Model.Apps /// /// Describes a NuGet package containing executable app components. /// - public class AppPackagePart + public class AppPackagePart : AppPackageIdentityPart { - /// - /// The id of the from which the package was installed. - /// - public string NuGetFeedId { get; set; } - - /// - /// The package id, for example Seq.Input.HealthCheck. - /// - public string PackageId { get; set; } - - /// - /// The version of the package. - /// - public string Version { get; set; } - /// /// Package authorship information. /// diff --git a/src/Seq.Api/Model/Cluster/ClusterNodeEntity.cs b/src/Seq.Api/Model/Cluster/ClusterNodeEntity.cs index db17fa3..c31e8d5 100644 --- a/src/Seq.Api/Model/Cluster/ClusterNodeEntity.cs +++ b/src/Seq.Api/Model/Cluster/ClusterNodeEntity.cs @@ -20,35 +20,40 @@ namespace Seq.Api.Model.Cluster public class ClusterNodeEntity : Entity { /// - /// The role the node is currently acting in. + /// An informational name associated with the node. /// - public NodeRole Role { get; set; } + public string Name { get; set; } /// - /// An informational name associated with the node. + /// The address the node will serve intra-cluster traffic on. /// - public string Name { get; set; } - + public string ClusterListenUri { get; set; } + /// - /// An informational representation of the storage generation committed to the node. + /// The address the node will serve regular API requests on. /// - public string Generation { get; set; } + public string InternalListenUri { get; set; } /// /// Whether any writes have occurred since the node's last completed sync. /// - public bool? IsUpToDate { get; set; } + public bool IsUpToDate { get; set; } /// /// The time since the node's last completed sync operation. /// - public double? MillisecondsSinceLastSync { get; set; } + public double? DataAgeMilliseconds { get; set; } /// /// The time since the follower's active sync was started. /// public double? MillisecondsSinceActiveSync { get; set; } + /// + /// The time since the follower's last heartbeat. + /// + public double? MillisecondsSinceLastHeartbeat { get; set; } + /// /// The total number of operations in the active sync. /// @@ -64,5 +69,10 @@ public class ClusterNodeEntity : Entity /// information about the node is available. /// public string StateDescription { get; set; } + + /// + /// Whether the node is currently leading the cluster. + /// + public bool IsLeading { get; set; } } } diff --git a/src/Seq.Api/Model/Cluster/NodeRole.cs b/src/Seq.Api/Model/Cluster/HealthCheckResultPart.cs similarity index 66% rename from src/Seq.Api/Model/Cluster/NodeRole.cs rename to src/Seq.Api/Model/Cluster/HealthCheckResultPart.cs index 0e753e2..fef535b 100644 --- a/src/Seq.Api/Model/Cluster/NodeRole.cs +++ b/src/Seq.Api/Model/Cluster/HealthCheckResultPart.cs @@ -15,23 +15,18 @@ namespace Seq.Api.Model.Cluster { /// - /// The role a node is acting in within a cluster of connected Seq instances. + /// A result reported from a health check endpoint. /// - public enum NodeRole + public class HealthCheckResultPart { /// - /// The node is not part of a cluster. + /// The status reported by the endpoint. /// - Standalone, - - /// - /// The node is a replica, following the state of a leader node. - /// - Follower, - + public HealthStatus Status { get; set; } + /// - /// The node is a replication leader. + /// An informational description of the reported status. /// - Leader + public string Description { get; set; } } } \ No newline at end of file diff --git a/src/Seq.Api/Model/Cluster/HealthStatus.cs b/src/Seq.Api/Model/Cluster/HealthStatus.cs new file mode 100644 index 0000000..61540e1 --- /dev/null +++ b/src/Seq.Api/Model/Cluster/HealthStatus.cs @@ -0,0 +1,39 @@ +// Copyright Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Seq.Api.Model.Cluster +{ + /// + /// A status value returned from a health check endpoint. + /// + /// Note that HTTP status code values returned from health checks should be inspected prior to + /// reading status information from the health check response payload. + public enum HealthStatus + { + /// + /// The target is healthy. + /// + Healthy, + + /// + /// The target is functioning in a degraded state; attention is required. + /// + Degraded, + + /// + /// The target is unhealthy. + /// + Unhealthy + } +} diff --git a/src/Seq.Api/Model/Dashboarding/ChartPart.cs b/src/Seq.Api/Model/Dashboarding/ChartPart.cs index afbc8aa..9ecc851 100644 --- a/src/Seq.Api/Model/Dashboarding/ChartPart.cs +++ b/src/Seq.Api/Model/Dashboarding/ChartPart.cs @@ -41,11 +41,16 @@ public class ChartPart /// The individual queries making up the chart. In most instances, only one query is currently supported /// here. /// - public List Queries { get; set; } = new List(); + public List Queries { get; set; } = new(); /// /// How the chart will appear on the dashboard. /// - public ChartDisplayStylePart DisplayStyle { get; set; } = new ChartDisplayStylePart(); + public ChartDisplayStylePart DisplayStyle { get; set; } = new(); + + /// + /// A short summary of the chart contents. + /// + public string Description { get; set; } } } diff --git a/src/Seq.Api/Model/Dashboarding/MeasurementDisplayStylePart.cs b/src/Seq.Api/Model/Dashboarding/MeasurementDisplayStylePart.cs index 48f703c..80d8d99 100644 --- a/src/Seq.Api/Model/Dashboarding/MeasurementDisplayStylePart.cs +++ b/src/Seq.Api/Model/Dashboarding/MeasurementDisplayStylePart.cs @@ -38,6 +38,11 @@ public class MeasurementDisplayStylePart /// For bar chart measurement display types, whether the sum of all bars will be shown as an overlay. /// public bool BarOverlaySum { get; set; } + + /// + /// For heatmap measurement display types, whether the scale is logarithmic. + /// + public bool UseLogarithmicScale { get; set; } /// /// For measurement display types that include a legend, whether the legend will be shown. diff --git a/src/Seq.Api/Model/Dashboarding/MeasurementDisplayType.cs b/src/Seq.Api/Model/Dashboarding/MeasurementDisplayType.cs index 6a83aeb..dc0e2be 100644 --- a/src/Seq.Api/Model/Dashboarding/MeasurementDisplayType.cs +++ b/src/Seq.Api/Model/Dashboarding/MeasurementDisplayType.cs @@ -47,6 +47,11 @@ public enum MeasurementDisplayType /// /// A table of raw data values. /// - Table + Table, + + /// + /// A heatmap chart. + /// + Heatmap, } } diff --git a/src/Seq.Api/Model/Data/ColumnMetadataPart.cs b/src/Seq.Api/Model/Data/ColumnMetadataPart.cs new file mode 100644 index 0000000..b21d3ff --- /dev/null +++ b/src/Seq.Api/Model/Data/ColumnMetadataPart.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Seq.Api.Model.Data +{ + /// + /// The metadata that can be reported for a column. + /// + public class ColumnMetadataPart + { + /// + /// The error value for a column that is a `Bucket` expression. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public decimal? BucketErrorParameter { get; set; } + + /// + /// The time grouping in ticks. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public decimal? IntervalTicks { get; set; } + } +} \ No newline at end of file diff --git a/src/Seq.Api/Model/Data/QueryResultPart.cs b/src/Seq.Api/Model/Data/QueryResultPart.cs index 2007a80..cbd14ea 100644 --- a/src/Seq.Api/Model/Data/QueryResultPart.cs +++ b/src/Seq.Api/Model/Data/QueryResultPart.cs @@ -34,6 +34,17 @@ public class QueryResultPart /// public string[] Columns { get; set; } + /// + /// Metadata, grouped by column. + /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public Dictionary ColumnMetadata { get; set; } + + /// + /// Metadata for the time grouping column. + /// + public ColumnMetadataPart TimeColumnMetadata { get; set; } + /// /// Result rows. /// diff --git a/src/Seq.Api/Model/Diagnostics/Storage/StorageConsumptionPart.cs b/src/Seq.Api/Model/Diagnostics/Storage/StorageConsumptionPart.cs index 437f9dd..04955b5 100644 --- a/src/Seq.Api/Model/Diagnostics/Storage/StorageConsumptionPart.cs +++ b/src/Seq.Api/Model/Diagnostics/Storage/StorageConsumptionPart.cs @@ -35,7 +35,12 @@ public class StorageConsumptionPart /// /// The duration of the timestamp interval covered by each result. /// - public uint IntervalMinutes { get; set; } + public uint IntervalSeconds { get; set; } + + /// + /// The file interval of the underlying storage. + /// + public uint TickIntervalSeconds { get; set; } /// /// A potentially-sparse rowset describing the storage space consumed diff --git a/src/Seq.Api/Model/Inputs/InputSettingsPart.cs b/src/Seq.Api/Model/Inputs/InputSettingsPart.cs index cd396c8..3eae56d 100644 --- a/src/Seq.Api/Model/Inputs/InputSettingsPart.cs +++ b/src/Seq.Api/Model/Inputs/InputSettingsPart.cs @@ -15,7 +15,6 @@ using System.Collections.Generic; using Seq.Api.Model.LogEvents; using Seq.Api.Model.Shared; -using Seq.Api.Model.Signals; namespace Seq.Api.Model.Inputs { diff --git a/src/Seq.Api/Model/License/LicenseEntity.cs b/src/Seq.Api/Model/License/LicenseEntity.cs index e9ddd3c..bf61674 100644 --- a/src/Seq.Api/Model/License/LicenseEntity.cs +++ b/src/Seq.Api/Model/License/LicenseEntity.cs @@ -67,10 +67,15 @@ public class LicenseEntity : Entity /// update the license when the subscription is renewed or tier changed. /// public bool AutomaticallyRefresh { get; set; } - + + /// + /// The amount of storage (in gigabytes) that Seq is licensed to store. + /// + public int? StorageLimitGigabytes { get; set; } + /// - /// If true, the license supports running Seq in a DR configuration. + /// If true, the license supports running Seq in a HA configuration. /// - public bool IncludesDisasterRecovery { get; set; } + public bool Clustered { get; set; } } } diff --git a/src/Seq.Api/Model/Settings/SettingName.cs b/src/Seq.Api/Model/Settings/SettingName.cs index 9609e57..bca9984 100644 --- a/src/Seq.Api/Model/Settings/SettingName.cs +++ b/src/Seq.Api/Model/Settings/SettingName.cs @@ -50,6 +50,12 @@ public enum SettingName /// be automatically granted default user access to Seq. /// AutomaticallyProvisionAuthenticatedUsers, + + /// + /// In a clustered configuration, the maximum data age allowed before a warning notification is generated + /// for an out-of-date follower node. + /// + DataAgeWarningThresholdMilliseconds, /// /// The Microsoft Entra ID authority. The default is login.windows.net; government cloud users may @@ -207,6 +213,12 @@ public enum SettingName /// SecretKeyIsBackedUp, + /// + /// In a clustered configuration, the minimum number of nodes that must carry up-to-date data in order for + /// operations like graceful fail-over to proceed. + /// + TargetReplicaCount, + /// /// A snippet of CSS that will be included in the front-end's user interface styles. /// diff --git a/src/Seq.Api/Model/Users/UserEntity.cs b/src/Seq.Api/Model/Users/UserEntity.cs index 81002f1..25d5b58 100644 --- a/src/Seq.Api/Model/Users/UserEntity.cs +++ b/src/Seq.Api/Model/Users/UserEntity.cs @@ -15,7 +15,6 @@ using System.Collections.Generic; using Newtonsoft.Json; using Seq.Api.Model.Shared; -using Seq.Api.Model.Signals; namespace Seq.Api.Model.Users { diff --git a/src/Seq.Api/ResourceGroups/AlertStateResourceGroup.cs b/src/Seq.Api/ResourceGroups/AlertStateResourceGroup.cs index e530ed0..768e820 100644 --- a/src/Seq.Api/ResourceGroups/AlertStateResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/AlertStateResourceGroup.cs @@ -17,7 +17,6 @@ using System.Threading; using System.Threading.Tasks; using Seq.Api.Model.Alerting; -using Seq.Api.Model.Dashboarding; namespace Seq.Api.ResourceGroups { diff --git a/src/Seq.Api/ResourceGroups/AppsResourceGroup.cs b/src/Seq.Api/ResourceGroups/AppsResourceGroup.cs index 6d18374..d64dfe4 100644 --- a/src/Seq.Api/ResourceGroups/AppsResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/AppsResourceGroup.cs @@ -16,7 +16,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Seq.Api.Model; using Seq.Api.Model.Apps; namespace Seq.Api.ResourceGroups @@ -86,9 +85,13 @@ public async Task InstallPackageAsync(string feedId, string packageId { if (feedId == null) throw new ArgumentNullException(nameof(feedId)); if (packageId == null) throw new ArgumentNullException(nameof(packageId)); - var parameters = new Dictionary { { "feedId", feedId }, { "packageId", packageId } }; - if (version != null) parameters.Add("version", version); - return await GroupPostAsync("InstallPackage", new object(), parameters, cancellationToken).ConfigureAwait(false); + var identity = new AppPackageIdentityPart + { + NuGetFeedId = feedId, + PackageId = packageId, + Version = version, + }; + return await GroupPostAsync("InstallPackage", identity, cancellationToken: cancellationToken).ConfigureAwait(false); } /// @@ -97,14 +100,19 @@ public async Task InstallPackageAsync(string feedId, string packageId /// The app to update the package for. /// The version to update to; if null, the latest available version in the feed will be used. /// If true, update the app package even if the same version is already installed. + /// A allowing the operation to be canceled. /// The app with updated package information. - public async Task UpdatePackageAsync(AppEntity entity, string version = null, bool force = false) + public async Task UpdatePackageAsync(AppEntity entity, string version = null, bool force = false, CancellationToken cancellationToken = default) { if (entity == null) throw new ArgumentNullException(nameof(entity)); var parameters = new Dictionary(); if (force) parameters.Add("force", true); - if (version != null) parameters.Add("version", version); - return await Client.PostAsync(entity, "UpdatePackage", new object(), parameters).ConfigureAwait(false); + var identity = new AppPackageIdentityPart + { + // `NuGetFeedId` and `PackageId` are ignored, but these may be accepted by a future API version. + Version = version, + }; + return await Client.PostAsync(entity, "UpdatePackage", identity, parameters, cancellationToken).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/Seq.Api/ResourceGroups/BackupsResourceGroup.cs b/src/Seq.Api/ResourceGroups/BackupsResourceGroup.cs index cfc16f4..1c6d5c8 100644 --- a/src/Seq.Api/ResourceGroups/BackupsResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/BackupsResourceGroup.cs @@ -58,7 +58,7 @@ public async Task> ListAsync(CancellationToken cancellationTo /// Download a backup with the current state of the server. Note that the backup will not be stored server-side. /// /// allowing the operation to be canceled. - /// The .seqbak backup file. + /// The .seqbac backup file. public async Task DownloadImmediateAsync(CancellationToken cancellationToken = default) { return await GroupPostReadBytesAsync("Immediate", new object(), cancellationToken: cancellationToken).ConfigureAwait(false); diff --git a/src/Seq.Api/ResourceGroups/ClusterNodesResourceGroup.cs b/src/Seq.Api/ResourceGroups/ClusterNodesResourceGroup.cs deleted file mode 100644 index b32ed4a..0000000 --- a/src/Seq.Api/ResourceGroups/ClusterNodesResourceGroup.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright Datalust and contributors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Seq.Api.Model.Cluster; - -namespace Seq.Api.ResourceGroups -{ - /// - /// Perform operations on a Seq cluster. - /// - /// Seq clustering initially supports a two-node replication - /// topology. - public class ClusterNodesResourceGroup : ApiResourceGroup - { - internal ClusterNodesResourceGroup(ILoadResourceGroup connection) - : base("ClusterNodes", connection) - { - } - - /// - /// Retrieve the cluster node with the given id; throws if the entity does not exist. - /// - /// The id of the cluster node. - /// A allowing the operation to be canceled. - /// The cluster node. - public async Task FindAsync(string id, CancellationToken cancellationToken = default) - { - if (id == null) throw new ArgumentNullException(nameof(id)); - return await GroupGetAsync("Item", new Dictionary { { "id", id } }, cancellationToken).ConfigureAwait(false); - } - - /// - /// Retrieve all known cluster nodes. - /// - /// allowing the operation to be canceled. - /// A list containing matching dashboards. - public async Task> ListAsync(CancellationToken cancellationToken = default) - { - return await GroupListAsync("Items", null, cancellationToken).ConfigureAwait(false); - } - - /// - /// Manually demote a leader node. The operation will proceed asynchronously; the state of the node can be checked - /// using (the node will disappear when demotion has finished). - /// - /// The leader node. - /// A allowing the operation to be canceled. - /// If successfully failed over, the node must be restarted before it can join the cluster as follower. - public async Task DemoteAsync(ClusterNodeEntity leader, CancellationToken cancellationToken = default) - { - if (leader == null) throw new ArgumentNullException(nameof(leader)); - await Client.PostAsync(leader, "Demote", new object(), cancellationToken: cancellationToken).ConfigureAwait(false); - } - } -} diff --git a/src/Seq.Api/ResourceGroups/ClusterResourceGroup.cs b/src/Seq.Api/ResourceGroups/ClusterResourceGroup.cs new file mode 100644 index 0000000..8b3be98 --- /dev/null +++ b/src/Seq.Api/ResourceGroups/ClusterResourceGroup.cs @@ -0,0 +1,107 @@ +// Copyright Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Seq.Api.Model.Cluster; +using Seq.Api.Model.Shared; + +namespace Seq.Api.ResourceGroups +{ + /// + /// Perform operations on the Seq cluster. + /// + public class ClusterResourceGroup : ApiResourceGroup + { + internal ClusterResourceGroup(ILoadResourceGroup connection) + : base("Cluster", connection) + { + } + + /// + /// Retrieve the cluster node with the given id; throws if the entity does not exist. + /// + /// The id of the cluster node. + /// A allowing the operation to be canceled. + /// The cluster node. + public async Task FindAsync(string id, CancellationToken cancellationToken = default) + { + if (id == null) throw new ArgumentNullException(nameof(id)); + return await GroupGetAsync("Item", new Dictionary { { "id", id } }, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieve all known cluster nodes. + /// + /// allowing the operation to be canceled. + /// A list containing matching dashboards. + public async Task> ListAsync(CancellationToken cancellationToken = default) + { + return await GroupListAsync("Items", null, cancellationToken).ConfigureAwait(false); + } + + /// + /// Manually cycle leadership in the cluster by draining active requests and taking the current leader node + /// offline. Warning, check cluster health first as this may leave the cluster unable to service requests if + /// invoked on an unhealthy cluster. + /// + /// The entity to drain. + /// A allowing the operation to be canceled. + public async Task DrainAsync(ClusterNodeEntity entity, CancellationToken cancellationToken = default) + { + await Client.PostAsync(entity, "Drain", new object(), cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Check the health of the cluster. + /// + /// This method will suppress exceptions generated when connecting to the target endpoint, and will return + /// a placeholder (unhealthy) response in those cases. + /// A allowing the operation to be canceled. + /// The information reported by the cluster health endpoint. + public async Task CheckHealthAsync(CancellationToken cancellationToken = default) + { + var group = await LoadGroupAsync(cancellationToken).ConfigureAwait(false); + var link = group.Links["Health"].GetUri(); + var response = await Client.HttpClient.GetAsync(link, cancellationToken: cancellationToken).ConfigureAwait(false); + + try + { + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + return JsonConvert.DeserializeObject(content); + } + catch + { + ErrorPart error = null; + try + { + var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + error = JsonConvert.DeserializeObject(content); + } + // ReSharper disable once EmptyGeneralCatchClause + catch { } + + return new HealthCheckResultPart + { + Status = HealthStatus.Unhealthy, + Description = error?.Error ?? + $"Could not connect to the Seq API endpoint ({(int)response.StatusCode}/{response.StatusCode})." + }; + } + } + } +} diff --git a/src/Seq.Api/ResourceGroups/DiagnosticsResourceGroup.cs b/src/Seq.Api/ResourceGroups/DiagnosticsResourceGroup.cs index 5ba40d8..1d905f8 100644 --- a/src/Seq.Api/ResourceGroups/DiagnosticsResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/DiagnosticsResourceGroup.cs @@ -84,34 +84,24 @@ public async Task GetMeasurementTimeseriesAsync(strin /// The (exclusive) end of the range to report on. If omitted, the results will report from the /// earliest stored data. The range must be a multiple of the interval size, or a whole number of days if /// no interval is specified. - /// The bucket size to use. Must be a multiple of 5 minutes. Defaults to 1440 (one day). + /// The bucket size to use. Must be a multiple of 5 minutes. Defaults to 1440 (one day). /// A allowing the operation to be canceled. /// Storage consumption information. public async Task GetStorageConsumptionAsync( DateTime? rangeStart, DateTime? rangeEnd, - int? intervalMinutes, + int? intervalSeconds, CancellationToken cancellationToken = default) { var parameters = new Dictionary { ["rangeStart"] = rangeStart, ["rangeEnd"] = rangeEnd, - ["intervalMinutes"] = intervalMinutes + ["intervalSeconds"] = intervalSeconds }; return await GroupGetAsync("Storage", parameters, cancellationToken); } - /// - /// Retrieve the cluster log. - /// - /// A allowing the operation to be canceled. - /// The cluster log. - public async Task GetClusterLogAsync(CancellationToken cancellationToken = default) - { - return await GroupGetStringAsync("ClusterLog", cancellationToken: cancellationToken); - } - /// /// Retrieve metrics about cluster connections. /// diff --git a/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs b/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs index bcb0194..4f51004 100644 --- a/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs @@ -40,12 +40,15 @@ internal EventsResourceGroup(ILoadResourceGroup connection) /// If the request is for a permalinked event, specifying the id of the permalink here will /// allow events that have otherwise been deleted to be found. The special value `"unknown"` provides backwards compatibility /// with versions prior to 5.0, which did not mark permalinks explicitly. + /// Run the search at a lower priority, using a lower proportion of available server + /// resources (may take longer to complete). /// Token through which the operation can be cancelled. /// The event. public async Task FindAsync( string id, bool render = false, string permalinkId = null, + bool background = false, CancellationToken cancellationToken = default) { if (id == null) throw new ArgumentNullException(nameof(id)); @@ -53,6 +56,7 @@ public async Task FindAsync( var parameters = new Dictionary {{"id", id}}; if (render) parameters.Add("render", true); if (permalinkId != null) parameters.Add("permalinkId", permalinkId); + if (background) parameters.Add("background", true); return await GroupGetAsync("Item", parameters, cancellationToken).ConfigureAwait(false); } @@ -77,8 +81,10 @@ public async Task FindAsync( /// If the request is for a permalinked event, specifying the id of the permalink here will /// allow events that have otherwise been deleted to be found. The special value `"unknown"` provides backwards compatibility /// with versions prior to 5.0, which did not mark permalinks explicitly. - /// Token through which the operation can be cancelled. /// Values for any free variables that appear in . + /// Run the search at a lower priority, using a lower proportion of available server + /// resources (may take longer to complete). + /// Token through which the operation can be cancelled. /// The complete list of events, ordered from least to most recent. public async IAsyncEnumerable EnumerateAsync( SignalEntity unsavedSignal = null, @@ -93,6 +99,7 @@ public async IAsyncEnumerable EnumerateAsync( int? shortCircuitAfter = null, string permalinkId = null, Dictionary variables = null, + bool background = false, [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -107,7 +114,7 @@ public async IAsyncEnumerable EnumerateAsync( while (true) { var resultSet = await PageAsync(unsavedSignal, signal, filter, nextCount, startAtId, nextAfterId, render, - fromDateUtc, toDateUtc, shortCircuitAfter, permalinkId, variables, cancellationToken).ConfigureAwait(false); + fromDateUtc, toDateUtc, shortCircuitAfter, permalinkId, variables, background, cancellationToken).ConfigureAwait(false); foreach (var evt in resultSet.Events) { @@ -153,6 +160,8 @@ public async IAsyncEnumerable EnumerateAsync( /// allow events that have otherwise been deleted to be found. The special value `"unknown"` provides backwards compatibility /// with versions prior to 5.0, which did not mark permalinks explicitly. /// Values for any free variables that appear in . + /// Run the search at a lower priority, using a lower proportion of available server + /// resources (may take longer to complete). /// Token through which the operation can be cancelled. /// The result set with a page of events. public async Task> ListAsync( @@ -168,11 +177,12 @@ public async Task> ListAsync( int? shortCircuitAfter = null, string permalinkId = null, Dictionary variables = null, + bool background = false, CancellationToken cancellationToken = default) { var results = new List(); await foreach (var item in EnumerateAsync(unsavedSignal, signal, filter, count, startAtId, afterId, render, - fromDateUtc, toDateUtc, shortCircuitAfter, permalinkId, variables, cancellationToken) + fromDateUtc, toDateUtc, shortCircuitAfter, permalinkId, variables, background, cancellationToken) .WithCancellation(cancellationToken) .ConfigureAwait(false)) results.Add(item); @@ -200,6 +210,8 @@ public async Task> ListAsync( /// allow events that have otherwise been deleted to be found. The special value `"unknown"` provides backwards compatibility /// with versions prior to 5.0, which did not mark permalinks explicitly. /// Values for any free variables that appear in . + /// Run the search at a lower priority, using a lower proportion of available server + /// resources (may take longer to complete). /// Token through which the operation can be cancelled. /// The result set with a page of events. public async Task PageAsync( @@ -215,6 +227,7 @@ public async Task PageAsync( int? shortCircuitAfter = null, string permalinkId = null, Dictionary variables = null, + bool background = false, CancellationToken cancellationToken = default) { var parameters = new Dictionary{{ "count", count }}; @@ -227,6 +240,7 @@ public async Task PageAsync( if (toDateUtc != null) { parameters.Add("toDateUtc", toDateUtc.Value); } if (shortCircuitAfter != null) { parameters.Add("shortCircuitAfter", shortCircuitAfter.Value); } if (permalinkId != null) { parameters.Add("permalinkId", permalinkId); } + if (background) parameters.Add("background", true); var body = new EvaluationContextPart { Signal = unsavedSignal, Variables = variables }; return await GroupPostAsync("InSignal", body, parameters, cancellationToken).ConfigureAwait(false); diff --git a/src/Seq.Api/ResourceGroups/RunningTasksResourceGroup.cs b/src/Seq.Api/ResourceGroups/RunningTasksResourceGroup.cs index fb0c217..992e1c4 100644 --- a/src/Seq.Api/ResourceGroups/RunningTasksResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/RunningTasksResourceGroup.cs @@ -17,7 +17,6 @@ using System.Threading; using System.Threading.Tasks; using Seq.Api.Model.Alerting; -using Seq.Api.Model.Dashboarding; using Seq.Api.Model.Tasks; namespace Seq.Api.ResourceGroups diff --git a/src/Seq.Api/ResourceGroups/UsersResourceGroup.cs b/src/Seq.Api/ResourceGroups/UsersResourceGroup.cs index 5b95402..99b83a1 100644 --- a/src/Seq.Api/ResourceGroups/UsersResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/UsersResourceGroup.cs @@ -174,5 +174,16 @@ public async Task LoginWindowsIntegratedAsync(CancellationToken canc response.EnsureSuccessStatusCode(); return await FindCurrentAsync(cancellationToken).ConfigureAwait(false); } + + /// Reset the authentication properties stored for . After this operation + /// completes, the user's will exclusively determine how they are linked + /// to the authentication provider on their next login. + /// The user to modify. + /// A allowing the operation to be canceled. + /// A task signalling completion. + public async Task UnlinkAuthenticationProviderAsync(UserEntity entity, CancellationToken cancellationToken = default) + { + await Client.PostAsync(entity, "UnlinkAuthenticationProvider", new {}, cancellationToken: cancellationToken).ConfigureAwait(false); + } } } diff --git a/src/Seq.Api/Seq.Api.csproj b/src/Seq.Api/Seq.Api.csproj index 9eed51c..a6a1e83 100644 --- a/src/Seq.Api/Seq.Api.csproj +++ b/src/Seq.Api/Seq.Api.csproj @@ -24,7 +24,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Seq.Api/SeqConnection.cs b/src/Seq.Api/SeqConnection.cs index 6032e27..de64a6a 100644 --- a/src/Seq.Api/SeqConnection.cs +++ b/src/Seq.Api/SeqConnection.cs @@ -19,9 +19,11 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Newtonsoft.Json; using Seq.Api.Client; using Seq.Api.Model; using Seq.Api.Model.Root; +using Seq.Api.Model.Shared; using Seq.Api.ResourceGroups; namespace Seq.Api @@ -31,8 +33,8 @@ namespace Seq.Api /// public class SeqConnection : ILoadResourceGroup, IDisposable { - readonly object _sync = new object(); - readonly Dictionary> _resourceGroups = new Dictionary>(); + readonly object _sync = new(); + readonly Dictionary> _resourceGroups = new(); Task _root; /// @@ -92,133 +94,133 @@ public void Dispose() /// /// Create and manage alerts. /// - public AlertsResourceGroup Alerts => new AlertsResourceGroup(this); + public AlertsResourceGroup Alerts => new(this); /// /// List and administratively remove active alerts. To create/edit/remove alerts in normal /// circumstances, use . /// - public AlertStateResourceGroup AlertState => new AlertStateResourceGroup(this); + public AlertStateResourceGroup AlertState => new(this); /// /// Perform operations on API keys. /// - public ApiKeysResourceGroup ApiKeys => new ApiKeysResourceGroup(this); + public ApiKeysResourceGroup ApiKeys => new(this); /// /// Perform operations on Seq app instances. /// - public AppInstancesResourceGroup AppInstances => new AppInstancesResourceGroup(this); + public AppInstancesResourceGroup AppInstances => new(this); /// /// Perform operations on Seq app packages. /// - public AppsResourceGroup Apps => new AppsResourceGroup(this); + public AppsResourceGroup Apps => new(this); /// /// Perform operations on backups. /// - public BackupsResourceGroup Backups => new BackupsResourceGroup(this); - + public BackupsResourceGroup Backups => new(this); + /// - /// Perform operations on the Seq cluster. + /// Perform operations on Seq cluster nodes. /// - public ClusterNodesResourceGroup ClusterNodes => new ClusterNodesResourceGroup(this); + public ClusterResourceGroup Cluster => new(this); /// /// Perform operations on dashboards. /// - public DashboardsResourceGroup Dashboards => new DashboardsResourceGroup(this); + public DashboardsResourceGroup Dashboards => new(this); /// /// Execute SQL-style queries against the API. /// - public DataResourceGroup Data => new DataResourceGroup(this); + public DataResourceGroup Data => new(this); /// /// Access server diagnostics. /// - public DiagnosticsResourceGroup Diagnostics => new DiagnosticsResourceGroup(this); + public DiagnosticsResourceGroup Diagnostics => new(this); /// /// Read and subscribe to events from the event store. /// - public EventsResourceGroup Events => new EventsResourceGroup(this); + public EventsResourceGroup Events => new(this); /// /// Perform operations on queries and filter expressions. /// - public ExpressionsResourceGroup Expressions => new ExpressionsResourceGroup(this); + public ExpressionsResourceGroup Expressions => new(this); /// /// Perform operations on expression indexes. /// - public ExpressionIndexesResourceGroup ExpressionIndexes => new ExpressionIndexesResourceGroup(this); + public ExpressionIndexesResourceGroup ExpressionIndexes => new(this); /// /// Perform operations on NuGet feeds. /// - public FeedsResourceGroup Feeds => new FeedsResourceGroup(this); + public FeedsResourceGroup Feeds => new(this); /// /// Statistics about indexes. /// - public IndexesResourceGroup Indexes => new IndexesResourceGroup(this); + public IndexesResourceGroup Indexes => new(this); /// /// Perform operations on the Seq license certificate. /// - public LicensesResourceGroup Licenses => new LicensesResourceGroup(this); + public LicensesResourceGroup Licenses => new(this); /// /// Perform operations on permalinked events. /// - public PermalinksResourceGroup Permalinks => new PermalinksResourceGroup(this); + public PermalinksResourceGroup Permalinks => new(this); /// /// Perform operations on retention policies. /// - public RetentionPoliciesResourceGroup RetentionPolicies => new RetentionPoliciesResourceGroup(this); + public RetentionPoliciesResourceGroup RetentionPolicies => new(this); /// /// Perform operations on user roles. /// - public RolesResourceGroup Roles => new RolesResourceGroup(this); + public RolesResourceGroup Roles => new(this); /// /// Perform operations on tasks running in the Seq server. /// - public RunningTasksResourceGroup RunningTasks => new RunningTasksResourceGroup(this); + public RunningTasksResourceGroup RunningTasks => new(this); /// /// Perform operations on system settings. /// - public SettingsResourceGroup Settings => new SettingsResourceGroup(this); + public SettingsResourceGroup Settings => new(this); /// /// Perform operations on signals. /// - public SignalsResourceGroup Signals => new SignalsResourceGroup(this); + public SignalsResourceGroup Signals => new(this); /// /// Perform operations on saved SQL queries. /// - public SqlQueriesResourceGroup SqlQueries => new SqlQueriesResourceGroup(this); + public SqlQueriesResourceGroup SqlQueries => new(this); /// /// Perform operations on known available Seq versions. /// - public UpdatesResourceGroup Updates => new UpdatesResourceGroup(this); + public UpdatesResourceGroup Updates => new(this); /// /// Perform operations on users. /// - public UsersResourceGroup Users => new UsersResourceGroup(this); + public UsersResourceGroup Users => new(this); /// /// Perform operations on workspaces. /// - public WorkspacesResourceGroup Workspaces => new WorkspacesResourceGroup(this); + public WorkspacesResourceGroup Workspaces => new(this); /// /// Check that the Seq API is available. If the initial attempt fails (i.e. the server is starting up), @@ -240,11 +242,11 @@ public async Task EnsureConnectedAsync(TimeSpan timeout) async Task ConnectAsync(bool throwOnFailure) { - HttpStatusCode statusCode; + HttpResponseMessage response; try { - statusCode = (await Client.HttpClient.GetAsync("api")).StatusCode; + response = await Client.HttpClient.GetAsync("api"); } catch { @@ -254,13 +256,27 @@ async Task ConnectAsync(bool throwOnFailure) return false; } + var statusCode = response.StatusCode; if (statusCode == HttpStatusCode.OK) return true; if (!throwOnFailure) return false; + + ErrorPart error = null; + try + { + var content = await response.Content.ReadAsStringAsync(); + error = JsonConvert.DeserializeObject(content); + } + // ReSharper disable once EmptyGeneralCatchClause + catch { } + + var exceptionMessage = $"Could not connect to the Seq API endpoint ({(int)statusCode}/{statusCode})."; + if (error?.Error != null) + exceptionMessage += $" {error.Error}"; - throw new SeqApiException($"Could not connect to the Seq API endpoint: {(int)statusCode}/{statusCode}.", statusCode); + throw new SeqApiException(exceptionMessage, statusCode); } async Task ILoadResourceGroup.LoadResourceGroupAsync(string name, CancellationToken cancellationToken)