diff --git a/src/Seq.Api/Client/SeqApiClient.cs b/src/Seq.Api/Client/SeqApiClient.cs index d25ae82..14f2c70 100644 --- a/src/Seq.Api/Client/SeqApiClient.cs +++ b/src/Seq.Api/Client/SeqApiClient.cs @@ -191,6 +191,14 @@ public async Task PostAsync(ILinked entity, strin return _serializer.Deserialize(new JsonTextReader(new StreamReader(stream))); } + // Callers are expected to derive 400 error information from the response stream. All other result status codes throw. + internal async Task TryPostAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) + { + var linkUri = ResolveLink(entity, link, parameters); + var request = new HttpRequestMessage(HttpMethod.Post, linkUri) { Content = MakeJsonContent(content) }; + var stream = await HttpTrySendAsync(request, cancellationToken).ConfigureAwait(false); + return _serializer.Deserialize(new JsonTextReader(new StreamReader(stream))); + } /// /// Issue a POST request accepting a serialized and returning a string by following from . /// @@ -452,8 +460,19 @@ async Task HttpGetStringAsync(string url, CancellationToken cancellation #endif ).ConfigureAwait(false); } - + + // Throws on 5xx errors; callers are expected to derive 400 error information from the response stream. + async Task HttpTrySendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + return await HttpSendAsyncCore(request, throwOn400: false, cancellationToken); + } + async Task HttpSendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) + { + return await HttpSendAsyncCore(request, throwOn400: true, cancellationToken); + } + + async Task HttpSendAsyncCore(HttpRequestMessage request, bool throwOn400, CancellationToken cancellationToken) { var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); var stream = await response.Content.ReadAsStreamAsync( @@ -462,7 +481,7 @@ async Task HttpSendAsync(HttpRequestMessage request, CancellationToken c #endif ).ConfigureAwait(false); - if (response.IsSuccessStatusCode) + if (response.IsSuccessStatusCode || (!throwOn400 && response.StatusCode == HttpStatusCode.BadRequest)) { return stream; } diff --git a/src/Seq.Api/Model/Alerting/AlertActivityPart.cs b/src/Seq.Api/Model/Alerting/AlertActivityPart.cs index 97f25f5..340e9f1 100644 --- a/src/Seq.Api/Model/Alerting/AlertActivityPart.cs +++ b/src/Seq.Api/Model/Alerting/AlertActivityPart.cs @@ -38,14 +38,14 @@ public class AlertActivityPart /// /// The most recent occurrences of the alert that triggered notifications. /// - public List RecentOccurrences { get; set; } = new List(); + public List RecentOccurrences { get; set; } = new(); /// /// Minimal metrics for the most recent occurrences of the alert that triggered notifications. /// The metrics in this list are a superset of . /// public List RecentOccurrenceRanges { get; set; } = - new List(); + new(); /// /// The number of times this alert has been triggered since its creation. diff --git a/src/Seq.Api/Model/Alerting/AlertEntity.cs b/src/Seq.Api/Model/Alerting/AlertEntity.cs index ad8bab1..d39927d 100644 --- a/src/Seq.Api/Model/Alerting/AlertEntity.cs +++ b/src/Seq.Api/Model/Alerting/AlertEntity.cs @@ -51,11 +51,21 @@ public class AlertEntity : Entity /// public bool IsDisabled { get; set; } + /// + /// The source of the data for the query. + /// + public DataSource DataSource { get; set; } = DataSource.Stream; + + /// + /// Lateral joins applied to the data source. + /// + public List Joins { get; set; } = []; + /// /// An optional limiting the data source that triggers the alert. /// public SignalExpressionPart SignalExpression { get; set; } - + /// /// An optional where clause limiting the data source that triggers the alert. /// @@ -75,7 +85,7 @@ public class AlertEntity : Entity /// /// The individual measurements that will be tested by the alert condition. /// - public List Select { get; set; } = new List(); + public List Select { get; set; } = []; /// /// The alert condition. This is a having clause over the grouped results diff --git a/src/Seq.Api/Model/Alerting/AlertOccurrencePart.cs b/src/Seq.Api/Model/Alerting/AlertOccurrencePart.cs index 8104e3d..fd04f7a 100644 --- a/src/Seq.Api/Model/Alerting/AlertOccurrencePart.cs +++ b/src/Seq.Api/Model/Alerting/AlertOccurrencePart.cs @@ -28,6 +28,6 @@ public class AlertOccurrencePart /// /// The NotificationChannelParts that were alerted. /// - public List Notifications { get; set; } = new List(); + public List Notifications { get; set; } = new(); } } \ No newline at end of file diff --git a/src/Seq.Api/Model/Alerting/NotificationChannelPart.cs b/src/Seq.Api/Model/Alerting/NotificationChannelPart.cs index 37988dc..bd901a9 100644 --- a/src/Seq.Api/Model/Alerting/NotificationChannelPart.cs +++ b/src/Seq.Api/Model/Alerting/NotificationChannelPart.cs @@ -58,6 +58,6 @@ public class NotificationChannelPart /// by the alert. /// public Dictionary NotificationAppSettingOverrides { get; set; } = - new Dictionary(); + new(); } } diff --git a/src/Seq.Api/Model/AppInstances/AppInstanceOutputMetricsPart.cs b/src/Seq.Api/Model/AppInstances/AppInstanceOutputMetricsPart.cs index d0f36bb..4e1c114 100644 --- a/src/Seq.Api/Model/AppInstances/AppInstanceOutputMetricsPart.cs +++ b/src/Seq.Api/Model/AppInstances/AppInstanceOutputMetricsPart.cs @@ -11,5 +11,12 @@ public class AppInstanceOutputMetricsPart /// it being processed by the app. /// public int DispatchedEventsPerMinute { get; set; } + + /// + /// The number of events per minute that failed to reach the app from Seq. Includes streamed events (if enabled), + /// manual invocations, and alert notifications. This value doesn't track events the app may have internally + /// failed to process. + /// + public int FailedEventsPerMinute { get; set; } } } diff --git a/src/Seq.Api/Model/Data/QueryResultPart.cs b/src/Seq.Api/Model/Data/QueryResultPart.cs index cbd14ea..0cbffa4 100644 --- a/src/Seq.Api/Model/Data/QueryResultPart.cs +++ b/src/Seq.Api/Model/Data/QueryResultPart.cs @@ -32,6 +32,7 @@ public class QueryResultPart /// /// The columns within the result set (at various levels of the hierarchy). /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public string[] Columns { get; set; } /// @@ -43,6 +44,7 @@ public class QueryResultPart /// /// Metadata for the time grouping column. /// + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public ColumnMetadataPart TimeColumnMetadata { get; set; } /// diff --git a/src/Seq.Api/Model/Metrics/MetricAggregationPreference.cs b/src/Seq.Api/Model/Metrics/MetricAggregationPreference.cs index a9e0985..e077c02 100644 --- a/src/Seq.Api/Model/Metrics/MetricAggregationPreference.cs +++ b/src/Seq.Api/Model/Metrics/MetricAggregationPreference.cs @@ -6,27 +6,37 @@ namespace Seq.Api.Model.Metrics; public enum MetricAggregationPreference { /// - /// The count() aggregate function. + /// The total count of observed values. /// - Count, - + Total, + /// - /// The sum() aggregate function. + /// The sum of all observed values. /// Sum, /// - /// The min() aggregate function. + /// The counts of values falling in each histogram bucket. + /// + BucketSum, + + /// + /// The smallest observed value. /// Min, /// - /// The mean() aggregate function. + /// The center observed value. /// Mean, /// - /// The max() aggregate function. + /// The largest observed value. + /// + Max, + + /// + /// The set of values greater than a percentage of all other observed values. /// - Max + Percentiles } \ No newline at end of file diff --git a/src/Seq.Api/Model/SqlQueries/SqlQueryEntity.cs b/src/Seq.Api/Model/Queries/QueryEntity.cs similarity index 87% rename from src/Seq.Api/Model/SqlQueries/SqlQueryEntity.cs rename to src/Seq.Api/Model/Queries/QueryEntity.cs index 49b3b15..9630fc8 100644 --- a/src/Seq.Api/Model/SqlQueries/SqlQueryEntity.cs +++ b/src/Seq.Api/Model/Queries/QueryEntity.cs @@ -14,19 +14,19 @@ using Seq.Api.Model.Security; -namespace Seq.Api.Model.SqlQueries +namespace Seq.Api.Model.Queries { /// - /// A saved SQL-style query. + /// A saved query. /// - public class SqlQueryEntity : Entity + public class QueryEntity : Entity { /// - /// Construct a . + /// Construct a . /// - public SqlQueryEntity() + public QueryEntity() { - Title = "New SQL Query"; + Title = "New Query"; Sql = ""; } diff --git a/src/Seq.Api/Model/Shared/JoinKind.cs b/src/Seq.Api/Model/Shared/JoinKind.cs new file mode 100644 index 0000000..59328d5 --- /dev/null +++ b/src/Seq.Api/Model/Shared/JoinKind.cs @@ -0,0 +1,31 @@ +// 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.Shared; + +/// +/// A type of relational join. +/// +public enum JoinKind +{ + /// + /// The unknown join kind. + /// + Unknown, + + /// + /// A join type that joins each row to the output of a table function evaluated in the context of the row. + /// + Lateral +} \ No newline at end of file diff --git a/src/Seq.Api/Model/Shared/JoinPart.cs b/src/Seq.Api/Model/Shared/JoinPart.cs new file mode 100644 index 0000000..51a439c --- /dev/null +++ b/src/Seq.Api/Model/Shared/JoinPart.cs @@ -0,0 +1,38 @@ +// 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.Shared; + +#nullable enable + +/// +/// The lateral cross join part of a from clause. +/// +public class JoinPart +{ + /// + /// The type of relational join. + /// + public JoinKind Kind { get; set; } + + /// + /// The set function call used in the lateral join. + /// + public string? SetFunctionCall { get; set; } + + /// + /// The alias of the set function call. + /// + public string? Alias { get; set; } +} \ No newline at end of file diff --git a/src/Seq.Api/Model/Workspaces/WorkspaceContentPart.cs b/src/Seq.Api/Model/Workspaces/WorkspaceContentPart.cs index b6924fd..ab4940a 100644 --- a/src/Seq.Api/Model/Workspaces/WorkspaceContentPart.cs +++ b/src/Seq.Api/Model/Workspaces/WorkspaceContentPart.cs @@ -16,7 +16,7 @@ using Seq.Api.Model.Dashboarding; using Seq.Api.Model.Metrics; using Seq.Api.Model.Signals; -using Seq.Api.Model.SqlQueries; +using Seq.Api.Model.Queries; namespace Seq.Api.Model.Workspaces { @@ -31,7 +31,7 @@ public class WorkspaceContentPart public List SignalIds { get; set; } = []; /// - /// A list of ids to include in the workspace. + /// A list of ids to include in the workspace. /// public List QueryIds { get; set; } = []; diff --git a/src/Seq.Api/ResourceGroups/ApiResourceGroup.cs b/src/Seq.Api/ResourceGroups/ApiResourceGroup.cs index 4347f64..48da13d 100644 --- a/src/Seq.Api/ResourceGroups/ApiResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/ApiResourceGroup.cs @@ -159,6 +159,13 @@ protected async Task GroupPostAsync(string link, var group = await LoadGroupAsync(cancellationToken).ConfigureAwait(false); return await Client.PostAsync(group, link, content, parameters, cancellationToken).ConfigureAwait(false); } + + // Callers are expected to derive 400 error information from the response stream. All other result status codes throw. + internal async Task GroupTryPostAsync(string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) + { + var group = await LoadGroupAsync(cancellationToken).ConfigureAwait(false); + return await Client.TryPostAsync(group, link, content, parameters, cancellationToken).ConfigureAwait(false); + } /// /// Update an entity. diff --git a/src/Seq.Api/ResourceGroups/DataResourceGroup.cs b/src/Seq.Api/ResourceGroups/DataResourceGroup.cs index df10b64..5a9d0e3 100644 --- a/src/Seq.Api/ResourceGroups/DataResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/DataResourceGroup.cs @@ -33,7 +33,8 @@ internal DataResourceGroup(ILoadResourceGroup connection) } /// - /// Execute an SQL query and retrieve the result set as a structured . + /// Execute an SQL query and retrieve the result set as a structured . For non-throwing + /// syntax error reporting, see . /// /// The query to execute. /// The earliest timestamp from which to include events in the query result. @@ -61,6 +62,37 @@ public async Task QueryAsync( return await GroupPostAsync("Query", body, parameters, cancellationToken).ConfigureAwait(false); } + /// + /// Execute an SQL query and retrieve the result set as a structured . This method + /// differs from by returning a result object with error information instead of throwing + /// when the query syntax is invalid. + /// + /// The query to execute. + /// The earliest timestamp from which to include events in the query result. + /// The exclusive latest timestamp to which events are included in the query result. The default is the current time. + /// A signal expression over which the query will be executed. + /// A constructed signal that may not appear on the server, for example, a that has been + /// created but not saved, a signal from another server, or the modified representation of an entity already persisted. + /// The query timeout; if not specified, the query will run until completion. + /// Values for any free variables that appear in . + /// Enable detailed (server-side) query tracing. + /// Token through which the operation can be cancelled. + /// A structured result set or syntax/execution error information. + public async Task TryQueryAsync( + string query, + DateTime? rangeStartUtc = null, + DateTime? rangeEndUtc = null, + SignalExpressionPart signal = null, + SignalEntity unsavedSignal = null, + TimeSpan? timeout = null, + Dictionary variables = null, + bool trace = false, + CancellationToken cancellationToken = default) + { + MakeParameters(query, rangeStartUtc, rangeEndUtc, signal, unsavedSignal, timeout, variables, trace, out var body, out var parameters); + return await GroupTryPostAsync("Query", body, parameters, cancellationToken).ConfigureAwait(false); + } + /// /// Execute an SQL query and retrieve the result set as a structured . /// diff --git a/src/Seq.Api/ResourceGroups/MetricsResourceGroup.cs b/src/Seq.Api/ResourceGroups/MetricsResourceGroup.cs index 182c7c0..29c5df5 100644 --- a/src/Seq.Api/ResourceGroups/MetricsResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/MetricsResourceGroup.cs @@ -37,8 +37,8 @@ internal MetricsResourceGroup(ILoadResourceGroup connection) /// A strict Seq filter expression to match (text expressions must be in double quotes). To /// convert a "fuzzy" filter into a strict one the way the Seq UI does, use . /// The number of definitions to retrieve. If not specified will default to 30. - /// The earliest timestamp from which to include events in the query result. - /// The exclusive latest timestamp to which events are included in the query result. The default is the current time. + /// The earliest timestamp from which to include definitions in the query result. + /// The exclusive latest timestamp to which definitions are included in the query result. The default is the current time. /// The query timeout; if not specified, the query will run until completion. /// Values for any free variables that appear in . /// Enable detailed (server-side) query tracing. @@ -61,13 +61,13 @@ public async Task SearchAsync( } /// - /// Retrieve information about the labels available for filtering samples matching a set of search criteria. + /// Retrieve information about the dimensions available for filtering samples matching a set of search criteria. /// - /// The number of definitions to retrieve. If not specified will default to 30. + /// The number of dimensions to retrieve. If not specified will default to 30. /// Optionally, the name of a metric to limit dimension search to. By default, dimensions /// for all metrics are returned. - /// The earliest timestamp from which to include events in the query result. - /// The exclusive latest timestamp to which events are included in the query result. The default is the current time. + /// The earliest timestamp from which to include dimensions in the query result. + /// The exclusive latest timestamp to which dimensions are included in the query result. The default is the current time. /// The query timeout; if not specified, the query will run until completion. /// Enable detailed (server-side) query tracing. /// Token through which the operation can be cancelled. @@ -88,6 +88,33 @@ public async Task> ListDimensionsAsync( return await GroupPostAsync>("Dimensions", body, parameters, cancellationToken).ConfigureAwait(false); } + /// + /// Retrieve the top values associated with the metric dimension . + /// + /// The dimension's accessor. This is generally a simple property path (cluster.node.name) but can + /// use explict root namespaces and indexer notation (@Resource.cluster['node name']) if necessary. + /// The number of values to retrieve. If not specified will default to 30. + /// The earliest timestamp from which to include values in the query result. + /// The exclusive latest timestamp to which values are included in the query result. The default is the current time. + /// The query timeout; if not specified, the query will run until completion. + /// Enable detailed (server-side) query tracing. + /// Token through which the operation can be cancelled. + /// A structured result set. + public async Task> ListDimensionValuesAsync( + string accessor, + int count = 30, + DateTime? rangeStartUtc = null, + DateTime? rangeEndUtc = null, + TimeSpan? timeout = null, + bool trace = false, + CancellationToken cancellationToken = default) + { + var parameters = MakeParameters(null, null, count, rangeStartUtc, rangeEndUtc, timeout, trace); + parameters.Add(nameof(accessor), accessor); + var body = new EvaluationContextPart(); + return await GroupPostAsync>("DimensionValues", body, parameters, cancellationToken).ConfigureAwait(false); + } + static Dictionary MakeParameters(List groups, string filter, int count, DateTime? rangeStartUtc, DateTime? rangeEndUtc, TimeSpan? timeout, bool trace) { var parameters = new Dictionary diff --git a/src/Seq.Api/ResourceGroups/SqlQueriesResourceGroup.cs b/src/Seq.Api/ResourceGroups/QueriesResourceGroup.cs similarity index 71% rename from src/Seq.Api/ResourceGroups/SqlQueriesResourceGroup.cs rename to src/Seq.Api/ResourceGroups/QueriesResourceGroup.cs index 83414ed..671fab2 100644 --- a/src/Seq.Api/ResourceGroups/SqlQueriesResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/QueriesResourceGroup.cs @@ -17,17 +17,17 @@ using System.Threading; using System.Threading.Tasks; using Seq.Api.Model; -using Seq.Api.Model.SqlQueries; +using Seq.Api.Model.Queries; using Seq.Api.Model.Users; namespace Seq.Api.ResourceGroups { /// - /// Perform operations on saved SQL queries. + /// Perform operations on saved queries. /// - public class SqlQueriesResourceGroup : ApiResourceGroup + public class QueriesResourceGroup : ApiResourceGroup { - internal SqlQueriesResourceGroup(ILoadResourceGroup connection) + internal QueriesResourceGroup(ILoadResourceGroup connection) : base("SqlQueries", connection) { } @@ -38,10 +38,10 @@ internal SqlQueriesResourceGroup(ILoadResourceGroup connection) /// The id of the query. /// A allowing the operation to be canceled. /// The query. - public async Task FindAsync(string id, CancellationToken cancellationToken = default) + 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); + return await GroupGetAsync("Item", new Dictionary { { "id", id } }, cancellationToken).ConfigureAwait(false); } /// @@ -52,10 +52,10 @@ public async Task FindAsync(string id, CancellationToken cancell /// If true, shared queries will be included in the result. /// allowing the operation to be canceled. /// A list containing matching queries. - public async Task> ListAsync(string ownerId = null, bool shared = false, CancellationToken cancellationToken = default) + public async Task> ListAsync(string ownerId = null, bool shared = false, CancellationToken cancellationToken = default) { var parameters = new Dictionary { { "ownerId", ownerId }, { "shared", shared } }; - return await GroupListAsync("Items", parameters, cancellationToken: cancellationToken).ConfigureAwait(false); + return await GroupListAsync("Items", parameters, cancellationToken: cancellationToken).ConfigureAwait(false); } /// @@ -63,9 +63,9 @@ public async Task> ListAsync(string ownerId = null, bool sh /// /// allowing the operation to be canceled. /// The unsaved query. - public async Task TemplateAsync(CancellationToken cancellationToken = default) + public async Task TemplateAsync(CancellationToken cancellationToken = default) { - return await GroupGetAsync("Template", cancellationToken: cancellationToken).ConfigureAwait(false); + return await GroupGetAsync("Template", cancellationToken: cancellationToken).ConfigureAwait(false); } /// @@ -74,9 +74,9 @@ public async Task TemplateAsync(CancellationToken cancellationTo /// The query to add. /// A allowing the operation to be canceled. /// The query, with server-allocated properties such as initialized. - public async Task AddAsync(SqlQueryEntity entity, CancellationToken cancellationToken = default) + public async Task AddAsync(QueryEntity entity, CancellationToken cancellationToken = default) { - return await GroupCreateAsync(entity, cancellationToken: cancellationToken).ConfigureAwait(false); + return await GroupCreateAsync(entity, cancellationToken: cancellationToken).ConfigureAwait(false); } /// @@ -85,7 +85,7 @@ public async Task AddAsync(SqlQueryEntity entity, CancellationTo /// The query to remove. /// A allowing the operation to be canceled. /// A task indicating completion. - public async Task RemoveAsync(SqlQueryEntity entity, CancellationToken cancellationToken = default) + public async Task RemoveAsync(QueryEntity entity, CancellationToken cancellationToken = default) { await Client.DeleteAsync(entity, "Self", entity, cancellationToken: cancellationToken).ConfigureAwait(false); } @@ -96,7 +96,7 @@ public async Task RemoveAsync(SqlQueryEntity entity, CancellationToken cancellat /// The query to update. /// A allowing the operation to be canceled. /// A task indicating completion. - public async Task UpdateAsync(SqlQueryEntity entity, CancellationToken cancellationToken = default) + public async Task UpdateAsync(QueryEntity entity, CancellationToken cancellationToken = default) { await Client.PutAsync(entity, "Self", entity, cancellationToken: cancellationToken).ConfigureAwait(false); } diff --git a/src/Seq.Api/SeqConnection.cs b/src/Seq.Api/SeqConnection.cs index 29f90e2..5ac056d 100644 --- a/src/Seq.Api/SeqConnection.cs +++ b/src/Seq.Api/SeqConnection.cs @@ -180,9 +180,9 @@ public void Dispose() public SignalsResourceGroup Signals => new(this); /// - /// Perform operations on saved SQL queries. + /// Perform operations on saved queries. /// - public SqlQueriesResourceGroup SqlQueries => new(this); + public QueriesResourceGroup Queries => new(this); /// /// Perform operations on known available Seq versions.