From b637a259d586390f4027cba6f1e430c09c422df6 Mon Sep 17 00:00:00 2001 From: Adrin Lopez Calvo Date: Mon, 28 Sep 2026 19:04:31 +0200 Subject: [PATCH 1/5] Ungate metrics from TIGER_EXPERIMENTAL, keep backups gated Splits the shared TIGER_EXPERIMENTAL flag: `service metrics`/`service_metrics_*` graduate and are now always registered, while `service backup`/`service_backups` stays behind the flag since it targets a gateway endpoint still marked x-tigerdata-preview and is under active development on origin/toni/backup-regions. Regenerates docs/cli for the newly-unconditional metrics commands, updates CLAUDE.md's Experimental Feature Gating section, and rewrites the CLI/MCP tests that previously assumed metrics needed the experimental gate. --- CLAUDE.md | 4 +- docs/cli/tiger_service.md | 1 + docs/cli/tiger_service_metrics.md | 31 ++++++++ .../tiger_service_metrics_available-series.md | 33 ++++++++ docs/cli/tiger_service_metrics_details.md | 51 ++++++++++++ docs/cli/tiger_service_metrics_series.md | 77 +++++++++++++++++++ internal/cmd/mcp_get_test.go | 30 +++----- internal/cmd/mcp_list_test.go | 71 +++++++++-------- internal/cmd/service.go | 10 +-- internal/cmd/service_metrics.go | 8 +- internal/cmd/service_metrics_details_test.go | 18 +---- internal/cmd/service_metrics_series_test.go | 9 --- internal/cmd/service_test.go | 20 ++--- internal/mcp/server.go | 26 +++---- internal/mcp/server_test.go | 14 +--- .../mcp/service_metrics_available_test.go | 24 +----- internal/mcp/service_metrics_details_test.go | 23 +----- internal/mcp/service_metrics_series_test.go | 38 +-------- 18 files changed, 284 insertions(+), 204 deletions(-) create mode 100644 docs/cli/tiger_service_metrics.md create mode 100644 docs/cli/tiger_service_metrics_available-series.md create mode 100644 docs/cli/tiger_service_metrics_details.md create mode 100644 docs/cli/tiger_service_metrics_series.md diff --git a/CLAUDE.md b/CLAUDE.md index 90f9a31b..7c4d1590 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,7 +67,7 @@ Generated files are marked `-diff linguist-generated=true` in `.gitattributes`: ## CLI Reference Docs -`docs/cli/` is a generated Markdown reference with one file per command, produced by `cmd/generate-docs`, which walks the command tree with cobra's `doc` package. It runs under `go generate ./...`, so any change to a command, flag, or help text needs a regeneration (CI fails on a stale or missing file). The tool forces `TIGER_EXPERIMENTAL` off for the run, so gated commands never reach the docs. +`docs/cli/` is a generated Markdown reference with one file per command, produced by `cmd/generate-docs`, which walks the command tree with cobra's `doc` package. It runs under `go generate ./...`, so any change to a command, flag, or help text needs a regeneration (CI fails on a stale or missing file). The tool forces `TIGER_EXPERIMENTAL` off for the run, so gated commands (currently `backups`) never reach the docs. ## Command Architecture @@ -108,7 +108,7 @@ Configuration is layered, with precedence **flags > `TIGER_*` env vars > config ### Experimental Feature Gating -`TIGER_EXPERIMENTAL` gates commands and MCP tools that aren't ready to be public yet, for whatever reason — including, but not limited to, anything backed by a gateway endpoint marked `x-tigerdata-preview: true` in `openapi.yaml` (those request/response shapes are still in flux, so a surface built on one must always be gated). +`TIGER_EXPERIMENTAL` gates commands and MCP tools that aren't ready to be public yet — currently just `service backups`/`service_backups`, which is backed by a gateway endpoint marked `x-tigerdata-preview: true` in `openapi.yaml` (those request/response shapes are still in flux, so a surface built on one must always be gated). Metrics (`service metrics`/`service_metrics_*`) graduated off this gate and is now always registered. It's an env var **only**: deliberately not a config key, not a flag, and hidden from `tiger config list`. `buildRootCmd` reads it once into `app.Experimental`, and the CLI guards its `AddCommand` calls with it while the MCP server guards its `addTool` calls, so when the env var is off the gated commands and tools don't exist at all — no help entry, no completion, not advertised to MCP clients (restart the MCP server after toggling). **Never mention `TIGER_EXPERIMENTAL` in user-facing docs, command help, or error messages.** When a feature graduates, delete the gates on both sides. diff --git a/docs/cli/tiger_service.md b/docs/cli/tiger_service.md index bff9d6fe..435ad108 100644 --- a/docs/cli/tiger_service.md +++ b/docs/cli/tiger_service.md @@ -32,6 +32,7 @@ Manage database services within Tiger Cloud platform. * [tiger service get](tiger_service_get.md) - Show detailed information about a service * [tiger service list](tiger_service_list.md) - List all services * [tiger service logs](tiger_service_logs.md) - View logs for a service +* [tiger service metrics](tiger_service_metrics.md) - View service metrics * [tiger service rename](tiger_service_rename.md) - Rename a database service * [tiger service resize](tiger_service_resize.md) - Resize a database service * [tiger service start](tiger_service_start.md) - Start a stopped database service diff --git a/docs/cli/tiger_service_metrics.md b/docs/cli/tiger_service_metrics.md new file mode 100644 index 00000000..4c328c91 --- /dev/null +++ b/docs/cli/tiger_service_metrics.md @@ -0,0 +1,31 @@ +## tiger service metrics + +View service metrics + +### Synopsis + +Commands for querying time-series metrics for a Tiger Cloud service. + +### Options + +``` + -h, --help help for metrics +``` + +### Options inherited from parent commands + +``` + --analytics enable/disable usage analytics (default true) + --color enable colored output (default true) + --config-dir string config directory (default "~/.config/tiger") + --password-storage string password storage method (keyring, pgpass, none) (default "keyring") + --service-id string service ID + --version-check check for updates on startup (default true) +``` + +### SEE ALSO + +* [tiger service](tiger_service.md) - Manage database services +* [tiger service metrics available-series](tiger_service_metrics_available-series.md) - List available metric series +* [tiger service metrics details](tiger_service_metrics_details.md) - Get metric details +* [tiger service metrics series](tiger_service_metrics_series.md) - Get metric series data diff --git a/docs/cli/tiger_service_metrics_available-series.md b/docs/cli/tiger_service_metrics_available-series.md new file mode 100644 index 00000000..10c580de --- /dev/null +++ b/docs/cli/tiger_service_metrics_available-series.md @@ -0,0 +1,33 @@ +## tiger service metrics available-series + +List available metric series + +### Synopsis + +List the names of all metric series available for a service. + +``` +tiger service metrics available-series [service-id] [flags] +``` + +### Options + +``` + -h, --help help for available-series + -o, --output string Output format (json, yaml, table) +``` + +### Options inherited from parent commands + +``` + --analytics enable/disable usage analytics (default true) + --color enable colored output (default true) + --config-dir string config directory (default "~/.config/tiger") + --password-storage string password storage method (keyring, pgpass, none) (default "keyring") + --service-id string service ID + --version-check check for updates on startup (default true) +``` + +### SEE ALSO + +* [tiger service metrics](tiger_service_metrics.md) - View service metrics diff --git a/docs/cli/tiger_service_metrics_details.md b/docs/cli/tiger_service_metrics_details.md new file mode 100644 index 00000000..3098f1e3 --- /dev/null +++ b/docs/cli/tiger_service_metrics_details.md @@ -0,0 +1,51 @@ +## tiger service metrics details + +Get metric details + +### Synopsis + +Get descriptive metadata for a metric: what it measures, its type, default +aggregation function, and available labels. + +Use 'tiger service metrics available-series' to discover valid metric names, +then 'tiger service metrics series' to fetch its data. + +These metrics have no richer metadata — expect just the name back, with type, +default aggregation, description, and labels all empty: timescale_cloud_system_cpu_total_millicores, timescale_cloud_system_cpu_usage_millicores, timescale_cloud_system_disk_io_read_bytes, timescale_cloud_system_disk_io_read_ops, timescale_cloud_system_disk_io_total_bytes, timescale_cloud_system_disk_io_total_ops, timescale_cloud_system_disk_io_write_bytes, timescale_cloud_system_disk_io_write_ops, timescale_cloud_system_disk_usage_bytes, timescale_cloud_system_memory_total_bytes, timescale_cloud_system_memory_usage_bytes, timescale_cloud_database_qps, timescale_cloud_database_num_connections, timescale_cloud_database_job_duration_usecs, timescale_cloud_database_job_success. + +``` +tiger service metrics details [service-id] [flags] +``` + +### Examples + +``` + # Describe a metric + tiger service metrics details --metric pg_stat_activity_count + + # Get metric details as JSON + tiger service metrics details --metric pg_stat_activity_count --output json +``` + +### Options + +``` + -h, --help help for details + --metric string Metric name + -o, --output string Output format (json, yaml, table) +``` + +### Options inherited from parent commands + +``` + --analytics enable/disable usage analytics (default true) + --color enable colored output (default true) + --config-dir string config directory (default "~/.config/tiger") + --password-storage string password storage method (keyring, pgpass, none) (default "keyring") + --service-id string service ID + --version-check check for updates on startup (default true) +``` + +### SEE ALSO + +* [tiger service metrics](tiger_service_metrics.md) - View service metrics diff --git a/docs/cli/tiger_service_metrics_series.md b/docs/cli/tiger_service_metrics_series.md new file mode 100644 index 00000000..d08b19a1 --- /dev/null +++ b/docs/cli/tiger_service_metrics_series.md @@ -0,0 +1,77 @@ +## tiger service metrics series + +Get metric series data + +### Synopsis + +Get time-series data for a specific metric. + +Use 'tiger service metrics available-series' to discover valid metric names. + +Each labeled series (e.g. one per replica) is returned independently with its +full list of raw data points. + +``` +tiger service metrics series [service-id] [flags] +``` + +### Examples + +``` + # Fetch CPU usage for the last hour + tiger service metrics series --metric timescale_cloud_system_cpu_usage_millicores \ + --from 2026-05-13T00:00:00Z --to 2026-05-13T01:00:00Z + + # Get memory data points as JSON + tiger service metrics series --metric timescale_cloud_system_memory_usage_bytes \ + --from 2026-05-13T00:00:00Z --to 2026-05-13T01:00:00Z --output json + + # Fetch data for the primary instance only + tiger service metrics series --metric timescale_cloud_system_cpu_usage_millicores \ + --from 2026-05-13T00:00:00Z --to 2026-05-13T01:00:00Z --role PRIMARY + + # Filter by an arbitrary label + tiger service metrics series --metric some_metric_name \ + --from 2026-05-13T00:00:00Z --to 2026-05-13T01:00:00Z \ + --filter ordinal=0 + + # Exclude a label value + tiger service metrics series --metric some_metric_name \ + --from 2026-05-13T00:00:00Z --to 2026-05-13T01:00:00Z \ + --filter role!=replica + + # Break the result into one series per role + tiger service metrics series --metric some_metric_name \ + --from 2026-05-13T00:00:00Z --to 2026-05-13T01:00:00Z \ + --group-by role +``` + +### Options + +``` + --bucket-seconds int Aggregation bucket size in seconds (optional; server auto-selects based on the time window when omitted, minimum 60s) + --filter strings Arbitrary label filter as name=value or name!=value (repeatable) + --fn string Aggregation function applied per bucket. One of: RATE, INCREASE, SUM, AVG, MIN, MAX, MIN_TOTAL, MAX_TOTAL, COUNT, P50, P90, P99, LAST. Rejected on the timescale_cloud_* resource/qps/connections/jobs metrics; omit to let the server pick the default + --from string Start of the time window (RFC3339) + --group-by strings Label key to break the result into one series per distinct value (repeatable). Rejected on the same metrics that reject --fn; omit to collapse into a single series + -h, --help help for series + --metric string Metric series name + -o, --output string Output format (json, yaml, table) + --role string Filter to a specific instance role (PRIMARY or REPLICA) + --to string End of the time window (RFC3339) +``` + +### Options inherited from parent commands + +``` + --analytics enable/disable usage analytics (default true) + --color enable colored output (default true) + --config-dir string config directory (default "~/.config/tiger") + --password-storage string password storage method (keyring, pgpass, none) (default "keyring") + --service-id string service ID + --version-check check for updates on startup (default true) +``` + +### SEE ALSO + +* [tiger service metrics](tiger_service_metrics.md) - View service metrics diff --git a/internal/cmd/mcp_get_test.go b/internal/cmd/mcp_get_test.go index 7f314f2b..5754d36d 100644 --- a/internal/cmd/mcp_get_test.go +++ b/internal/cmd/mcp_get_test.go @@ -352,30 +352,22 @@ Output: wantErr: `capability "service_create" not found`, }, { - name: "experimental tool hidden by default", - args: []string{"mcp", "get", "service_metrics_available"}, - opts: noDocsProxy(nil), - wantErr: `capability "service_metrics_available" not found`, - }, - { - name: "experimental tool visible with gate on", - args: []string{"mcp", "get", "service_metrics_available"}, - opts: append(noDocsProxy(nil), - withEnv("TIGER_EXPERIMENTAL", "true")), + name: "service_metrics_available visible by default", + args: []string{"mcp", "get", "service_metrics_available"}, + opts: noDocsProxy(nil), wantStdout: metricsAvailableText, }, { - name: "service_metrics_details hidden by default", - args: []string{"mcp", "get", "service_metrics_details"}, - opts: noDocsProxy(nil), - wantErr: `capability "service_metrics_details" not found`, + name: "service_metrics_details visible by default", + args: []string{"mcp", "get", "service_metrics_details"}, + opts: noDocsProxy(nil), + wantStdout: metricsDetailsText, }, { - name: "service_metrics_details visible with gate on", - args: []string{"mcp", "get", "service_metrics_details"}, - opts: append(noDocsProxy(nil), - withEnv("TIGER_EXPERIMENTAL", "true")), - wantStdout: metricsDetailsText, + name: "experimental-gated tool hidden by default", + args: []string{"mcp", "get", "service_backups"}, + opts: noDocsProxy(nil), + wantErr: `capability "service_backups" not found`, }, }) } diff --git a/internal/cmd/mcp_list_test.go b/internal/cmd/mcp_list_test.go index 4705ace3..e8d9716e 100644 --- a/internal/cmd/mcp_list_test.go +++ b/internal/cmd/mcp_list_test.go @@ -19,6 +19,9 @@ func TestMCPListCmd(t *testing.T) { "service_get", "service_list", "service_logs", + "service_metrics_available", + "service_metrics_details", + "service_metrics_series", "service_rename", "service_resize", "service_start", @@ -26,40 +29,48 @@ func TestMCPListCmd(t *testing.T) { "service_update_password", } - wantText := `┌──────┬─────────────────────────┐ -│ TYPE │ NAME │ -├──────┼─────────────────────────┤ -│ tool │ db_query │ -│ tool │ db_schema │ -│ tool │ feedback │ -│ tool │ service_create │ -│ tool │ service_delete │ -│ tool │ service_fork │ -│ tool │ service_get │ -│ tool │ service_list │ -│ tool │ service_logs │ -│ tool │ service_rename │ -│ tool │ service_resize │ -│ tool │ service_start │ -│ tool │ service_stop │ -│ tool │ service_update_password │ -└──────┴─────────────────────────┘ + wantText := `┌──────┬───────────────────────────┐ +│ TYPE │ NAME │ +├──────┼───────────────────────────┤ +│ tool │ db_query │ +│ tool │ db_schema │ +│ tool │ feedback │ +│ tool │ service_create │ +│ tool │ service_delete │ +│ tool │ service_fork │ +│ tool │ service_get │ +│ tool │ service_list │ +│ tool │ service_logs │ +│ tool │ service_metrics_available │ +│ tool │ service_metrics_details │ +│ tool │ service_metrics_series │ +│ tool │ service_rename │ +│ tool │ service_resize │ +│ tool │ service_start │ +│ tool │ service_stop │ +│ tool │ service_update_password │ +└──────┴───────────────────────────┘ ` // Read-only mode skips the service-mutating tools at registration time. - wantTextReadOnly := `┌──────┬──────────────┐ -│ TYPE │ NAME │ -├──────┼──────────────┤ -│ tool │ db_query │ -│ tool │ db_schema │ -│ tool │ feedback │ -│ tool │ service_get │ -│ tool │ service_list │ -│ tool │ service_logs │ -└──────┴──────────────┘ + // The metrics tools are all read-only themselves, so they stay registered. + wantTextReadOnly := `┌──────┬───────────────────────────┐ +│ TYPE │ NAME │ +├──────┼───────────────────────────┤ +│ tool │ db_query │ +│ tool │ db_schema │ +│ tool │ feedback │ +│ tool │ service_get │ +│ tool │ service_list │ +│ tool │ service_logs │ +│ tool │ service_metrics_available │ +│ tool │ service_metrics_details │ +│ tool │ service_metrics_series │ +└──────┴───────────────────────────┘ ` - // TIGER_EXPERIMENTAL registers the preview backups and metrics tools. + // TIGER_EXPERIMENTAL registers the preview backup tools on top of the + // (always-registered) default set, which already includes metrics. wantTextExperimental := `┌──────┬──────────────────────────────┐ │ TYPE │ NAME │ ├──────┼──────────────────────────────┤ @@ -167,7 +178,7 @@ func TestMCPListCmd(t *testing.T) { wantStdout: wantTextReadOnly, }, { - name: "experimental adds metrics tools", + name: "experimental adds backups tool", args: []string{"mcp", "list"}, opts: append(noDocsProxy(nil), withEnv("TIGER_EXPERIMENTAL", "true")), diff --git a/internal/cmd/service.go b/internal/cmd/service.go index 558b8707..9595e6eb 100644 --- a/internal/cmd/service.go +++ b/internal/cmd/service.go @@ -15,9 +15,9 @@ import ( ) // buildServiceCmd creates the main service command with all subcommands. -// experimental gates preview-stage subcommands (currently `metrics`); when -// false, those subtrees are not added to the tree at all — matching ghost's -// TIGER_EXPERIMENTAL pattern. See CLAUDE.md's "Experimental Feature Gating". +// experimental still gates `backups`, which is backed by a gateway endpoint +// marked `x-tigerdata-preview: true`; when false, that subtree is not added to +// the tree at all. See CLAUDE.md's "Experimental Feature Gating". func buildServiceCmd(app *common.App) *cobra.Command { cmd := &cobra.Command{ Use: "service", @@ -38,10 +38,10 @@ func buildServiceCmd(app *common.App) *cobra.Command { cmd.AddCommand(buildServiceRenameCmd(app)) cmd.AddCommand(buildServiceResizeCmd(app)) cmd.AddCommand(buildServiceLogsCmd(app)) + cmd.AddCommand(buildServiceMetricsCmd(app)) - // Experimental commands, unregistered until the preview graduates. + // Experimental command, unregistered until the preview graduates. if app.Experimental { - cmd.AddCommand(buildServiceMetricsCmd(app)) cmd.AddCommand(buildServiceBackupCmd(app)) } diff --git a/internal/cmd/service_metrics.go b/internal/cmd/service_metrics.go index 4dec3fcb..344806c8 100644 --- a/internal/cmd/service_metrics.go +++ b/internal/cmd/service_metrics.go @@ -6,12 +6,8 @@ import ( "github.com/timescale/tiger-cli/internal/common" ) -// buildServiceMetricsCmd creates the metrics subcommand group. The metrics -// surface targets gateway endpoints marked `x-tigerdata-preview: true` in the -// OpenAPI spec — their request/response contract is still in flux. Registration is -// gated on TIGER_EXPERIMENTAL in buildServiceCmd, so this builder is only -// called when the env var is set; the tree doesn't include `metrics` at all -// otherwise. +// buildServiceMetricsCmd creates the metrics subcommand group. Registered +// unconditionally in buildServiceCmd. func buildServiceMetricsCmd(app *common.App) *cobra.Command { cmd := &cobra.Command{ Use: "metrics", diff --git a/internal/cmd/service_metrics_details_test.go b/internal/cmd/service_metrics_details_test.go index 2e3d6bcf..47e2f27c 100644 --- a/internal/cmd/service_metrics_details_test.go +++ b/internal/cmd/service_metrics_details_test.go @@ -11,10 +11,6 @@ import ( ) func TestServiceMetricsDetailsCmd(t *testing.T) { - // The command is experimental-gated (see the gate test in service_test.go), - // so every case registers it explicitly. - experimental := withEnv("TIGER_EXPERIMENTAL", "true") - maxTotal := api.MetricsAggFnMAXTOTAL gauge := api.MetricTypeGAUGE @@ -90,26 +86,23 @@ func TestServiceMetricsDetailsCmd(t *testing.T) { { name: "not logged in", args: []string{"service", "metrics", "details", "svc-12345", "--metric", "pg_stat_activity_count"}, - opts: []runOption{experimental, withNotLoggedIn()}, + opts: []runOption{withNotLoggedIn()}, wantErr: notLoggedInMsg, checks: []checkFunc{checkExitCode(common.ExitAuthenticationError)}, }, { name: "missing metric flag", args: []string{"service", "metrics", "details", "svc-12345"}, - opts: []runOption{experimental}, wantErr: `required flag(s) "metric" not set`, }, { name: "missing service id", args: []string{"service", "metrics", "details", "--metric", "pg_stat_activity_count"}, - opts: []runOption{experimental}, wantErr: "service name or ID is required. Provide it as an argument or set a default with 'tiger config set service_id '", }, { name: "ambiguous name refused", args: []string{"service", "metrics", "details", "my-api-db", "--metric", "pg_stat_activity_count"}, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { expectResolveRefStatus(m, "my-api-db", http.StatusBadRequest, &api.Error{Message: new("ambiguous service name matches multiple services")}) }, @@ -119,7 +112,6 @@ func TestServiceMetricsDetailsCmd(t *testing.T) { { name: "network error", args: []string{"service", "metrics", "details", "svc-12345", "--metric", "pg_stat_activity_count"}, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { expectResolveRefID(m, "svc-12345") m.EXPECT().GetServiceMetricDetailsWithResponse(validCtx, testProjectID, "svc-12345", "pg_stat_activity_count"). @@ -130,7 +122,6 @@ func TestServiceMetricsDetailsCmd(t *testing.T) { { name: "API error", args: []string{"service", "metrics", "details", "svc-12345", "--metric", "pg_stat_activity_count"}, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { expectResolveRefID(m, "svc-12345") m.EXPECT().GetServiceMetricDetailsWithResponse(validCtx, testProjectID, "svc-12345", "pg_stat_activity_count"). @@ -145,7 +136,6 @@ func TestServiceMetricsDetailsCmd(t *testing.T) { { name: "nil response body", args: []string{"service", "metrics", "details", "svc-12345", "--metric", "pg_stat_activity_count"}, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { expectResolveRefID(m, "svc-12345") m.EXPECT().GetServiceMetricDetailsWithResponse(validCtx, testProjectID, "svc-12345", "pg_stat_activity_count"). @@ -159,14 +149,13 @@ func TestServiceMetricsDetailsCmd(t *testing.T) { { name: "table output with labels", args: []string{"service", "metrics", "details", "svc-12345", "--metric", "pg_stat_activity_count"}, - opts: []runOption{experimental}, mock: setupDetails("pg_stat_activity_count", fullDetails), wantStdout: fullDetailsTable, }, { name: "default service id from config", args: []string{"service", "metrics", "details", "--metric", "pg_stat_activity_count"}, - opts: []runOption{experimental, withConfig(map[string]any{"service_id": "svc-12345"})}, + opts: []runOption{withConfig(map[string]any{"service_id": "svc-12345"})}, mock: setupDetails("pg_stat_activity_count", fullDetails), wantStdout: fullDetailsTable, }, @@ -176,14 +165,12 @@ func TestServiceMetricsDetailsCmd(t *testing.T) { // labels. name: "table output without labels", args: []string{"service", "metrics", "details", "svc-12345", "--metric", "some_new_metric"}, - opts: []runOption{experimental}, mock: setupDetails("some_new_metric", undocumentedDetails), wantStdout: undocumentedTable, }, { name: "json output", args: []string{"service", "metrics", "details", "svc-12345", "--metric", "pg_stat_activity_count", "-o", "json"}, - opts: []runOption{experimental}, mock: setupDetails("pg_stat_activity_count", fullDetails), wantStdout: `{ "default_agg": "MAX_TOTAL", @@ -226,7 +213,6 @@ func TestServiceMetricsDetailsCmd(t *testing.T) { { name: "yaml output", args: []string{"service", "metrics", "details", "svc-12345", "--metric", "pg_stat_activity_count", "-o", "yaml"}, - opts: []runOption{experimental}, mock: setupDetails("pg_stat_activity_count", fullDetails), wantStdout: `default_agg: MAX_TOTAL description: Number of connections in pg_stat_activity, grouped by state, connected role, and backend type. diff --git a/internal/cmd/service_metrics_series_test.go b/internal/cmd/service_metrics_series_test.go index df97f75f..b9329a94 100644 --- a/internal/cmd/service_metrics_series_test.go +++ b/internal/cmd/service_metrics_series_test.go @@ -11,10 +11,6 @@ import ( ) func TestServiceMetricsSeriesCmd(t *testing.T) { - // The command is experimental-gated (see the gate test in service_test.go), - // so every case registers it explicitly. - experimental := withEnv("TIGER_EXPERIMENTAL", "true") - fromTime := time.Date(2026, 5, 13, 0, 0, 0, 0, time.UTC) toTime := time.Date(2026, 5, 13, 1, 0, 0, 0, time.UTC) @@ -52,14 +48,12 @@ func TestServiceMetricsSeriesCmd(t *testing.T) { { name: "key=value filter builds an EQUAL request", args: args("ordinal=0"), - opts: []runOption{experimental}, mock: expectSeries([]api.MetricLabelFilter{{Key: "ordinal", Value: "0"}}), wantStdout: noDataMsg, }, { name: "key!=value filter builds a NOT_EQUAL request", args: args("role!=replica"), - opts: []runOption{experimental}, mock: expectSeries([]api.MetricLabelFilter{ {Key: "role", Value: "replica", MatchType: new(api.MetricMatchTypeNOTEQUAL)}, }), @@ -68,13 +62,11 @@ func TestServiceMetricsSeriesCmd(t *testing.T) { { name: "malformed filter", args: args("nokeyvalue"), - opts: []runOption{experimental}, wantErr: `--filter must be name=value or name!=value, got "nokeyvalue"`, }, { name: "filter missing a value after !=", args: args("role!="), - opts: []runOption{experimental}, wantErr: `--filter must be name=value or name!=value, got "role!="`, }, { @@ -97,7 +89,6 @@ func TestServiceMetricsSeriesCmd(t *testing.T) { "--group-by", "role", "--group-by", "ordinal", }, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { expectResolveRefID(m, "svc-12345") empty := []api.MetricSeries{} diff --git a/internal/cmd/service_test.go b/internal/cmd/service_test.go index db956c06..3df53f49 100644 --- a/internal/cmd/service_test.go +++ b/internal/cmd/service_test.go @@ -41,10 +41,11 @@ func TestServiceCommandAliases(t *testing.T) { }) } -// TestServiceExperimentalGate covers the registration gate for the preview -// subtrees (`service metrics`, `service backup`): buildServiceCmd only adds -// them when the experimental env var is truthy, so by default the commands -// don't exist in the tree at all. Help output is asserted loosely +// TestServiceExperimentalGate covers the registration gate for the remaining +// preview subtree (`service backup`): buildServiceCmd only adds it when the +// experimental env var is truthy, so by default the command doesn't exist in +// the tree at all. `service metrics` graduated off this gate and is asserted +// present regardless of the flag. Help output is asserted loosely // (containment) rather than exactly, so the gate test doesn't break every time // an unrelated subcommand's help text changes. func TestServiceExperimentalGate(t *testing.T) { @@ -68,15 +69,14 @@ func TestServiceExperimentalGate(t *testing.T) { // Cobra only reports "unknown command" at the root level; for a // non-root group it treats the unknown name as a stray argument // and prints the group's help. The gate is observable as the - // absence of any gated entry in that help. - name: "unregistered by default", - args: []string{"service", "metrics"}, - wantStdout: matchHelp("Available Commands:", "metrics", "backup"), + // absence of the gated entry in that help. + name: "backup unregistered by default", + args: []string{"service", "backup"}, + wantStdout: matchHelp("Available Commands:", "backup"), }, { - name: "metrics registered when experimental", + name: "metrics registered by default", args: []string{"service", "metrics", "--help"}, - opts: []runOption{withEnv("TIGER_EXPERIMENTAL", "true")}, wantStdout: matchHelp("Commands for querying time-series metrics"), }, { diff --git a/internal/mcp/server.go b/internal/mcp/server.go index d6915fdf..74078641 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -95,22 +95,18 @@ func buildServerInstructions(cfg *config.Config, experimental bool) string { intro = "Tiger MCP provides tools for managing and querying Tiger Cloud database services (managed TimescaleDB/PostgreSQL). " // capabilitiesBase and readOnlyCapabilitiesBase describe what the server - // can do in each mode. metricsMention, when non-empty, folds into - // whichever one is used so the result still reads as one sentence. + // can do in each mode. metricsMention folds into whichever one is used so + // the result still reads as one sentence. capabilitiesBase = "Use it to provision and fork services, start/stop/resize/delete instances, rotate credentials, fetch service logs, execute SQL queries, and search Tiger documentation." readOnlyCapabilitiesBase = "Use it to list and inspect services, fetch service logs, execute read-only SQL queries, and search Tiger documentation." - // Metrics tools are registered regardless of read-only mode (they're all - // read-only themselves), so this folds into capabilities in every branch. + // Metrics tools are always registered and are all read-only themselves, + // so this folds into capabilities in every branch, regardless of mode. metricsMention = " Available metrics span hardware/resource usage, PostgreSQL settings, PgBouncer stats, database activity, and TimescaleDB internals." ) - metrics := "" - if experimental { - metrics = metricsMention - } - capabilities := capabilitiesBase + metrics - readOnlyCapabilities := readOnlyCapabilitiesBase + metrics + capabilities := capabilitiesBase + metricsMention + readOnlyCapabilities := readOnlyCapabilitiesBase + metricsMention switch cfg.ReadOnly { case config.ReadOnlyAll: @@ -218,13 +214,13 @@ func (s *Server) registerServiceTools(mode config.ReadOnlyMode, experimental boo addTool(s, mode, newServiceRenameTool(), s.handleServiceRename) addTool(s, mode, newServiceDeleteTool(), s.handleServiceDelete) addTool(s, mode, newServiceLogsTool(), s.handleServiceLogs) + addTool(s, mode, newServiceMetricsAvailableTool(), s.handleServiceMetricsAvailable) + addTool(s, mode, newServiceMetricsDetailsTool(), s.handleServiceMetricsDetails) + addTool(s, mode, newServiceMetricsSeriesTool(), s.handleServiceMetricsSeries) - // Metrics tools target gateway endpoints marked `x-tigerdata-preview: true`. They - // are registered only when the experimental gate is on at server startup. + // Backups target a gateway endpoint marked `x-tigerdata-preview: true`. It is + // registered only when the experimental gate is on at server startup. if experimental { - addTool(s, mode, newServiceMetricsAvailableTool(), s.handleServiceMetricsAvailable) - addTool(s, mode, newServiceMetricsDetailsTool(), s.handleServiceMetricsDetails) - addTool(s, mode, newServiceMetricsSeriesTool(), s.handleServiceMetricsSeries) addTool(s, mode, newServiceBackupListTool(), s.handleServiceBackupList) addTool(s, mode, newServiceBackupRegionListTool(), s.handleServiceBackupRegionList) addTool(s, mode, newServiceBackupRegionAddTool(), s.handleServiceBackupRegionAdd) diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 3db749b3..2b99caa3 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -133,21 +133,15 @@ func TestBuildServerInstructions(t *testing.T) { t.Errorf("prod-mode instructions should explain that PROD services are refused: %q", prodOnly) } - // Metrics tools are only mentioned when the experimental gate is on — - // otherwise they aren't registered, so mentioning them would mislead. + // Metrics tools are always registered, so the metrics blurb is always + // present — independent of the (now backups-only) experimental bool. const metricsMarker = "Available metrics span" - for _, got := range []string{readWrite, readOnly, prodOnly} { - if strings.Contains(got, metricsMarker) { - t.Errorf("instructions should not mention metrics tools when experimental is off: %q", got) - } - } - readWriteExperimental := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyOff}, true) readOnlyExperimental := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyAll}, true) prodOnlyExperimental := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyProd}, true) - for _, got := range []string{readWriteExperimental, readOnlyExperimental, prodOnlyExperimental} { + for _, got := range []string{readWrite, readOnly, prodOnly, readWriteExperimental, readOnlyExperimental, prodOnlyExperimental} { if !strings.Contains(got, metricsMarker) { - t.Errorf("instructions should mention metrics tools when experimental is on: %q", got) + t.Errorf("instructions should always mention metrics tools: %q", got) } } } diff --git a/internal/mcp/service_metrics_available_test.go b/internal/mcp/service_metrics_available_test.go index f4be9479..3acb3be8 100644 --- a/internal/mcp/service_metrics_available_test.go +++ b/internal/mcp/service_metrics_available_test.go @@ -12,10 +12,6 @@ import ( func TestServiceMetricsAvailableTool(t *testing.T) { args := map[string]any{"service_id": "e6ue9697jf"} - // The tool is experimental-gated (see the first case), so every other - // case registers it explicitly. - experimental := withExperimental() - expectSeries := func(series *[]string) func(*mocks.MockClientWithResponsesInterface) { return func(m *mocks.MockClientWithResponsesInterface) { m.EXPECT().GetServiceMetricsAvailableSeriesWithResponse(validCtx, testProjectID, "e6ue9697jf"). @@ -28,41 +24,29 @@ func TestServiceMetricsAvailableTool(t *testing.T) { noSeries := map[string]any{"series": []any{}} runToolTests(t, []toolTest{ - { - // The tool is gated at registration, so without the experimental - // gate the server never advertises it and the SDK refuses the call - // itself — a transport error rather than a result. - name: "not registered without the experimental gate", - tool: toolServiceMetricsAvailable, - args: args, - wantCallErr: `calling "tools/call": unknown tool "service_metrics_available"`, - }, { name: "not logged in", tool: toolServiceMetricsAvailable, args: args, - opts: []runOption{experimental, withNotLoggedIn()}, + opts: []runOption{withNotLoggedIn()}, wantErr: notLoggedInMsg, }, { name: "service ID failing the schema pattern", tool: toolServiceMetricsAvailable, args: map[string]any{"service_id": "NOPE"}, - opts: []runOption{experimental}, wantErr: `validating "arguments": validating root: validating /properties/service_id: pattern: "NOPE" does not match regular expression "^[a-z0-9]{10}$"`, }, { name: "missing service ID", tool: toolServiceMetricsAvailable, args: map[string]any{}, - opts: []runOption{experimental}, wantErr: `validating "arguments": validating root: required: missing properties: ["service_id"]`, }, { name: "network error", tool: toolServiceMetricsAvailable, args: args, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { m.EXPECT().GetServiceMetricsAvailableSeriesWithResponse(validCtx, testProjectID, "e6ue9697jf"). Return(nil, errors.New("connection refused")) @@ -73,7 +57,6 @@ func TestServiceMetricsAvailableTool(t *testing.T) { name: "API error", tool: toolServiceMetricsAvailable, args: args, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { m.EXPECT().GetServiceMetricsAvailableSeriesWithResponse(validCtx, testProjectID, "e6ue9697jf"). Return(&api.GetServiceMetricsAvailableSeriesResponse{ @@ -87,7 +70,6 @@ func TestServiceMetricsAvailableTool(t *testing.T) { name: "API error without a message body", tool: toolServiceMetricsAvailable, args: args, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { m.EXPECT().GetServiceMetricsAvailableSeriesWithResponse(validCtx, testProjectID, "e6ue9697jf"). Return(&api.GetServiceMetricsAvailableSeriesResponse{ @@ -101,7 +83,6 @@ func TestServiceMetricsAvailableTool(t *testing.T) { name: "nil response body", tool: toolServiceMetricsAvailable, args: args, - opts: []runOption{experimental}, mock: expectSeries(nil), wantOutput: noSeries, }, @@ -111,7 +92,6 @@ func TestServiceMetricsAvailableTool(t *testing.T) { name: "null series array", tool: toolServiceMetricsAvailable, args: args, - opts: []runOption{experimental}, mock: expectSeries(new([]string)), wantOutput: noSeries, }, @@ -119,7 +99,6 @@ func TestServiceMetricsAvailableTool(t *testing.T) { name: "no series available", tool: toolServiceMetricsAvailable, args: args, - opts: []runOption{experimental}, mock: expectSeries(&[]string{}), wantOutput: noSeries, }, @@ -127,7 +106,6 @@ func TestServiceMetricsAvailableTool(t *testing.T) { name: "series listed", tool: toolServiceMetricsAvailable, args: args, - opts: []runOption{experimental}, mock: expectSeries(&[]string{ "timescale_cloud_system_cpu_usage_millicores", "timescale_cloud_system_memory_usage_bytes", diff --git a/internal/mcp/service_metrics_details_test.go b/internal/mcp/service_metrics_details_test.go index 3e57596b..b408abeb 100644 --- a/internal/mcp/service_metrics_details_test.go +++ b/internal/mcp/service_metrics_details_test.go @@ -12,10 +12,6 @@ import ( func TestServiceMetricsDetailsTool(t *testing.T) { args := map[string]any{"service_id": "e6ue9697jf", "metric_name": "pg_stat_activity_count"} - // The tool is experimental-gated (see the first case), so every other - // case registers it explicitly. - experimental := withExperimental() - expectDetails := func(metric string, details *api.MetricDetails) func(*mocks.MockClientWithResponsesInterface) { return func(m *mocks.MockClientWithResponsesInterface) { m.EXPECT().GetServiceMetricDetailsWithResponse(validCtx, testProjectID, "e6ue9697jf", metric). @@ -50,41 +46,29 @@ func TestServiceMetricsDetailsTool(t *testing.T) { }} runToolTests(t, []toolTest{ - { - // The tool is gated at registration, so without the experimental - // gate the server never advertises it and the SDK refuses the call - // itself — a transport error rather than a result. - name: "not registered without the experimental gate", - tool: toolServiceMetricsDetails, - args: args, - wantCallErr: `calling "tools/call": unknown tool "service_metrics_details"`, - }, { name: "not logged in", tool: toolServiceMetricsDetails, args: args, - opts: []runOption{experimental, withNotLoggedIn()}, + opts: []runOption{withNotLoggedIn()}, wantErr: notLoggedInMsg, }, { name: "service ID failing the schema pattern", tool: toolServiceMetricsDetails, args: map[string]any{"service_id": "NOPE", "metric_name": "pg_stat_activity_count"}, - opts: []runOption{experimental}, wantErr: `validating "arguments": validating root: validating /properties/service_id: pattern: "NOPE" does not match regular expression "^[a-z0-9]{10}$"`, }, { name: "missing metric name", tool: toolServiceMetricsDetails, args: map[string]any{"service_id": "e6ue9697jf"}, - opts: []runOption{experimental}, wantErr: `validating "arguments": validating root: required: missing properties: ["metric_name"]`, }, { name: "network error", tool: toolServiceMetricsDetails, args: args, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { m.EXPECT().GetServiceMetricDetailsWithResponse(validCtx, testProjectID, "e6ue9697jf", "pg_stat_activity_count"). Return(nil, errors.New("connection refused")) @@ -95,7 +79,6 @@ func TestServiceMetricsDetailsTool(t *testing.T) { name: "API error", tool: toolServiceMetricsDetails, args: args, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { m.EXPECT().GetServiceMetricDetailsWithResponse(validCtx, testProjectID, "e6ue9697jf", "pg_stat_activity_count"). Return(&api.GetServiceMetricDetailsResponse{ @@ -109,7 +92,6 @@ func TestServiceMetricsDetailsTool(t *testing.T) { name: "API error without a message body", tool: toolServiceMetricsDetails, args: args, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { m.EXPECT().GetServiceMetricDetailsWithResponse(validCtx, testProjectID, "e6ue9697jf", "pg_stat_activity_count"). Return(&api.GetServiceMetricDetailsResponse{ @@ -122,7 +104,6 @@ func TestServiceMetricsDetailsTool(t *testing.T) { name: "nil response body", tool: toolServiceMetricsDetails, args: args, - opts: []runOption{experimental}, mock: expectDetails("pg_stat_activity_count", nil), wantErr: "empty response from API", }, @@ -130,7 +111,6 @@ func TestServiceMetricsDetailsTool(t *testing.T) { name: "documented metric", tool: toolServiceMetricsDetails, args: args, - opts: []runOption{experimental}, mock: expectDetails("pg_stat_activity_count", documented), wantOutput: wantDocumented, }, @@ -140,7 +120,6 @@ func TestServiceMetricsDetailsTool(t *testing.T) { name: "undocumented metric", tool: toolServiceMetricsDetails, args: map[string]any{"service_id": "e6ue9697jf", "metric_name": "some_new_metric"}, - opts: []runOption{experimental}, mock: expectDetails("some_new_metric", &api.MetricDetails{Name: "some_new_metric"}), wantOutput: map[string]any{"details": map[string]any{ "name": "some_new_metric", diff --git a/internal/mcp/service_metrics_series_test.go b/internal/mcp/service_metrics_series_test.go index 79ead6ec..974aacc6 100644 --- a/internal/mcp/service_metrics_series_test.go +++ b/internal/mcp/service_metrics_series_test.go @@ -24,10 +24,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { return out } - // The tool is experimental-gated (see the first case), so every other - // case registers it explicitly. - experimental := withExperimental() - from := time.Date(2026, 5, 13, 0, 0, 0, 0, time.UTC) to := time.Date(2026, 5, 13, 1, 0, 0, 0, time.UTC) baseBody := api.MetricsSeriesRequest{ @@ -91,55 +87,41 @@ func TestServiceMetricsSeriesTool(t *testing.T) { noSeries := map[string]any{"series": []any{}} runToolTests(t, []toolTest{ - { - // The tool is gated at registration, so without the experimental - // gate the server never advertises it and the SDK refuses the call - // itself — a transport error rather than a result. - name: "not registered without the experimental gate", - tool: toolServiceMetricsSeries, - args: baseArgs, - wantCallErr: `calling "tools/call": unknown tool "service_metrics_series"`, - }, { name: "not logged in", tool: toolServiceMetricsSeries, args: baseArgs, - opts: []runOption{experimental, withNotLoggedIn()}, + opts: []runOption{withNotLoggedIn()}, wantErr: notLoggedInMsg, }, { name: "no arguments", tool: toolServiceMetricsSeries, args: map[string]any{}, - opts: []runOption{experimental}, wantErr: `validating "arguments": validating root: required: missing properties: ["service_id" "metric_name" "from" "to"]`, }, { name: "service ID failing the schema pattern", tool: toolServiceMetricsSeries, args: args(map[string]any{"service_id": "NOPE"}), - opts: []runOption{experimental}, wantErr: `validating "arguments": validating root: validating /properties/service_id: pattern: "NOPE" does not match regular expression "^[a-z0-9]{10}$"`, }, { name: "role outside the enum", tool: toolServiceMetricsSeries, args: args(map[string]any{"role": "primary"}), - opts: []runOption{experimental}, wantErr: `validating "arguments": validating root: validating /properties/role: enum: primary does not equal any of: [PRIMARY REPLICA]`, }, { name: "aggregation function outside the enum", tool: toolServiceMetricsSeries, args: args(map[string]any{"fn": "MEDIAN"}), - opts: []runOption{experimental}, wantErr: `validating "arguments": validating root: validating /properties/fn: enum: MEDIAN does not equal any of: [RATE INCREASE SUM AVG MIN MAX MIN_TOTAL MAX_TOTAL COUNT P50 P90 P99 LAST]`, }, { name: "bucket below the minimum", tool: toolServiceMetricsSeries, args: args(map[string]any{"bucket_seconds": 30}), - opts: []runOption{experimental}, wantErr: `validating "arguments": validating root: validating /properties/bucket_seconds: minimum: 30/1 is less than 60.000000`, }, { @@ -148,21 +130,18 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "from is not RFC3339", tool: toolServiceMetricsSeries, args: args(map[string]any{"from": "yesterday"}), - opts: []runOption{experimental}, wantErr: `from must be RFC3339 (e.g., 2026-05-13T00:00:00Z): parsing time "yesterday" as "2006-01-02T15:04:05Z07:00": cannot parse "yesterday" as "2006"`, }, { name: "to is not RFC3339", tool: toolServiceMetricsSeries, args: args(map[string]any{"to": "2026-05-13"}), - opts: []runOption{experimental}, wantErr: `to must be RFC3339 (e.g., 2026-05-13T01:00:00Z): parsing time "2026-05-13" as "2006-01-02T15:04:05Z07:00": cannot parse "" as "T"`, }, { name: "network error", tool: toolServiceMetricsSeries, args: baseArgs, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { m.EXPECT().GetServiceMetricsSeriesWithResponse(validCtx, testProjectID, "e6ue9697jf", baseBody). Return(nil, errors.New("connection refused")) @@ -173,7 +152,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "API error", tool: toolServiceMetricsSeries, args: baseArgs, - opts: []runOption{experimental}, mock: expectError(http.StatusBadRequest, &api.ClientError{Message: new("unknown metric name")}), wantErr: "unknown metric name", }, @@ -181,7 +159,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "API error without a message body", tool: toolServiceMetricsSeries, args: baseArgs, - opts: []runOption{experimental}, mock: expectError(http.StatusInternalServerError, nil), wantErr: "unknown error", }, @@ -190,7 +167,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "nil response body", tool: toolServiceMetricsSeries, args: baseArgs, - opts: []runOption{experimental}, mock: expectSeries(baseBody, nil), wantOutput: noSeries, }, @@ -200,7 +176,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "null series array", tool: toolServiceMetricsSeries, args: baseArgs, - opts: []runOption{experimental}, mock: expectSeries(baseBody, new([]api.MetricSeries)), wantOutput: noSeries, }, @@ -208,7 +183,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "no data in the window", tool: toolServiceMetricsSeries, args: baseArgs, - opts: []runOption{experimental}, mock: expectSeries(baseBody, &[]api.MetricSeries{}), wantOutput: noSeries, }, @@ -216,7 +190,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "series returned", tool: toolServiceMetricsSeries, args: baseArgs, - opts: []runOption{experimental}, mock: expectSeries(baseBody, &series), wantOutput: wantSeries, }, @@ -225,7 +198,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "role filter", tool: toolServiceMetricsSeries, args: args(map[string]any{"role": "REPLICA"}), - opts: []runOption{experimental}, mock: expectSeries(body(func(b *api.MetricsSeriesRequest) { b.Filters = &[]api.MetricLabelFilter{{Key: "role", Value: "replica"}} }), &[]api.MetricSeries{}), @@ -243,7 +215,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { map[string]any{"key": "job_id", "value": ""}, }, }), - opts: []runOption{experimental}, mock: expectSeries(body(func(b *api.MetricsSeriesRequest) { b.Filters = &[]api.MetricLabelFilter{ {Key: "role", Value: "primary"}, @@ -259,7 +230,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "filter without match_type omits it from the request", tool: toolServiceMetricsSeries, args: args(map[string]any{"filters": []any{map[string]any{"key": "ordinal", "value": "0"}}}), - opts: []runOption{experimental}, mock: expectSeries(body(func(b *api.MetricsSeriesRequest) { b.Filters = &[]api.MetricLabelFilter{{Key: "ordinal", Value: "0"}} }), &[]api.MetricSeries{}), @@ -269,7 +239,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "NOT_EQUAL match_type reaches the request", tool: toolServiceMetricsSeries, args: args(map[string]any{"filters": []any{map[string]any{"key": "role", "value": "replica", "match_type": "NOT_EQUAL"}}}), - opts: []runOption{experimental}, mock: expectSeries(body(func(b *api.MetricsSeriesRequest) { b.Filters = &[]api.MetricLabelFilter{{Key: "role", Value: "replica", MatchType: new(api.MetricMatchTypeNOTEQUAL)}} }), &[]api.MetricSeries{}), @@ -279,14 +248,12 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "match_type outside the enum", tool: toolServiceMetricsSeries, args: args(map[string]any{"filters": []any{map[string]any{"key": "role", "value": "replica", "match_type": "REGEX"}}}), - opts: []runOption{experimental}, wantErr: `validating "arguments": validating root: validating /properties/filters: validating /properties/filters/items: validating /properties/filters/items/properties/match_type: enum: REGEX does not equal any of: [EQUAL NOT_EQUAL]`, }, { name: "group_by reaches the request", tool: toolServiceMetricsSeries, args: args(map[string]any{"group_by": []any{"role", "ordinal"}}), - opts: []runOption{experimental}, mock: expectSeries(body(func(b *api.MetricsSeriesRequest) { b.GroupBy = &[]string{"role", "ordinal"} }), &[]api.MetricSeries{}), @@ -297,7 +264,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "empty group_by list", tool: toolServiceMetricsSeries, args: args(map[string]any{"group_by": []any{}}), - opts: []runOption{experimental}, mock: expectSeries(baseBody, &[]api.MetricSeries{}), wantOutput: noSeries, }, @@ -305,7 +271,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "bucket size and aggregation function", tool: toolServiceMetricsSeries, args: args(map[string]any{"bucket_seconds": 3600, "fn": "AVG"}), - opts: []runOption{experimental}, mock: expectSeries(body(func(b *api.MetricsSeriesRequest) { b.BucketSeconds = new(3600) b.Fn = new(api.MetricsAggFnAVG) @@ -317,7 +282,6 @@ func TestServiceMetricsSeriesTool(t *testing.T) { name: "empty filter list", tool: toolServiceMetricsSeries, args: args(map[string]any{"filters": []any{}}), - opts: []runOption{experimental}, mock: expectSeries(baseBody, &[]api.MetricSeries{}), wantOutput: noSeries, }, From 4e9cb3b9bf732235379f405b4fe9bc801fb9cee6 Mon Sep 17 00:00:00 2001 From: Adrin Lopez Calvo Date: Mon, 28 Sep 2026 19:07:28 +0200 Subject: [PATCH 2/5] Drop now-unused experimental param from buildServerInstructions Metrics are unconditional now, so the bool had no remaining effect. --- internal/mcp/server.go | 4 ++-- internal/mcp/server_test.go | 14 +++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 74078641..af35d971 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -90,7 +90,7 @@ func addTool[In, Out any](s *Server, mode config.ReadOnlyMode, t *mcp.Tool, h mc // buildServerInstructions returns the `instructions` string the MCP SDK sends // to clients at initialize. Evaluated once at server start, like tool registration. -func buildServerInstructions(cfg *config.Config, experimental bool) string { +func buildServerInstructions(cfg *config.Config) string { const ( intro = "Tiger MCP provides tools for managing and querying Tiger Cloud database services (managed TimescaleDB/PostgreSQL). " @@ -140,7 +140,7 @@ func NewServer(ctx context.Context, app *common.App, logger *slog.Logger) (*Serv Title: serverTitle, Version: config.Version, }, &mcp.ServerOptions{ - Instructions: buildServerInstructions(cfg, app.Experimental), + Instructions: buildServerInstructions(cfg), Logger: logger, }) diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 2b99caa3..9261336f 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -100,9 +100,9 @@ func TestReadOnlyToolRegistration(t *testing.T) { func TestBuildServerInstructions(t *testing.T) { const capabilitiesMarker = "Tiger MCP provides tools" - readWrite := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyOff}, false) - readOnly := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyAll}, false) - prodOnly := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyProd}, false) + readWrite := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyOff}) + readOnly := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyAll}) + prodOnly := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyProd}) // The capabilities blurb is always present, whatever the mode. for _, got := range []string{readWrite, readOnly, prodOnly} { @@ -133,13 +133,9 @@ func TestBuildServerInstructions(t *testing.T) { t.Errorf("prod-mode instructions should explain that PROD services are refused: %q", prodOnly) } - // Metrics tools are always registered, so the metrics blurb is always - // present — independent of the (now backups-only) experimental bool. + // Metrics tools are always registered, so the metrics blurb is always present. const metricsMarker = "Available metrics span" - readWriteExperimental := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyOff}, true) - readOnlyExperimental := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyAll}, true) - prodOnlyExperimental := buildServerInstructions(&config.Config{ReadOnly: config.ReadOnlyProd}, true) - for _, got := range []string{readWrite, readOnly, prodOnly, readWriteExperimental, readOnlyExperimental, prodOnlyExperimental} { + for _, got := range []string{readWrite, readOnly, prodOnly} { if !strings.Contains(got, metricsMarker) { t.Errorf("instructions should always mention metrics tools: %q", got) } From 2d1c09d5187756bbf254bc51c44e95f36fed14b2 Mon Sep 17 00:00:00 2001 From: Adrin Lopez Calvo Date: Mon, 28 Sep 2026 19:15:14 +0200 Subject: [PATCH 3/5] Revert CLAUDE.md changes Not needed for this PR. --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7c4d1590..90f9a31b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -67,7 +67,7 @@ Generated files are marked `-diff linguist-generated=true` in `.gitattributes`: ## CLI Reference Docs -`docs/cli/` is a generated Markdown reference with one file per command, produced by `cmd/generate-docs`, which walks the command tree with cobra's `doc` package. It runs under `go generate ./...`, so any change to a command, flag, or help text needs a regeneration (CI fails on a stale or missing file). The tool forces `TIGER_EXPERIMENTAL` off for the run, so gated commands (currently `backups`) never reach the docs. +`docs/cli/` is a generated Markdown reference with one file per command, produced by `cmd/generate-docs`, which walks the command tree with cobra's `doc` package. It runs under `go generate ./...`, so any change to a command, flag, or help text needs a regeneration (CI fails on a stale or missing file). The tool forces `TIGER_EXPERIMENTAL` off for the run, so gated commands never reach the docs. ## Command Architecture @@ -108,7 +108,7 @@ Configuration is layered, with precedence **flags > `TIGER_*` env vars > config ### Experimental Feature Gating -`TIGER_EXPERIMENTAL` gates commands and MCP tools that aren't ready to be public yet — currently just `service backups`/`service_backups`, which is backed by a gateway endpoint marked `x-tigerdata-preview: true` in `openapi.yaml` (those request/response shapes are still in flux, so a surface built on one must always be gated). Metrics (`service metrics`/`service_metrics_*`) graduated off this gate and is now always registered. +`TIGER_EXPERIMENTAL` gates commands and MCP tools that aren't ready to be public yet, for whatever reason — including, but not limited to, anything backed by a gateway endpoint marked `x-tigerdata-preview: true` in `openapi.yaml` (those request/response shapes are still in flux, so a surface built on one must always be gated). It's an env var **only**: deliberately not a config key, not a flag, and hidden from `tiger config list`. `buildRootCmd` reads it once into `app.Experimental`, and the CLI guards its `AddCommand` calls with it while the MCP server guards its `addTool` calls, so when the env var is off the gated commands and tools don't exist at all — no help entry, no completion, not advertised to MCP clients (restart the MCP server after toggling). **Never mention `TIGER_EXPERIMENTAL` in user-facing docs, command help, or error messages.** When a feature graduates, delete the gates on both sides. From 45b80c5f3ac05afe8a671473513d38a2ffc58809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20L=C3=B3pez=20Calvo?= Date: Wed, 30 Sep 2026 10:46:18 +0200 Subject: [PATCH 4/5] CLI metrics UX: rename available-series, default series window, shell completions (#251) --- docs/cli/tiger_service_metrics.md | 2 +- ....md => tiger_service_metrics_available.md} | 9 +- docs/cli/tiger_service_metrics_details.md | 7 +- docs/cli/tiger_service_metrics_series.md | 21 +++-- internal/api/types.go | 8 +- internal/cmd/completion_helper.go | 29 ++++++ internal/cmd/mcp_get_test.go | 2 +- internal/cmd/service_metrics.go | 2 +- ...series.go => service_metrics_available.go} | 6 +- .../cmd/service_metrics_available_test.go | 93 +++++++++++++++++++ internal/cmd/service_metrics_details.go | 5 +- internal/cmd/service_metrics_details_test.go | 8 +- internal/cmd/service_metrics_series.go | 52 ++++++++--- internal/cmd/service_metrics_series_test.go | 68 ++++++++++++-- internal/mcp/service_metrics_details.go | 2 +- internal/mcp/service_metrics_details_test.go | 8 +- internal/mcp/service_metrics_series.go | 6 +- internal/mcp/service_metrics_series_test.go | 6 +- openapi.yaml | 8 +- 19 files changed, 280 insertions(+), 62 deletions(-) rename docs/cli/{tiger_service_metrics_available-series.md => tiger_service_metrics_available.md} (76%) rename internal/cmd/{service_metrics_available_series.go => service_metrics_available.go} (89%) create mode 100644 internal/cmd/service_metrics_available_test.go diff --git a/docs/cli/tiger_service_metrics.md b/docs/cli/tiger_service_metrics.md index 4c328c91..dd19d442 100644 --- a/docs/cli/tiger_service_metrics.md +++ b/docs/cli/tiger_service_metrics.md @@ -26,6 +26,6 @@ Commands for querying time-series metrics for a Tiger Cloud service. ### SEE ALSO * [tiger service](tiger_service.md) - Manage database services -* [tiger service metrics available-series](tiger_service_metrics_available-series.md) - List available metric series +* [tiger service metrics available](tiger_service_metrics_available.md) - List available metric series * [tiger service metrics details](tiger_service_metrics_details.md) - Get metric details * [tiger service metrics series](tiger_service_metrics_series.md) - Get metric series data diff --git a/docs/cli/tiger_service_metrics_available-series.md b/docs/cli/tiger_service_metrics_available.md similarity index 76% rename from docs/cli/tiger_service_metrics_available-series.md rename to docs/cli/tiger_service_metrics_available.md index 10c580de..f2c1f87c 100644 --- a/docs/cli/tiger_service_metrics_available-series.md +++ b/docs/cli/tiger_service_metrics_available.md @@ -1,4 +1,4 @@ -## tiger service metrics available-series +## tiger service metrics available List available metric series @@ -6,14 +6,17 @@ List available metric series List the names of all metric series available for a service. +The service can be given by ID or name as an argument, or will use the default +service from your configuration. + ``` -tiger service metrics available-series [service-id] [flags] +tiger service metrics available [name-or-id] [flags] ``` ### Options ``` - -h, --help help for available-series + -h, --help help for available -o, --output string Output format (json, yaml, table) ``` diff --git a/docs/cli/tiger_service_metrics_details.md b/docs/cli/tiger_service_metrics_details.md index 3098f1e3..46de9551 100644 --- a/docs/cli/tiger_service_metrics_details.md +++ b/docs/cli/tiger_service_metrics_details.md @@ -7,14 +7,17 @@ Get metric details Get descriptive metadata for a metric: what it measures, its type, default aggregation function, and available labels. -Use 'tiger service metrics available-series' to discover valid metric names, +Use 'tiger service metrics available' to discover valid metric names, then 'tiger service metrics series' to fetch its data. These metrics have no richer metadata — expect just the name back, with type, default aggregation, description, and labels all empty: timescale_cloud_system_cpu_total_millicores, timescale_cloud_system_cpu_usage_millicores, timescale_cloud_system_disk_io_read_bytes, timescale_cloud_system_disk_io_read_ops, timescale_cloud_system_disk_io_total_bytes, timescale_cloud_system_disk_io_total_ops, timescale_cloud_system_disk_io_write_bytes, timescale_cloud_system_disk_io_write_ops, timescale_cloud_system_disk_usage_bytes, timescale_cloud_system_memory_total_bytes, timescale_cloud_system_memory_usage_bytes, timescale_cloud_database_qps, timescale_cloud_database_num_connections, timescale_cloud_database_job_duration_usecs, timescale_cloud_database_job_success. +The service can be given by ID or name as an argument, or will use the default +service from your configuration. + ``` -tiger service metrics details [service-id] [flags] +tiger service metrics details [name-or-id] [flags] ``` ### Examples diff --git a/docs/cli/tiger_service_metrics_series.md b/docs/cli/tiger_service_metrics_series.md index d08b19a1..ef9ec30d 100644 --- a/docs/cli/tiger_service_metrics_series.md +++ b/docs/cli/tiger_service_metrics_series.md @@ -6,19 +6,28 @@ Get metric series data Get time-series data for a specific metric. -Use 'tiger service metrics available-series' to discover valid metric names. +Use 'tiger service metrics available' to discover valid metric names. Each labeled series (e.g. one per replica) is returned independently with its full list of raw data points. +The service can be given by ID or name as an argument, or will use the default +service from your configuration. + +--from and --to default to the last 24 hours when both are omitted, bucketed +into 1-hour (3600s) intervals unless --bucket-seconds is also given. + ``` -tiger service metrics series [service-id] [flags] +tiger service metrics series [name-or-id] [flags] ``` ### Examples ``` - # Fetch CPU usage for the last hour + # Fetch CPU usage for the last 24 hours (the default window) + tiger service metrics series --metric timescale_cloud_system_cpu_usage_millicores + + # Fetch CPU usage for a specific hour tiger service metrics series --metric timescale_cloud_system_cpu_usage_millicores \ --from 2026-05-13T00:00:00Z --to 2026-05-13T01:00:00Z @@ -49,16 +58,16 @@ tiger service metrics series [service-id] [flags] ### Options ``` - --bucket-seconds int Aggregation bucket size in seconds (optional; server auto-selects based on the time window when omitted, minimum 60s) + --bucket-seconds int Aggregation bucket size in seconds (minimum 60s). Defaults to 3600 (1h) when --from/--to are also omitted; otherwise the server auto-selects based on the time window --filter strings Arbitrary label filter as name=value or name!=value (repeatable) --fn string Aggregation function applied per bucket. One of: RATE, INCREASE, SUM, AVG, MIN, MAX, MIN_TOTAL, MAX_TOTAL, COUNT, P50, P90, P99, LAST. Rejected on the timescale_cloud_* resource/qps/connections/jobs metrics; omit to let the server pick the default - --from string Start of the time window (RFC3339) + --from string Start of the time window (RFC3339). Defaults to 24 hours ago when --to is also omitted --group-by strings Label key to break the result into one series per distinct value (repeatable). Rejected on the same metrics that reject --fn; omit to collapse into a single series -h, --help help for series --metric string Metric series name -o, --output string Output format (json, yaml, table) --role string Filter to a specific instance role (PRIMARY or REPLICA) - --to string End of the time window (RFC3339) + --to string End of the time window (RFC3339). Defaults to now when --from is also omitted ``` ### Options inherited from parent commands diff --git a/internal/api/types.go b/internal/api/types.go index 5490f842..b3b08f55 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -1821,10 +1821,10 @@ type MetricDetails struct { // pgbouncer-sourced metrics only get region, not role/ordinal. Labels []MetricLabelDetails `json:"labels"` - // Name Metric series name. + // MetricName Metric series name. // // Example: pg_locks_count - Name string `json:"name"` + MetricName string `json:"metric_name"` // Type The shape of this metric's data, or null if undocumented. // @@ -2004,10 +2004,10 @@ type MetricsSeriesRequest struct { // Example: ["role"] GroupBy *[]string `json:"group_by,omitempty"` - // Name Metric series name. Use getServiceMetricsAvailableSeries to discover valid values. + // MetricName Metric series name. Use getServiceMetricsAvailableSeries to discover valid values. // // Example: timescale_cloud_system_cpu_usage_millicores - Name string `json:"name"` + MetricName string `json:"metric_name"` // To End of the time window (RFC3339; nanosecond precision accepted). // diff --git a/internal/cmd/completion_helper.go b/internal/cmd/completion_helper.go index 09297508..8e88894a 100644 --- a/internal/cmd/completion_helper.go +++ b/internal/cmd/completion_helper.go @@ -184,6 +184,35 @@ var addonsCompletion = cobra.FixedCompletions(append(common.ValidAddons(), commo // drawn from the same list used to validate `tiger config set password_storage`. var passwordStorageCompletion = cobra.FixedCompletions(config.ValidPasswordStorageOptions(), cobra.ShellCompDirectiveNoFileComp) +// metricNameCompletion completes --metric on `service metrics details` and +// `service metrics series`, drawn from the target service's available metric +// series (the same list `service metrics available` returns). +func metricNameCompletion(app *common.App) cobra.CompletionFunc { + return withAppLoad(app, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + cfg, client, projectID, err := app.GetAll() + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + serviceRef, err := getServiceRef(cmd, cfg, args) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + service, err := resolveService(cmd.Context(), client, projectID, serviceRef) + if err != nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + resp, err := client.GetServiceMetricsAvailableSeriesWithResponse(cmd.Context(), projectID, service.ServiceID) + if err != nil || resp.StatusCode() != http.StatusOK || resp.JSON200 == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + return filterCompletionsByPrefix(*resp.JSON200, toComplete), cobra.ShellCompDirectiveNoFileComp + }) +} + // metricsSeriesRoleCompletion completes --role on `service metrics series`. var metricsSeriesRoleCompletion = cobra.FixedCompletions([]string{"PRIMARY", "REPLICA"}, cobra.ShellCompDirectiveNoFileComp) diff --git a/internal/cmd/mcp_get_test.go b/internal/cmd/mcp_get_test.go index 5754d36d..05bb9858 100644 --- a/internal/cmd/mcp_get_test.go +++ b/internal/cmd/mcp_get_test.go @@ -291,7 +291,7 @@ Output: • labels (required): []object, null - All labels this metric can be filtered or grouped by: its own labels (e.g. datname on pg_stat_database_*) plus the region/role/ordinal labels most metrics also carry. A few datasources only attach a subset of those — e.g. pgbouncer-sourced metrics only get region, not role/ordinal. • description (required): string - What this label identifies, or empty if undocumented. • name (required): string - The label's key. - • name (required): string - Metric series name. + • metric_name (required): string - Metric series name. • type (required): string, null - The shape of this metric's data, or null if undocumented. ` diff --git a/internal/cmd/service_metrics.go b/internal/cmd/service_metrics.go index 344806c8..3c8264e4 100644 --- a/internal/cmd/service_metrics.go +++ b/internal/cmd/service_metrics.go @@ -14,7 +14,7 @@ func buildServiceMetricsCmd(app *common.App) *cobra.Command { Short: "View service metrics", Long: `Commands for querying time-series metrics for a Tiger Cloud service.`, } - cmd.AddCommand(buildServiceMetricsAvailableSeriesCmd(app)) + cmd.AddCommand(buildServiceMetricsAvailableCmd(app)) cmd.AddCommand(buildServiceMetricsDetailsCmd(app)) cmd.AddCommand(buildServiceMetricsSeriesCmd(app)) return cmd diff --git a/internal/cmd/service_metrics_available_series.go b/internal/cmd/service_metrics_available.go similarity index 89% rename from internal/cmd/service_metrics_available_series.go rename to internal/cmd/service_metrics_available.go index 80abd0ef..c3550937 100644 --- a/internal/cmd/service_metrics_available_series.go +++ b/internal/cmd/service_metrics_available.go @@ -11,10 +11,10 @@ import ( "github.com/timescale/tiger-cli/internal/util" ) -// buildServiceMetricsAvailableSeriesCmd lists the metric series available for a service -func buildServiceMetricsAvailableSeriesCmd(app *common.App) *cobra.Command { +// buildServiceMetricsAvailableCmd lists the metric series available for a service +func buildServiceMetricsAvailableCmd(app *common.App) *cobra.Command { cmd := &cobra.Command{ - Use: "available-series [name-or-id]", + Use: "available [name-or-id]", Short: "List available metric series", Long: `List the names of all metric series available for a service. diff --git a/internal/cmd/service_metrics_available_test.go b/internal/cmd/service_metrics_available_test.go new file mode 100644 index 00000000..a951fc77 --- /dev/null +++ b/internal/cmd/service_metrics_available_test.go @@ -0,0 +1,93 @@ +package cmd + +import ( + "errors" + "net/http" + "testing" + + "github.com/timescale/tiger-cli/internal/api" + "github.com/timescale/tiger-cli/internal/api/mocks" + "github.com/timescale/tiger-cli/internal/common" +) + +func TestServiceMetricsAvailableCmd(t *testing.T) { + names := []string{"pg_stat_activity_count", "timescale_cloud_system_cpu_usage_millicores"} + + setupAvailable := func(names []string) func(m *mocks.MockClientWithResponsesInterface) { + return func(m *mocks.MockClientWithResponsesInterface) { + expectResolveRefID(m, "svc-12345") + m.EXPECT().GetServiceMetricsAvailableSeriesWithResponse(validCtx, testProjectID, "svc-12345"). + Return(&api.GetServiceMetricsAvailableSeriesResponse{ + HTTPResponse: httpResponse(http.StatusOK), + JSON200: &names, + }, nil) + } + } + + runCmdTests(t, []cmdTest{ + { + name: "not logged in", + args: []string{"service", "metrics", "available", "svc-12345"}, + opts: []runOption{withNotLoggedIn()}, + wantErr: notLoggedInMsg, + checks: []checkFunc{checkExitCode(common.ExitAuthenticationError)}, + }, + { + name: "missing service id", + args: []string{"service", "metrics", "available"}, + wantErr: "service name or ID is required. Provide it as an argument or set a default with 'tiger config set service_id '", + }, + { + name: "ambiguous name refused", + args: []string{"service", "metrics", "available", "my-api-db"}, + mock: func(m *mocks.MockClientWithResponsesInterface) { + expectResolveRefStatus(m, "my-api-db", http.StatusBadRequest, &api.Error{Message: new("ambiguous service name matches multiple services")}) + }, + wantErr: "ambiguous service name matches multiple services\nRun 'tiger service list' to find the ID you want", + checks: []checkFunc{checkExitCode(common.ExitInvalidParameters)}, + }, + { + name: "network error", + args: []string{"service", "metrics", "available", "svc-12345"}, + mock: func(m *mocks.MockClientWithResponsesInterface) { + expectResolveRefID(m, "svc-12345") + m.EXPECT().GetServiceMetricsAvailableSeriesWithResponse(validCtx, testProjectID, "svc-12345"). + Return(nil, errors.New("connection refused")) + }, + wantErr: "failed to list metric series: connection refused", + }, + { + name: "API error", + args: []string{"service", "metrics", "available", "svc-12345"}, + mock: func(m *mocks.MockClientWithResponsesInterface) { + expectResolveRefID(m, "svc-12345") + m.EXPECT().GetServiceMetricsAvailableSeriesWithResponse(validCtx, testProjectID, "svc-12345"). + Return(&api.GetServiceMetricsAvailableSeriesResponse{ + HTTPResponse: httpResponse(http.StatusNotFound), + JSON4XX: &api.ClientError{Message: new("service not found")}, + }, nil) + }, + wantErr: "service not found", + checks: []checkFunc{checkExitCode(common.ExitServiceNotFound)}, + }, + { + name: "table output", + args: []string{"service", "metrics", "available", "svc-12345"}, + mock: setupAvailable(names), + wantStdout: "pg_stat_activity_count\ntimescale_cloud_system_cpu_usage_millicores\n", + }, + { + name: "default service id from config", + args: []string{"service", "metrics", "available"}, + opts: []runOption{withConfig(map[string]any{"service_id": "svc-12345"})}, + mock: setupAvailable(names), + wantStdout: "pg_stat_activity_count\ntimescale_cloud_system_cpu_usage_millicores\n", + }, + { + name: "json output", + args: []string{"service", "metrics", "available", "svc-12345", "-o", "json"}, + mock: setupAvailable(names), + wantStdout: "[\n \"pg_stat_activity_count\",\n \"timescale_cloud_system_cpu_usage_millicores\"\n]\n", + }, + }) +} diff --git a/internal/cmd/service_metrics_details.go b/internal/cmd/service_metrics_details.go index 83353627..994bc5f1 100644 --- a/internal/cmd/service_metrics_details.go +++ b/internal/cmd/service_metrics_details.go @@ -25,7 +25,7 @@ func buildServiceMetricsDetailsCmd(app *common.App) *cobra.Command { Long: fmt.Sprintf(`Get descriptive metadata for a metric: what it measures, its type, default aggregation function, and available labels. -Use 'tiger service metrics available-series' to discover valid metric names, +Use 'tiger service metrics available' to discover valid metric names, then 'tiger service metrics series' to fetch its data. These metrics have no richer metadata — expect just the name back, with type, @@ -77,6 +77,7 @@ service from your configuration.`, strings.Join(common.LegacyMetrics, ", ")), cmd.Flags().StringVar(&metric, "metric", "", "Metric name") cmd.Flags().VarP(new(outputFlag), "output", "o", "Output format (json, yaml, table)") registerFlagCompletion(cmd, "output", outputCompletion()) + registerFlagCompletion(cmd, "metric", metricNameCompletion(app)) markFlagRequired(cmd, "metric") @@ -101,7 +102,7 @@ func outputMetricDetailsTable(details api.MetricDetails, output io.Writer) error table := tablewriter.NewWriter(output) table.Header("PROPERTY", "VALUE") - table.Append("Name", details.Name) + table.Append("Name", details.MetricName) metricType := "undocumented" if details.Type != nil { diff --git a/internal/cmd/service_metrics_details_test.go b/internal/cmd/service_metrics_details_test.go index 47e2f27c..f73845d8 100644 --- a/internal/cmd/service_metrics_details_test.go +++ b/internal/cmd/service_metrics_details_test.go @@ -18,7 +18,7 @@ func TestServiceMetricsDetailsCmd(t *testing.T) { // backend_type) carry no description yet, plus the region/role/ordinal // labels every registry-backed metric gets, which do. fullDetails := api.MetricDetails{ - Name: "pg_stat_activity_count", + MetricName: "pg_stat_activity_count", Type: &gauge, DefaultAgg: &maxTotal, Description: "Number of connections in pg_stat_activity, grouped by state, connected role, and backend type.", @@ -36,7 +36,7 @@ func TestServiceMetricsDetailsCmd(t *testing.T) { // A metric with no documented metadata yet: type/default_agg are nil, // description and labels are empty. undocumentedDetails := api.MetricDetails{ - Name: "some_new_metric", + MetricName: "some_new_metric", } const fullDetailsTable = `┌─────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────┐ @@ -205,7 +205,7 @@ func TestServiceMetricsDetailsCmd(t *testing.T) { "name": "ordinal" } ], - "name": "pg_stat_activity_count", + "metric_name": "pg_stat_activity_count", "type": "GAUGE" } `, @@ -231,7 +231,7 @@ labels: name: role - description: Per-pod ordinal within the service. name: ordinal -name: pg_stat_activity_count +metric_name: pg_stat_activity_count type: GAUGE `, }, diff --git a/internal/cmd/service_metrics_series.go b/internal/cmd/service_metrics_series.go index f496b4db..7d5b8e55 100644 --- a/internal/cmd/service_metrics_series.go +++ b/internal/cmd/service_metrics_series.go @@ -15,6 +15,17 @@ import ( "github.com/timescale/tiger-cli/internal/util" ) +// defaultMetricsSeriesWindow and defaultMetricsSeriesBucketSeconds are the +// --from/--to/--bucket-seconds values `service metrics series` uses when +// --from and --to are both omitted, so the command works without first +// having to construct RFC3339 timestamps. They apply at the CLI layer only — +// the MCP tool still requires from/to explicitly, since an agent should +// reason about the window it's asking for. +const ( + defaultMetricsSeriesWindow = 24 * time.Hour + defaultMetricsSeriesBucketSeconds = 3600 +) + // buildServiceMetricsSeriesCmd fetches time-series data for a named metric func buildServiceMetricsSeriesCmd(app *common.App) *cobra.Command { var metric string @@ -31,14 +42,20 @@ func buildServiceMetricsSeriesCmd(app *common.App) *cobra.Command { Short: "Get metric series data", Long: `Get time-series data for a specific metric. -Use 'tiger service metrics available-series' to discover valid metric names. +Use 'tiger service metrics available' to discover valid metric names. Each labeled series (e.g. one per replica) is returned independently with its full list of raw data points. The service can be given by ID or name as an argument, or will use the default -service from your configuration.`, - Example: ` # Fetch CPU usage for the last hour +service from your configuration. + +--from and --to default to the last 24 hours when both are omitted, bucketed +into 1-hour (3600s) intervals unless --bucket-seconds is also given.`, + Example: ` # Fetch CPU usage for the last 24 hours (the default window) + tiger service metrics series --metric timescale_cloud_system_cpu_usage_millicores + + # Fetch CPU usage for a specific hour tiger service metrics series --metric timescale_cloud_system_cpu_usage_millicores \ --from 2026-05-13T00:00:00Z --to 2026-05-13T01:00:00Z @@ -68,6 +85,16 @@ service from your configuration.`, ValidArgsFunction: serviceIDCompletion(app), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { + // Defaulting only kicks in when both are omitted — a single + // explicit flag still requires its counterpart, via the RFC3339 + // parse errors below. + usingDefaultWindow := from == "" && to == "" + if usingDefaultWindow { + now := time.Now().UTC() + from = now.Add(-defaultMetricsSeriesWindow).Format(time.RFC3339) + to = now.Format(time.RFC3339) + } + fromTime, err := time.Parse(time.RFC3339, from) if err != nil { return fmt.Errorf("--from must be RFC3339 (e.g., 2026-05-13T00:00:00Z): %w", err) @@ -77,6 +104,10 @@ service from your configuration.`, return fmt.Errorf("--to must be RFC3339 (e.g., 2026-05-13T01:00:00Z): %w", err) } + if usingDefaultWindow && bucketSeconds == 0 { + bucketSeconds = defaultMetricsSeriesBucketSeconds + } + labelFilters, err := parseMetricFilters(role, filters) if err != nil { return err @@ -98,9 +129,9 @@ service from your configuration.`, } body := api.MetricsSeriesRequest{ - Name: metric, - From: fromTime, - To: toTime, + MetricName: metric, + From: fromTime, + To: toTime, } if bucketSeconds > 0 { bs := bucketSeconds @@ -135,21 +166,20 @@ service from your configuration.`, } cmd.Flags().StringVar(&metric, "metric", "", "Metric series name") - cmd.Flags().StringVar(&from, "from", "", "Start of the time window (RFC3339)") - cmd.Flags().StringVar(&to, "to", "", "End of the time window (RFC3339)") + cmd.Flags().StringVar(&from, "from", "", "Start of the time window (RFC3339). Defaults to 24 hours ago when --to is also omitted") + cmd.Flags().StringVar(&to, "to", "", "End of the time window (RFC3339). Defaults to now when --from is also omitted") cmd.Flags().StringVar(&role, "role", "", "Filter to a specific instance role (PRIMARY or REPLICA)") cmd.Flags().StringSliceVar(&filters, "filter", nil, "Arbitrary label filter as name=value or name!=value (repeatable)") - cmd.Flags().IntVar(&bucketSeconds, "bucket-seconds", 0, "Aggregation bucket size in seconds (optional; server auto-selects based on the time window when omitted, minimum 60s)") + cmd.Flags().IntVar(&bucketSeconds, "bucket-seconds", 0, "Aggregation bucket size in seconds (minimum 60s). Defaults to 3600 (1h) when --from/--to are also omitted; otherwise the server auto-selects based on the time window") cmd.Flags().StringVar(&fn, "fn", "", "Aggregation function applied per bucket. One of: RATE, INCREASE, SUM, AVG, MIN, MAX, MIN_TOTAL, MAX_TOTAL, COUNT, P50, P90, P99, LAST. Rejected on the timescale_cloud_* resource/qps/connections/jobs metrics; omit to let the server pick the default") cmd.Flags().StringSliceVar(&groupBy, "group-by", nil, "Label key to break the result into one series per distinct value (repeatable). Rejected on the same metrics that reject --fn; omit to collapse into a single series") cmd.Flags().VarP(new(outputFlag), "output", "o", "Output format (json, yaml, table)") registerFlagCompletion(cmd, "output", outputCompletion()) registerFlagCompletion(cmd, "role", metricsSeriesRoleCompletion) registerFlagCompletion(cmd, "fn", metricsSeriesFnCompletion) + registerFlagCompletion(cmd, "metric", metricNameCompletion(app)) markFlagRequired(cmd, "metric") - markFlagRequired(cmd, "from") - markFlagRequired(cmd, "to") return cmd } diff --git a/internal/cmd/service_metrics_series_test.go b/internal/cmd/service_metrics_series_test.go index b9329a94..8a79d712 100644 --- a/internal/cmd/service_metrics_series_test.go +++ b/internal/cmd/service_metrics_series_test.go @@ -31,10 +31,10 @@ func TestServiceMetricsSeriesCmd(t *testing.T) { expectResolveRefID(m, "svc-12345") empty := []api.MetricSeries{} m.EXPECT().GetServiceMetricsSeriesWithResponse(validCtx, testProjectID, "svc-12345", api.MetricsSeriesRequest{ - Name: "some_metric", - From: fromTime, - To: toTime, - Filters: &filters, + MetricName: "some_metric", + From: fromTime, + To: toTime, + Filters: &filters, }).Return(&api.GetServiceMetricsSeriesResponse{ HTTPResponse: httpResponse(http.StatusOK), JSON200: &empty, @@ -72,7 +72,6 @@ func TestServiceMetricsSeriesCmd(t *testing.T) { { name: "ambiguous name refused", args: []string{"service", "metrics", "series", "my-api-db", "--metric", "some_metric", "--from", "2026-05-13T00:00:00Z", "--to", "2026-05-13T01:00:00Z"}, - opts: []runOption{experimental}, mock: func(m *mocks.MockClientWithResponsesInterface) { expectResolveRefStatus(m, "my-api-db", http.StatusBadRequest, &api.Error{Message: new("ambiguous service name matches multiple services")}) }, @@ -94,10 +93,61 @@ func TestServiceMetricsSeriesCmd(t *testing.T) { empty := []api.MetricSeries{} groupBy := []string{"role", "ordinal"} m.EXPECT().GetServiceMetricsSeriesWithResponse(validCtx, testProjectID, "svc-12345", api.MetricsSeriesRequest{ - Name: "some_metric", - From: fromTime, - To: toTime, - GroupBy: &groupBy, + MetricName: "some_metric", + From: fromTime, + To: toTime, + GroupBy: &groupBy, + }).Return(&api.GetServiceMetricsSeriesResponse{ + HTTPResponse: httpResponse(http.StatusOK), + JSON200: &empty, + }, nil) + }, + wantStdout: noDataMsg, + }, + { + // synctest's bubble clock always starts at 2000-01-01 UTC, so the + // default window and bucket size can be spelled out exactly. + name: "omitting --from and --to defaults to the last 24 hours at a 1h bucket", + args: []string{"service", "metrics", "series", "svc-12345", "--metric", "some_metric"}, + synctest: true, + mock: func(m *mocks.MockClientWithResponsesInterface) { + expectResolveRefID(m, "svc-12345") + empty := []api.MetricSeries{} + bucket := 3600 + m.EXPECT().GetServiceMetricsSeriesWithResponse(validCtx, testProjectID, "svc-12345", api.MetricsSeriesRequest{ + MetricName: "some_metric", + From: time.Date(1999, 12, 31, 0, 0, 0, 0, time.UTC), + To: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + BucketSeconds: &bucket, + }).Return(&api.GetServiceMetricsSeriesResponse{ + HTTPResponse: httpResponse(http.StatusOK), + JSON200: &empty, + }, nil) + }, + wantStdout: noDataMsg, + }, + { + // Defaulting only applies when both --from and --to are omitted — + // giving just one still requires its counterpart. + name: "omitting only --to still requires it", + args: []string{"service", "metrics", "series", "svc-12345", "--metric", "some_metric", "--from", "2026-05-13T00:00:00Z"}, + wantErr: `--to must be RFC3339 (e.g., 2026-05-13T01:00:00Z): parsing time "" as "2006-01-02T15:04:05Z07:00": cannot parse "" as "2006"`, + }, + { + // An explicit --bucket-seconds is respected even in the default + // window, rather than being overridden by the 3600s default. + name: "explicit --bucket-seconds overrides the default window's bucket size", + args: []string{"service", "metrics", "series", "svc-12345", "--metric", "some_metric", "--bucket-seconds", "60"}, + synctest: true, + mock: func(m *mocks.MockClientWithResponsesInterface) { + expectResolveRefID(m, "svc-12345") + empty := []api.MetricSeries{} + bucket := 60 + m.EXPECT().GetServiceMetricsSeriesWithResponse(validCtx, testProjectID, "svc-12345", api.MetricsSeriesRequest{ + MetricName: "some_metric", + From: time.Date(1999, 12, 31, 0, 0, 0, 0, time.UTC), + To: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + BucketSeconds: &bucket, }).Return(&api.GetServiceMetricsSeriesResponse{ HTTPResponse: httpResponse(http.StatusOK), JSON200: &empty, diff --git a/internal/mcp/service_metrics_details.go b/internal/mcp/service_metrics_details.go index 5fad5cb6..8cb171f2 100644 --- a/internal/mcp/service_metrics_details.go +++ b/internal/mcp/service_metrics_details.go @@ -46,7 +46,7 @@ func (ServiceMetricsDetailsOutput) Schema() *jsonschema.Schema { // but their content is only present once the metric is documented — every // undocumented metric still returns the key, just with a null/empty value. details := schema.Properties["details"] - details.Properties["name"].Description = "Metric series name." + details.Properties["metric_name"].Description = "Metric series name." details.Properties["type"].Description = "The shape of this metric's data, or null if undocumented." details.Properties["default_agg"].Description = "The aggregation function used by default when fn is omitted from a series query, or null if undocumented." details.Properties["description"].Description = "What this metric measures, or empty if undocumented." diff --git a/internal/mcp/service_metrics_details_test.go b/internal/mcp/service_metrics_details_test.go index b408abeb..28d70adc 100644 --- a/internal/mcp/service_metrics_details_test.go +++ b/internal/mcp/service_metrics_details_test.go @@ -25,7 +25,7 @@ func TestServiceMetricsDetailsTool(t *testing.T) { // A documented metric: its own labels carry no description yet, while the // region/role/ordinal labels every registry-backed metric gets do. documented := &api.MetricDetails{ - Name: "pg_stat_activity_count", + MetricName: "pg_stat_activity_count", Type: new(api.MetricTypeGAUGE), DefaultAgg: new(api.MetricsAggFnMAXTOTAL), Description: "Number of connections in pg_stat_activity, grouped by state, connected role, and backend type.", @@ -35,7 +35,7 @@ func TestServiceMetricsDetailsTool(t *testing.T) { }, } wantDocumented := map[string]any{"details": map[string]any{ - "name": "pg_stat_activity_count", + "metric_name": "pg_stat_activity_count", "type": "GAUGE", "default_agg": "MAX_TOTAL", "description": "Number of connections in pg_stat_activity, grouped by state, connected role, and backend type.", @@ -120,9 +120,9 @@ func TestServiceMetricsDetailsTool(t *testing.T) { name: "undocumented metric", tool: toolServiceMetricsDetails, args: map[string]any{"service_id": "e6ue9697jf", "metric_name": "some_new_metric"}, - mock: expectDetails("some_new_metric", &api.MetricDetails{Name: "some_new_metric"}), + mock: expectDetails("some_new_metric", &api.MetricDetails{MetricName: "some_new_metric"}), wantOutput: map[string]any{"details": map[string]any{ - "name": "some_new_metric", + "metric_name": "some_new_metric", "type": nil, "default_agg": nil, "description": "", diff --git a/internal/mcp/service_metrics_series.go b/internal/mcp/service_metrics_series.go index 5ffe99d8..592044ed 100644 --- a/internal/mcp/service_metrics_series.go +++ b/internal/mcp/service_metrics_series.go @@ -141,9 +141,9 @@ func (s *Server) handleServiceMetricsSeries(ctx context.Context, req *mcp.CallTo filters := buildMetricFilters(input.Role, input.Filters) body := api.MetricsSeriesRequest{ - Name: input.MetricName, - From: fromTime, - To: toTime, + MetricName: input.MetricName, + From: fromTime, + To: toTime, } if input.BucketSeconds > 0 { bs := input.BucketSeconds diff --git a/internal/mcp/service_metrics_series_test.go b/internal/mcp/service_metrics_series_test.go index 974aacc6..4b06927a 100644 --- a/internal/mcp/service_metrics_series_test.go +++ b/internal/mcp/service_metrics_series_test.go @@ -27,9 +27,9 @@ func TestServiceMetricsSeriesTool(t *testing.T) { from := time.Date(2026, 5, 13, 0, 0, 0, 0, time.UTC) to := time.Date(2026, 5, 13, 1, 0, 0, 0, time.UTC) baseBody := api.MetricsSeriesRequest{ - Name: "timescale_cloud_system_cpu_usage_millicores", - From: from, - To: to, + MetricName: "timescale_cloud_system_cpu_usage_millicores", + From: from, + To: to, } // body is baseBody with the optional fields the case expects filled in. body := func(apply func(*api.MetricsSeriesRequest)) api.MetricsSeriesRequest { diff --git a/openapi.yaml b/openapi.yaml index 2f5310a8..a160a197 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -2905,11 +2905,11 @@ components: body so callers can pass an unbounded filter set without query-string length pressure. required: - - name + - metric_name - from - to properties: - name: + metric_name: type: string description: Metric series name. Use getServiceMetricsAvailableSeries to discover valid values. example: "timescale_cloud_system_cpu_usage_millicores" @@ -3848,13 +3848,13 @@ components: default_agg may be null and description/labels may be empty, but every field below is always present in the response. required: - - name + - metric_name - description - type - default_agg - labels properties: - name: + metric_name: type: string description: Metric series name. example: "pg_locks_count" From cf0416bdb6b852936eecd361465f88a16fc59c49 Mon Sep 17 00:00:00 2001 From: Adrin Lopez Calvo Date: Wed, 30 Sep 2026 18:18:58 +0200 Subject: [PATCH 5/5] Resync openapi.yaml from savannah-gateway master Drops the last x-tigerdata-preview markers on the metrics endpoints and schemas, now that savannah-gateway#2038 merged. Diffed cleanly against gateway's current spec first: the only difference was these 12 leftover markers (this repo's earlier metric_name patch already matched). Regenerated internal/api/client.go via go generate -- doc-comment changes only, no field/signature changes, so no other code needed updating. This clears the last blocker on this PR. --- internal/api/client.go | 32 -------------------------------- openapi.yaml | 18 ------------------ 2 files changed, 50 deletions(-) diff --git a/internal/api/client.go b/internal/api/client.go index 47ffe3f1..ad094a9a 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -745,8 +745,6 @@ type ClientInterface interface { // GetServiceMetricsAvailableSeries List available metric series // - // **Preview — this endpoint is experimental and may change without notice.** - // // Returns the names of all metric series available for a service. // // Corresponds with GET /projects/{project_id}/services/{service_id}/metrics/available-series (the `GetServiceMetricsAvailableSeries` operationId). @@ -754,8 +752,6 @@ type ClientInterface interface { // GetServiceMetricDetails Get details about a metric // - // **Preview — this endpoint is experimental and may change without notice.** - // // Returns descriptive metadata for a named metric: what it measures, // its type, default aggregation, and every label it can be filtered or // grouped by — its own labels plus the region/role/ordinal labels most @@ -767,8 +763,6 @@ type ClientInterface interface { // GetServiceMetricsSeriesWithBody Get a metric series // - // **Preview — this endpoint is experimental and may change without notice.** - // // Returns time-series data points for a named metric within a time window. // Use getServiceMetricsAvailableSeries to discover valid metric names. // @@ -784,8 +778,6 @@ type ClientInterface interface { // GetServiceMetricsSeries Get a metric series // - // **Preview — this endpoint is experimental and may change without notice.** - // // Returns time-series data points for a named metric within a time window. // Use getServiceMetricsAvailableSeries to discover valid metric names. // @@ -2287,8 +2279,6 @@ func (c *Client) GetServiceLogs(ctx context.Context, projectID ProjectID, servic // GetServiceMetricsAvailableSeries List available metric series // -// **Preview — this endpoint is experimental and may change without notice.** -// // Returns the names of all metric series available for a service. // // Corresponds with GET /projects/{project_id}/services/{service_id}/metrics/available-series (the `GetServiceMetricsAvailableSeries` operationId). @@ -2306,8 +2296,6 @@ func (c *Client) GetServiceMetricsAvailableSeries(ctx context.Context, projectID // GetServiceMetricDetails Get details about a metric // -// **Preview — this endpoint is experimental and may change without notice.** -// // Returns descriptive metadata for a named metric: what it measures, // its type, default aggregation, and every label it can be filtered or // grouped by — its own labels plus the region/role/ordinal labels most @@ -2329,8 +2317,6 @@ func (c *Client) GetServiceMetricDetails(ctx context.Context, projectID ProjectI // GetServiceMetricsSeriesWithBody Get a metric series // -// **Preview — this endpoint is experimental and may change without notice.** -// // Returns time-series data points for a named metric within a time window. // Use getServiceMetricsAvailableSeries to discover valid metric names. // @@ -2356,8 +2342,6 @@ func (c *Client) GetServiceMetricsSeriesWithBody(ctx context.Context, projectID // GetServiceMetricsSeries Get a metric series // -// **Preview — this endpoint is experimental and may change without notice.** -// // Returns time-series data points for a named metric within a time window. // Use getServiceMetricsAvailableSeries to discover valid metric names. // @@ -6750,8 +6734,6 @@ type ClientWithResponsesInterface interface { // GetServiceMetricsAvailableSeriesWithResponse List available metric series // - // **Preview — this endpoint is experimental and may change without notice.** - // // Returns the names of all metric series available for a service. // // Returns a wrapper object for the known response body format(s). @@ -6761,8 +6743,6 @@ type ClientWithResponsesInterface interface { // GetServiceMetricDetailsWithResponse Get details about a metric // - // **Preview — this endpoint is experimental and may change without notice.** - // // Returns descriptive metadata for a named metric: what it measures, // its type, default aggregation, and every label it can be filtered or // grouped by — its own labels plus the region/role/ordinal labels most @@ -6776,8 +6756,6 @@ type ClientWithResponsesInterface interface { // GetServiceMetricsSeriesWithBodyWithResponse Get a metric series // - // **Preview — this endpoint is experimental and may change without notice.** - // // Returns time-series data points for a named metric within a time window. // Use getServiceMetricsAvailableSeries to discover valid metric names. // @@ -6793,8 +6771,6 @@ type ClientWithResponsesInterface interface { // GetServiceMetricsSeriesWithResponse Get a metric series // - // **Preview — this endpoint is experimental and may change without notice.** - // // Returns time-series data points for a named metric within a time window. // Use getServiceMetricsAvailableSeries to discover valid metric names. // @@ -11092,8 +11068,6 @@ func (c *ClientWithResponses) GetServiceLogsWithResponse(ctx context.Context, pr // GetServiceMetricsAvailableSeriesWithResponse List available metric series // -// **Preview — this endpoint is experimental and may change without notice.** -// // Returns the names of all metric series available for a service. // // Returns a wrapper object for the known response body format(s). @@ -11109,8 +11083,6 @@ func (c *ClientWithResponses) GetServiceMetricsAvailableSeriesWithResponse(ctx c // GetServiceMetricDetailsWithResponse Get details about a metric // -// **Preview — this endpoint is experimental and may change without notice.** -// // Returns descriptive metadata for a named metric: what it measures, // its type, default aggregation, and every label it can be filtered or // grouped by — its own labels plus the region/role/ordinal labels most @@ -11130,8 +11102,6 @@ func (c *ClientWithResponses) GetServiceMetricDetailsWithResponse(ctx context.Co // GetServiceMetricsSeriesWithBodyWithResponse Get a metric series // -// **Preview — this endpoint is experimental and may change without notice.** -// // Returns time-series data points for a named metric within a time window. // Use getServiceMetricsAvailableSeries to discover valid metric names. // @@ -11153,8 +11123,6 @@ func (c *ClientWithResponses) GetServiceMetricsSeriesWithBodyWithResponse(ctx co // GetServiceMetricsSeriesWithResponse Get a metric series // -// **Preview — this endpoint is experimental and may change without notice.** -// // Returns time-series data points for a named metric within a time window. // Use getServiceMetricsAvailableSeries to discover valid metric names. // diff --git a/openapi.yaml b/openapi.yaml index a160a197..a6d83931 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1201,13 +1201,10 @@ paths: get: operationId: getServiceMetricsAvailableSeries x-tigerdata-owner: prodexp - x-tigerdata-preview: true tags: - Services summary: List available metric series description: | - **Preview — this endpoint is experimental and may change without notice.** - Returns the names of all metric series available for a service. parameters: - $ref: '#/components/parameters/ProjectId' @@ -1228,13 +1225,10 @@ paths: post: operationId: getServiceMetricsSeries x-tigerdata-owner: prodexp - x-tigerdata-preview: true tags: - Services summary: Get a metric series description: | - **Preview — this endpoint is experimental and may change without notice.** - Returns time-series data points for a named metric within a time window. Use getServiceMetricsAvailableSeries to discover valid metric names. @@ -1267,13 +1261,10 @@ paths: get: operationId: getServiceMetricDetails x-tigerdata-owner: prodexp - x-tigerdata-preview: true tags: - Services summary: Get details about a metric description: | - **Preview — this endpoint is experimental and may change without notice.** - Returns descriptive metadata for a named metric: what it measures, its type, default aggregation, and every label it can be filtered or grouped by — its own labels plus the region/role/ordinal labels most @@ -2769,7 +2760,6 @@ components: - INCREMENTAL - UNKNOWN MetricsAggFn: - x-tigerdata-preview: true type: string description: | Aggregation function applied to raw/pre-aggregated samples when @@ -2807,7 +2797,6 @@ components: - P99 - LAST MetricType: - x-tigerdata-preview: true type: string description: | The shape of a metric's underlying data. @@ -2897,7 +2886,6 @@ components: example: "The requested service could not be found." MetricsSeriesRequest: - x-tigerdata-preview: true type: object additionalProperties: false description: | @@ -2993,7 +2981,6 @@ components: example: ["role"] MetricLabelFilter: - x-tigerdata-preview: true type: object description: A single key/value label match applied to a metric series query. required: @@ -3018,7 +3005,6 @@ components: default: EQUAL MetricMatchType: - x-tigerdata-preview: true type: string description: | - `EQUAL`: the series must have the label equal to `value`. @@ -3030,7 +3016,6 @@ components: - NOT_EQUAL MetricSeries: - x-tigerdata-preview: true type: object description: One labeled time series — the label set plus its per-bucket data points. required: @@ -3051,7 +3036,6 @@ components: $ref: '#/components/schemas/MetricDataPoint' MetricDataPoint: - x-tigerdata-preview: true type: object description: A single time/value pair within a MetricSeries. required: @@ -3840,7 +3824,6 @@ components: example: "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://eastus-1.in.applicationinsights.azure.com/" MetricDetails: - x-tigerdata-preview: true type: object description: | Descriptive metadata for a metric series — what it is, not its @@ -3886,7 +3869,6 @@ components: $ref: '#/components/schemas/MetricLabelDetails' MetricLabelDetails: - x-tigerdata-preview: true type: object description: One label specific to a metric. required: