Skip to content

Commit f5c4611

Browse files
authored
Merge pull request #468 from nblumhardt/metrics-commands-2
Metrics command group
2 parents 9b4fc2a + 4cd44a5 commit f5c4611

28 files changed

Lines changed: 1110 additions & 215 deletions

README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1868,7 +1868,7 @@ PS > seqcli signal list -i signal-m33302 --json
18681868
{"Title": "Alarms", "Description": "Automatically created", "Filters": [{"De...
18691869
```
18701870

1871-
## Store-and-forward ingestion proxy (preview)
1871+
## Store-and-forward ingestion proxy
18721872

18731873
The `seqcli forwarder` family of commands provide simple, durable ingestion buffering for occasionally-connected and
18741874
intermittently-disconnected systems. The forwarder implements the Seq ingestion API, so applications that write
@@ -1890,12 +1890,9 @@ destination Seq server.
18901890
To start a forwarder instance at the terminal, listening on port 5341 and forwarding to `seq.example.com`, run:
18911891

18921892
```shell
1893-
seqcli forwarder run --pre --listen http://127.0.0.1:5341 -s https://seq.example.com
1893+
seqcli forwarder run --listen http://127.0.0.1:5341 -s https://seq.example.com
18941894
```
18951895

1896-
> While the `forwarder` command group is in preview, all `forwarder` commands require the `--pre` switch; you'll
1897-
> also need to supply `--pre` when requesting help, e.g. `seqcli help forwarder run --pre`.
1898-
18991896
You can test your forwarder using the `seqcli log` command:
19001897

19011898
```shell

src/SeqCli/Cli/Commands/Mcp/RunCommand.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@
1414

1515
using System;
1616
using System.Threading.Tasks;
17-
using Autofac.Extensions.DependencyInjection;
1817
using Microsoft.Extensions.DependencyInjection;
1918
using Microsoft.Extensions.Hosting;
2019
using SeqCli.Api;
2120
using SeqCli.Cli.Features;
2221
using SeqCli.Config;
2322
using SeqCli.Mcp;
23+
using SeqCli.Mcp.Tools.Metrics;
24+
using SeqCli.Mcp.Tools.Query;
2425
using SeqCli.Mcp.Tools.Search;
26+
using SeqCli.Mcp.Tools.Signals;
2527
using Serilog;
2628

2729
namespace SeqCli.Cli.Commands.Mcp;
@@ -58,15 +60,17 @@ protected override async Task<int> Run()
5860
try
5961
{
6062
var builder = Host.CreateApplicationBuilder();
61-
builder.ConfigureContainer(new AutofacServiceProviderFactory());
6263
builder.Services.AddSerilog();
6364
builder.Services.AddSingleton(_ => SeqConnectionFactory.Connect(_connection, config));
6465
builder.Services.AddSingleton<McpSession>();
6566
builder.Services
6667
.AddMcpServer()
6768
.WithStdioServerTransport()
6869
.WithTools([
69-
typeof(SearchAndQueryToolType)
70+
typeof(SearchTools),
71+
typeof(MetricsTools),
72+
typeof(QueryTools),
73+
typeof(SignalTools)
7074
]);
7175

7276
await builder.Build().RunAsync();
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright © Datalust Pty Ltd
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Globalization;
17+
using System.Threading.Tasks;
18+
using SeqCli.Api;
19+
using SeqCli.Cli.Features;
20+
using SeqCli.Config;
21+
using Serilog;
22+
23+
namespace SeqCli.Cli.Commands.Metrics;
24+
25+
[Command("metrics", "dimensionvalues", "List distinct values for a metric dimension",
26+
Example = "seqcli metrics dimensionvalues --accessor @Resource.service.name")]
27+
class DimensionValuesCommand : Command
28+
{
29+
readonly ConnectionFeature _connection;
30+
readonly OutputFormatFeature _output;
31+
readonly DateRangeFeature _range;
32+
readonly StoragePathFeature _storagePath;
33+
string? _accessor;
34+
int _count = 30;
35+
bool _trace;
36+
37+
public DimensionValuesCommand()
38+
{
39+
Options.Add(
40+
"d=|accessor=",
41+
"The dimension accessor, e.g. `cpu.mode`",
42+
v => _accessor= v);
43+
44+
Options.Add(
45+
"c=|count=",
46+
$"The maximum number of dimensions to retrieve; the default is {_count}",
47+
v => _count = int.Parse(v, CultureInfo.InvariantCulture));
48+
49+
_range = Enable<DateRangeFeature>();
50+
_output = Enable(new OutputFormatFeature(supportNative: true));
51+
_storagePath = Enable<StoragePathFeature>();
52+
53+
Options.Add("trace", "Enable detailed (server-side) query tracing", _ => _trace = true);
54+
55+
_connection = Enable<ConnectionFeature>();
56+
}
57+
58+
protected override async Task<int> Run()
59+
{
60+
try
61+
{
62+
if (string.IsNullOrWhiteSpace(_accessor))
63+
{
64+
Log.Error("A dimension `--accessor` must be specified");
65+
return 1;
66+
}
67+
68+
var config = RuntimeConfigurationLoader.Load(_storagePath);
69+
var output = _output.GetOutputFormat(config);
70+
var connection = SeqConnectionFactory.Connect(_connection, config);
71+
72+
var result = await connection.Metrics.ListDimensionValuesAsync(
73+
_accessor,
74+
_count,
75+
rangeStartUtc: _range.Start,
76+
rangeEndUtc: _range.End,
77+
trace: _trace);
78+
79+
if (output.Json)
80+
{
81+
// In the JSON case we write an array with all values.
82+
output.WriteObject(result);
83+
}
84+
else
85+
{
86+
// Native and plain text formatting use one-per-line output (both allow multi-line strings, but
87+
// string boundaries are clearer in native mode).
88+
foreach (var value in result)
89+
{
90+
output.WriteObject(value);
91+
}
92+
}
93+
94+
return 0;
95+
}
96+
catch (Exception ex)
97+
{
98+
Log.Error(ex, "Could not retrieve metrics: {ErrorMessage}", ex.Message);
99+
return 1;
100+
}
101+
}
102+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright © Datalust Pty Ltd
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Globalization;
17+
using System.Threading.Tasks;
18+
using SeqCli.Api;
19+
using SeqCli.Cli.Features;
20+
using SeqCli.Config;
21+
using Serilog;
22+
23+
namespace SeqCli.Cli.Commands.Metrics;
24+
25+
[Command("metrics", "dimensions", "List the dimensions associated with a given metric",
26+
Example = "seqcli metrics dimensions -m http.response.status_code")]
27+
class DimensionsCommand : Command
28+
{
29+
readonly ConnectionFeature _connection;
30+
readonly OutputFormatFeature _output;
31+
readonly DateRangeFeature _range;
32+
readonly StoragePathFeature _storagePath;
33+
string? _metric;
34+
int _count = 30;
35+
bool _trace;
36+
37+
public DimensionsCommand()
38+
{
39+
Options.Add(
40+
"m=|metric=",
41+
"A metric name, for example `hats-sold` or `http.request.duration`; omit to list dimensions for all metrics",
42+
v => _metric= v);
43+
44+
Options.Add(
45+
"c=|count=",
46+
$"The maximum number of dimensions to retrieve; the default is {_count}",
47+
v => _count = int.Parse(v, CultureInfo.InvariantCulture));
48+
49+
_range = Enable<DateRangeFeature>();
50+
_output = Enable<OutputFormatFeature>();
51+
_storagePath = Enable<StoragePathFeature>();
52+
53+
Options.Add("trace", "Enable detailed (server-side) query tracing", _ => _trace = true);
54+
55+
_connection = Enable<ConnectionFeature>();
56+
}
57+
58+
protected override async Task<int> Run()
59+
{
60+
try
61+
{
62+
var config = RuntimeConfigurationLoader.Load(_storagePath);
63+
var output = _output.GetOutputFormat(config);
64+
var connection = SeqConnectionFactory.Connect(_connection, config);
65+
66+
var result = await connection.Metrics.ListDimensionsAsync(
67+
_count,
68+
_metric,
69+
rangeStartUtc: _range.Start,
70+
rangeEndUtc: _range.End,
71+
trace: _trace);
72+
73+
if (output.Json)
74+
{
75+
output.WriteObject(result);
76+
}
77+
else
78+
{
79+
foreach (var dimension in result)
80+
{
81+
output.WriteText(dimension.Accessor);
82+
}
83+
}
84+
85+
return 0;
86+
}
87+
catch (Exception ex)
88+
{
89+
Log.Error(ex, "Could not retrieve metrics: {ErrorMessage}", ex.Message);
90+
return 1;
91+
}
92+
}
93+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright © Datalust Pty Ltd
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Collections.Generic;
17+
using System.Globalization;
18+
using System.Linq;
19+
using System.Threading.Tasks;
20+
using Seq.Api.Model.Data;
21+
using SeqCli.Api;
22+
using SeqCli.Cli.Features;
23+
using SeqCli.Config;
24+
using SeqCli.Util;
25+
using Serilog;
26+
27+
namespace SeqCli.Cli.Commands.Metrics;
28+
29+
[Command("metrics", "search", "List available metric definitions",
30+
Example = "seqcli metrics search -f \"@Resource.service.name = 'proxy'\" -c 512")]
31+
class SearchCommand : Command
32+
{
33+
readonly ConnectionFeature _connection;
34+
readonly OutputFormatFeature _output;
35+
readonly DateRangeFeature _range;
36+
readonly StoragePathFeature _storagePath;
37+
string? _filter;
38+
readonly List<string> _groups = [];
39+
int _count = 30;
40+
bool _trace;
41+
42+
public SearchCommand()
43+
{
44+
Options.Add(
45+
"f=|filter=",
46+
"A filter to apply to the search, including metric name/description text in double quotes, for example `\"cpu\" and Host = 'xmpweb-01.example.com'`",
47+
v => _filter = v);
48+
49+
Options.Add(
50+
"g=|group=",
51+
"Group key for metric definition breakdown; this argument can be used multiple times",
52+
c => _groups.Add(ArgumentString.Normalize(c) ?? throw new ArgumentException("Group keys require a value.")));
53+
54+
Options.Add(
55+
"c=|count=",
56+
$"The maximum number of metric definitions to retrieve; the default is {_count}",
57+
v => _count = int.Parse(v, CultureInfo.InvariantCulture));
58+
59+
_range = Enable<DateRangeFeature>();
60+
// Native is not supported because accessor expressions appear in the output, and the escaping applied to them
61+
// as native strings does more harm than good.
62+
_output = Enable<OutputFormatFeature>();
63+
_storagePath = Enable<StoragePathFeature>();
64+
65+
Options.Add("trace", "Enable detailed (server-side) query tracing", _ => _trace = true);
66+
67+
_connection = Enable<ConnectionFeature>();
68+
}
69+
70+
protected override async Task<int> Run()
71+
{
72+
try
73+
{
74+
var config = RuntimeConfigurationLoader.Load(_storagePath);
75+
var output = _output.GetOutputFormat(config);
76+
var connection = SeqConnectionFactory.Connect(_connection, config);
77+
78+
string? filter = null;
79+
if (!string.IsNullOrWhiteSpace(_filter))
80+
filter = (await connection.Expressions.ToStrictAsync(_filter)).StrictExpression;
81+
82+
var result = await connection.Metrics.SearchAsync(
83+
_groups,
84+
filter,
85+
_count,
86+
rangeStartUtc: _range.Start,
87+
rangeEndUtc: _range.End,
88+
trace: _trace);
89+
90+
// We convert the metric into a query result to improve formatting consistency. Room for an abstraction of
91+
// some kind here.
92+
var rows = new List<object?[]>();
93+
foreach (var metric in result.Metrics)
94+
{
95+
var row = new List<object?>
96+
{
97+
metric.Name ?? metric.Accessor,
98+
metric.Kind,
99+
metric.Unit,
100+
metric.Description
101+
};
102+
103+
foreach (var value in metric.GroupKey)
104+
row.Add(value);
105+
106+
rows.Add(row.ToArray());
107+
}
108+
var asRowset = new QueryResultPart
109+
{
110+
Columns = new[] { "Name", "Kind", "Unit", "Description" }.Concat(_groups).ToArray(),
111+
Rows = rows.ToArray()
112+
};
113+
114+
output.WriteQueryResult(asRowset);
115+
116+
return 0;
117+
}
118+
catch (Exception ex)
119+
{
120+
Log.Error(ex, "Could not retrieve metrics: {ErrorMessage}", ex.Message);
121+
return 1;
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)