# Metric Drop & Aggregation Recommender (`michiganpublictech/metric-usage-optimizer`) Actor

Find metrics and labels that no Grafana dashboard, alert, recording rule or query uses. Get ranked drop & aggregation recommendations with savings, plus OTel Collector, Prometheus relabel and recording-rule configs.

- **URL**: https://apify.com/michiganpublictech/metric-usage-optimizer.md
- **Developed by:** [Justin Hagerty](https://apify.com/michiganpublictech) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $500.00 / 1,000 metrics analyzeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Metric Drop & Aggregation Recommender

Find the metrics and labels that **nothing reads**: no Grafana dashboard, alert, recording rule or logged query.
Get ranked **drop** and **aggregation** recommendations with series and $/month savings, plus ready-to-apply
**OpenTelemetry Collector** processors, **Prometheus `metric_relabel_configs`**, remote-write
**`write_relabel_configs`**, **recording rules** and a phased **rollout plan**. Works like usage-based "adaptive metrics" tools, but runs on your own exports, with
no agent and no access to your systems.

### Why use it

Most metric bills pay for series nobody looks at: whole exporters nobody charts, `pod` and `instance` labels
that every panel sums away, histogram buckets when dashboards only show averages. Cardinality reports tell you
what is *big*. This Actor tells you what is **unused**, which is the part you can safely remove.

### Who it's for

- Platform/observability teams cutting Prometheus, Mimir, Grafana Cloud, Chronosphere or other metrics bills
- SREs cleaning up after OTel migrations (duplicate OTel/Prometheus names, old semconv names)
- CI pipelines that should fail when an alert references a metric that no longer exists

### How usage is determined

1. **Inventory**: what you store. Supported formats: metric list JSON, Prometheus `/api/v1/series`,
   `/api/v1/label/__name__/values` and `/api/v1/metadata` exports, `/api/v1/status/tsdb`, OTLP/JSON metrics,
   and Prometheus/OpenMetrics exposition text. Histograms and summaries are grouped into families
   (`_bucket`/`_sum`/`_count`); their bucket boundaries (`le`) are kept. Optional per-metric `job`/`jobs`
   (or a `job` label) tells the Actor which scrape job emits a metric.
2. **Usage sources**: Grafana dashboards (panels, rows, collapsed rows, legacy `rows`, templating variables,
   `label_values()` / `metrics()` / `query_result()` variable queries), Prometheus/Mimir rule files
   (`groups:` files, PrometheusRule CRDs, ruler exports) and query lists or Prometheus query logs.
3. Every expression is parsed by a **real PromQL parser**. Grafana macros (`$__rate_interval`, `$__interval`,
   `${var}`, `[[var]]`) are substituted first, outside string literals. Variables with known options are expanded.
4. For each selector the Actor computes **which labels the query depends on**, top-down: results shown on a
   dashboard need all labels, `sum by (service)` narrows that to `service`, `without (pod)` needs everything
   except `pod`, `on()`/`group_left()` add join keys, `label_replace` adds its source label,
   `histogram_quantile` adds `le`, and matchers always count.
5. **Recording rules are followed transitively.** A rule is used only if something used reads its output, and
   label needs flow through the chain. A metric that only feeds an unused recording rule counts as unused.
6. Unparsable queries fall back to a conservative token scan. Metrics found that way are marked
   **uncertain** and are never dropped or aggregated.

#### PromQL coverage

Selectors (incl. `{__name__=~"…"}` regex names, UTF-8 quoted names), all matchers, all aggregations
(`by`/`without`, before or after the argument, `topk`/`quantile`/`count_values` parameters), binary operators
with correct precedence, `bool`, `on`/`ignoring`, `group_left`/`group_right`, set operators, functions, range
vectors, subqueries (`[1h:5m]`), `offset` (incl. negative) and `@` (incl. `start()`/`end()`), comments.

### Recommendation types

| Type | Rule | What it means |
|---|---|---|
| `drop_metric` | MUO-DROP-001/003/004, MUO-QRY-003 | Nothing reads it (or only unused recording rules / stale queries) |
| `drop_family` | MUO-DROP-002 | ≥3 unused metrics share a prefix and nothing under the prefix is used |
| `aggregate_labels` | MUO-AGG-001 | Every query aggregates these labels away; pre-aggregate with sum/max/min/mean |
| `drop_label_values` | MUO-AGG-007 | Every query filters to e.g. `env="prod"`; drop the other values |
| `drop_histogram_buckets` | MUO-HIST-001 | Only `_sum`/`_count` are used |
| `drop_summary_quantiles` | MUO-HIST-004 | Only `_sum`/`_count` of a summary are used |
| `reduce_buckets`, `exponential_histogram` | MUO-HIST-002/003 | Only a few quantiles queried (with concrete suggested boundaries) / large classic histogram |
| `increase_interval` | MUO-RES-001/002 | Only long-range dashboards, or windows ≥ 16× the interval |
| `recording_rule` | MUO-QRY-001 | Same expensive aggregation computed in many places |

Each recommendation carries series before/after, $/month saved (if you gave prices), **confidence**
(high/medium/low), evidence (the queries and their label needs) and risk notes. They are ranked by savings.
The aggregation function is derived from how queries aggregate: counters and histograms use `sum` only;
gauges use the operator the queries use, or a name/unit heuristic flagged by MUO-AGG-003. Queries using
`count`, `quantile`, `stddev` or non-linear functions before the aggregation block pre-aggregation.

### Safety rules

- No usage sources means no drops (MUO-DATA-001).
- Metrics used by alerts are never dropped. Aggregations keep every label the alert expression **and its
  `{{ $labels.x }}` templates** use, and are capped at medium confidence.
- Protected metrics/labels (globs or `/regex/`; `up`, `scrape_*`, `ALERTS*`, `target_info` by default) are never touched.
- Uncertain usage (unparsable queries, dynamic metric names) is never dropped. Drops are high confidence only
  when dashboards **and** rules/query logs were supplied and everything parsed.
- Counters aggregated across `pod`/`instance` get a reset caveat (MUO-AGG-006).
- Prometheus `labeldrop` is **never generated** for aggregations: without aggregation it merges distinct
  series into duplicates. Aggregation goes through the Collector or recording rules.
- Integrity checks: alerts on missing metrics (critical), panels on missing metrics, filters on labels the
  metric doesn't have, and existing drop rules that remove used metrics or labels.

### Existing configuration

Supplied Collector and scrape configurations are read so the Actor does not recommend what is already done
and can warn when an existing rule removes something that is used:

- **Filter processor OTTL conditions** are parsed (`and`/`or`/`not`, `name`, `IsMatch`, `metric.type ==
  METRIC_DATA_TYPE_*`, `unit`, `resource.attributes["service.name"]`, `HasAttrOnDatapoint`) and evaluated
  per metric. A condition that depends on data-point attributes (e.g. `attributes["env"] == "dev"`) is
  reported as a *partial* drop in `data_quality` instead of being guessed.
- **`metric_relabel_configs` are scoped to their scrape job.** With `job`/`jobs` in the inventory (or a
  `job` label, as in `/api/v1/series` exports) a job's drop/keep rules only count for the metrics that job
  emits, and generated relabel rules are only appended to the jobs that emit the affected metrics. Without
  job information, drop rules count globally and keep lists only when there is a single job.

### How to use

#### In Apify Console

Paste your inventory (e.g. `curl prometheus/api/v1/status/tsdb?limit=1000`, or a `/metrics` dump), add
dashboard JSON exports, rule files and optionally query logs, enter your prices and run.

#### Via the API

```bash
curl -X POST "https://api.apify.com/v2/acts/michiganpublictech~metric-usage-optimizer/runs?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d @input.json
```

#### CI

Set `failOnSeverity: "critical"` to fail a pipeline when an alert references a metric that is missing from the inventory.

### Input

| Field | Description |
|---|---|
| `metricInventory` | Inventory text/JSON (any supported format, auto-detected) |
| `inventoryFormat` | `auto` (default) or force `exposition` |
| `dashboards` | Grafana dashboard JSON objects (or JSON strings) |
| `ruleFiles` | Rule files as YAML strings (or objects) |
| `queries` | Query strings or `{query, count, lastUsed, source}` objects, or Prometheus query-log lines |
| `collectorConfig` / `scrapeConfig` | Existing configuration: honoured, and generated rules are merged into it |
| `analysisName` | Label for the analysis |
| `analyses` | Batch: list of objects with the per-analysis fields above. Each one is billed separately |
| `costPerActiveSeriesPerMonth`, `costPerMillionSamples` | **Your** prices (no vendor prices are assumed) |
| `defaultScrapeIntervalSeconds` | Interval for sample costs when the inventory has none (60) |
| `minSavingsSeries`, `minSavingsUsdPerMonth` | Hide small recommendations |
| `protectedMetrics`, `protectedLabels`, `includeDefaultProtected` | Never touch these |
| `staleQueryDays` | Ignore queries not run for N days (relative to the newest `lastUsed` in the input) |
| `highCardinalityThreshold` | Cardinality that makes an unused label "high" (100) |
| `outputs` | `otel-collector`, `prometheus-relabel`, `prometheus-remote-write`, `recording-rules`, `vendor-neutral` |
| `generateConfigs` | Turn generated configuration off |
| `failOnSeverity`, `disabledRules`, `licenseKey` | Licensing & CI (disabling a rule also removes its recommendations) |

### Output

One dataset item per analysis (the shared `Report` shape). `data` contains:

- `summary`: metrics total/used/unused/uncertain, series and $/month before/after, savings %
- `recommendations`: the ranked list described above
- `usage`: per metric, where it is used (dashboards, panels, alerts, recording rules, query executions),
  labels used/unused and whether it is only used aggregated
- `generated`: `otel_collector_yaml`, `prometheus_relabel_yaml`, `prometheus_remote_write_yaml` (plus
  `remote_write_recorded`: raw → recorded series and the queries to rewrite), `recording_rules_yaml`, `vendor_neutral`
- `rollout_plan`: phases with verification queries to run before and after
- `data_quality`: notes on partial inventories, parse failures, dynamic variables and so on

Example recommendation:

```json
{"id": "R2", "type": "drop_label_values", "title": "Keep only env=prod for http_server_request_duration_seconds",
 "series_before": 32000, "series_after": 16000, "usd_per_month_saved": 819.2, "confidence": "medium",
 "evidence": ["Every query filters env to ['prod']; 2 values exist."],
 "risk_notes": ["New values of this label (e.g. a new environment) will be dropped too — review when you add one."]}
```

Generated Collector processors (merged into your config, placed after `memory_limiter` and before `batch`):

```yaml
processors:
  filter/muo_drop:
    error_mode: ignore
    metrics:
      metric:
      - name == "rpc_server_call_duration_seconds"
      - IsMatch(name, "^go_memstats_")
      datapoint:
      - metric.name == "http_server_request_duration_seconds" and not (attributes["env"] == "prod")
  metricstransform/muo_aggregate:
    transforms:
    - include: http_server_requests_total
      match_type: strict
      action: update
      operations:
      - action: aggregate_labels
        label_set: [env, route, service, status_code]
        aggregation_type: sum
```

Histograms whose buckets are dropped keep `_sum`/`_count`: the transform processor extracts them as
non-monotonic sums so Prometheus-compatible exporters keep the names your queries use (a monotonic sum would be
exported as `<name>_sum_total`). Generated processors are checked against `otelcol-contrib` 0.115 and 0.150.

Recording rules follow `level:metric:operations`, e.g.
`service:process_cpu_seconds:rate5m: sum by (service) (rate(process_cpu_seconds_total[5m]))`.

**Prometheus remote write** (`prometheus-remote-write`): `write_relabel_configs` that stop sending unused
series to your backend while the local Prometheus keeps everything. With `recording-rules` selected it also
sends **only the recorded series** of aggregated metrics (raw series stay local); `remote_write_recorded` lists
the dashboard queries to point at the recorded series first. Merged into your `remote_write` section when the
scrape configuration has one.
The key-value store also gets `OUTPUT`, `REPORT.md` (with an "Estimated savings" summary line, MUO-COST-003)
and `REPORT.sarif`. See [RULES.md](RULES.md) for all rules.

### Rollout plan

0. **Shadow & baseline**: deploy recording rules and record the "before" verification queries.
1. **Drop unused (high confidence)**.
2. **Drop the rest** after checking with metric owners.
3. **Aggregate labels**, one metric at a time, comparing dashboards and alert state.
4. **Manual review**: bucket layouts (with suggested boundaries that keep your SLO thresholds, plus queries
   that show where the quantiles fall today), intervals, native histograms.

### Pricing

Pay per event:

| Event | Price | When |
|---|---|---|
| `metrics-analyzed` | $0.50 | One inventory analysed with its usage sources (each item in `analyses` counts once) |
| `rules-generated` | $0.25 | Generated configuration contains at least one recommendation |

Inventories that cannot be parsed are not charged. Your spending limit is checked before each analysis.

### Limitations

- "Unused" only means unused by the sources you supplied. Add query logs to see ad-hoc and API use.
- Savings are estimates. Post-aggregation series = product of kept label cardinalities, capped at today's count.
- Collector metric names: the Actor uses the OTel name when it is known, otherwise the Prometheus name.
  Check them with the debug exporter.
- Logs, traces and non-Prometheus panels (Loki, SQL…) are skipped. LogQL/MetricsQL extensions are not parsed.

### FAQ

**Will dropping break my dashboards?** Only metrics that no supplied panel, alert, rule or query reads are
dropped, and each item comes with verification queries.

**Why not labeldrop?** It creates duplicate series. See the safety rules.

**How is this different from a cardinality analyzer?** Cardinality tools rank what is big. This Actor decides
what is *used*, and uses cardinality only to estimate savings.

### Changelog

See [CHANGELOG.md](CHANGELOG.md).

# Changelog

This Actor's version history is a separate document: https://apify.com/michiganpublictech/metric-usage-optimizer/changelog.md

# Actor input Schema

## `metricInventory` (type: `string`):

Paste any of: a metric list JSON ({"metrics": \[{name, type, labels: {label: cardinality | \[values]}, series, scrapeInterval, buckets, bucketBounds, job | jobs}]}), Prometheus /api/v1/series or /api/v1/label/**name**/values exports, /api/v1/status/tsdb JSON, OTLP/JSON metrics, or Prometheus/OpenMetrics exposition text (/metrics output). The format is detected automatically.

## `inventoryFormat` (type: `string`):

Leave on auto-detect. Choose 'exposition' to force Prometheus text parsing.

## `dashboards` (type: `array`):

Grafana dashboard JSON models (Share → Export, or the /api/dashboards/uid/<uid> response). Rows, collapsed rows, library-panel models, templating variables, $\_\_rate_interval and other macros are handled.

## `ruleFiles` (type: `array`):

Alerting and recording rule files as YAML strings (one per item): Prometheus 'groups:' files, PrometheusRule CRDs or Mimir/Cortex ruler exports. Metrics used by alerts are never dropped.

## `queries` (type: `array`):

PromQL queries from query logs, API clients or notebooks: strings or objects {"query": "...", "count": 12, "lastUsed": "2026-09-30T08:00:00Z"}. Prometheus query-log lines ({"params": {"query": ...}, "ts": ...}) are accepted too.

## `collectorConfig` (type: `string`):

Optional. Existing filter/transform/metricstransform/attributes processors are taken into account, and the generated processors are merged into this configuration and wired into its metrics pipelines.

## `scrapeConfig` (type: `string`):

Optional. prometheus.yml (or a list of metric_relabel_configs). Existing drops/labeldrops are taken into account per scrape job, generated relabel rules are appended to the jobs that emit the affected metrics (every job when the inventory has no job information), and remote-write rules are merged into its remote_write section.

## `analysisName` (type: `string`):

Label for this analysis in the output (e.g. the cluster or team).

## `analyses` (type: `array`):

Optional. Analyse several inventories in one run: a list of objects with the same per-analysis fields (metricInventory, dashboards, ruleFiles, queries, collectorConfig, scrapeConfig, analysisName). Pricing and options below apply to all. Each analysis is billed separately.

## `costPerActiveSeriesPerMonth` (type: `number`):

Your price per active time series per month, e.g. 0.008 for $8 per 1,000 series.

## `costPerMillionSamples` (type: `number`):

Your price per million ingested samples/data points. Set both prices if you pay both.

## `defaultScrapeIntervalSeconds` (type: `integer`):

Used for sample-based costs when the inventory has no interval for a metric.

## `minSavingsSeries` (type: `integer`):

Hide recommendations that save fewer active series than this.

## `minSavingsUsdPerMonth` (type: `number`):

Hide recommendations that save less than this per month (needs a price above).

## `protectedMetrics` (type: `array`):

Never drop or aggregate these. Globs (kube\_\*), or regex as /…/ or re:…

## `protectedLabels` (type: `array`):

Labels that are always kept (e.g. cluster, region). Globs or /regex/.

## `includeDefaultProtected` (type: `boolean`):

Also protect up, scrape\_\*, ALERTS, ALERTS_FOR_STATE and target_info.

## `staleQueryDays` (type: `integer`):

0 = off. Queries whose lastUsed is more than N days older than the newest query in the input do not count as usage (deterministic: no wall clock).

## `highCardinalityThreshold` (type: `integer`):

Unused labels with at least this many values are reported separately (MUO-AGG-002).

## `outputs` (type: `array`):

Which configuration to generate. Charged once per analysis as 'rules-generated', only when at least one recommendation is written into the output.

## `generateConfigs` (type: `boolean`):

Turn off to get the analysis and recommendations only (no 'rules-generated' charge).

## `failOnSeverity` (type: `string`):

Fail the run (non-zero exit, useful in CI) when any finding is at or above this severity — e.g. 'critical' catches alerts that reference missing metrics.

## `disabledRules` (type: `array`):

Rule IDs to skip, e.g. MUO-HIST-003. Disabling a rule also removes its recommendations. Requires a plan with the custom rules feature.

## `licenseKey` (type: `string`):

Optional enterprise or prepaid license key. Leave empty to pay per event on Apify.

## Actor input object example

```json
{
  "metricInventory": "{\n  \"metrics\": [\n    {\n      \"name\": \"http_server_requests_total\",\n      \"type\": \"counter\",\n      \"series\": 12000,\n      \"scrapeInterval\": \"30s\",\n      \"labels\": {\n        \"service\": 8,\n        \"route\": 40,\n        \"method\": 5,\n        \"status_code\": 6,\n        \"pod\": 60,\n        \"env\": [\n          \"prod\",\n          \"staging\"\n        ]\n      }\n    },\n    {\n      \"name\": \"http_server_request_duration_seconds\",\n      \"type\": \"histogram\",\n      \"buckets\": 14,\n      \"series\": 32000,\n      \"labels\": {\n        \"service\": 8,\n        \"route\": 40,\n        \"method\": 5,\n        \"pod\": 60,\n        \"env\": [\n          \"prod\",\n          \"staging\"\n        ]\n      }\n    },\n    {\n      \"name\": \"grpc_server_handled_total\",\n      \"type\": \"counter\",\n      \"series\": 6000,\n      \"labels\": {\n        \"grpc_service\": 10,\n        \"grpc_method\": 50,\n        \"grpc_code\": 17,\n        \"pod\": 60\n      }\n    },\n    {\n      \"name\": \"go_memstats_alloc_bytes\",\n      \"type\": \"gauge\",\n      \"series\": 60,\n      \"labels\": {\n        \"service\": 8,\n        \"pod\": 60\n      }\n    },\n    {\n      \"name\": \"go_memstats_heap_inuse_bytes\",\n      \"type\": \"gauge\",\n      \"series\": 60,\n      \"labels\": {\n        \"service\": 8,\n        \"pod\": 60\n      }\n    },\n    {\n      \"name\": \"go_memstats_sys_bytes\",\n      \"type\": \"gauge\",\n      \"series\": 60,\n      \"labels\": {\n        \"service\": 8,\n        \"pod\": 60\n      }\n    },\n    {\n      \"name\": \"kafka_consumer_lag\",\n      \"type\": \"gauge\",\n      \"series\": 3000,\n      \"labels\": {\n        \"topic\": 30,\n        \"partition\": 50,\n        \"consumer_group\": 4,\n        \"pod\": 20\n      }\n    }\n  ]\n}",
  "inventoryFormat": "auto",
  "dashboards": [
    {
      "title": "Service Overview",
      "time": {
        "from": "now-6h",
        "to": "now"
      },
      "templating": {
        "list": [
          {
            "name": "service",
            "type": "query",
            "query": "label_values(http_server_requests_total, service)"
          }
        ]
      },
      "panels": [
        {
          "id": 1,
          "title": "Request rate",
          "targets": [
            {
              "refId": "A",
              "expr": "sum by (service) (rate(http_server_requests_total{env=\"prod\", service=~\"$service\"}[$__rate_interval]))"
            }
          ]
        },
        {
          "id": 2,
          "title": "p99 latency",
          "targets": [
            {
              "refId": "A",
              "expr": "histogram_quantile(0.99, sum by (le, service) (rate(http_server_request_duration_seconds_bucket{env=\"prod\"}[$__rate_interval])))"
            }
          ]
        },
        {
          "id": 3,
          "type": "row",
          "title": "Kafka",
          "collapsed": true,
          "panels": [
            {
              "id": 4,
              "title": "Lag by topic",
              "targets": [
                {
                  "refId": "A",
                  "expr": "max by (topic) (kafka_consumer_lag)"
                }
              ]
            }
          ]
        }
      ]
    }
  ],
  "ruleFiles": [
    "groups:\n  - name: service-alerts\n    rules:\n      - alert: HighErrorRate\n        expr: |\n          sum by (service) (rate(http_server_requests_total{env=\"prod\", status_code=~\"5..\"}[5m]))\n            / sum by (service) (rate(http_server_requests_total{env=\"prod\"}[5m])) > 0.05\n        for: 10m\n        annotations:\n          summary: \"High error rate on {{ $labels.service }}\"\n"
  ],
  "defaultScrapeIntervalSeconds": 60,
  "minSavingsSeries": 0,
  "minSavingsUsdPerMonth": 0,
  "includeDefaultProtected": true,
  "staleQueryDays": 0,
  "highCardinalityThreshold": 100,
  "outputs": [
    "otel-collector",
    "prometheus-relabel",
    "prometheus-remote-write",
    "recording-rules",
    "vendor-neutral"
  ],
  "generateConfigs": true,
  "failOnSeverity": "none"
}
```

# Actor output Schema

## `results` (type: `string`):

One report per analysis: savings summary, ranked recommendations, usage map, generated configuration and rollout plan.

## `summary` (type: `string`):

Plan, per-analysis scores and billing summary (OUTPUT record).

## `reportMarkdown` (type: `string`):

Human-readable report of all findings (REPORT.md).

## `reportSarif` (type: `string`):

SARIF 2.1.0 report for CI tools (REPORT.sarif).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "metricInventory": `{
  "metrics": [
    {
      "name": "http_server_requests_total",
      "type": "counter",
      "series": 12000,
      "scrapeInterval": "30s",
      "labels": {
        "service": 8,
        "route": 40,
        "method": 5,
        "status_code": 6,
        "pod": 60,
        "env": [
          "prod",
          "staging"
        ]
      }
    },
    {
      "name": "http_server_request_duration_seconds",
      "type": "histogram",
      "buckets": 14,
      "series": 32000,
      "labels": {
        "service": 8,
        "route": 40,
        "method": 5,
        "pod": 60,
        "env": [
          "prod",
          "staging"
        ]
      }
    },
    {
      "name": "grpc_server_handled_total",
      "type": "counter",
      "series": 6000,
      "labels": {
        "grpc_service": 10,
        "grpc_method": 50,
        "grpc_code": 17,
        "pod": 60
      }
    },
    {
      "name": "go_memstats_alloc_bytes",
      "type": "gauge",
      "series": 60,
      "labels": {
        "service": 8,
        "pod": 60
      }
    },
    {
      "name": "go_memstats_heap_inuse_bytes",
      "type": "gauge",
      "series": 60,
      "labels": {
        "service": 8,
        "pod": 60
      }
    },
    {
      "name": "go_memstats_sys_bytes",
      "type": "gauge",
      "series": 60,
      "labels": {
        "service": 8,
        "pod": 60
      }
    },
    {
      "name": "kafka_consumer_lag",
      "type": "gauge",
      "series": 3000,
      "labels": {
        "topic": 30,
        "partition": 50,
        "consumer_group": 4,
        "pod": 20
      }
    }
  ]
}`,
    "dashboards": [
        {
            "title": "Service Overview",
            "time": {
                "from": "now-6h",
                "to": "now"
            },
            "templating": {
                "list": [
                    {
                        "name": "service",
                        "type": "query",
                        "query": "label_values(http_server_requests_total, service)"
                    }
                ]
            },
            "panels": [
                {
                    "id": 1,
                    "title": "Request rate",
                    "targets": [
                        {
                            "refId": "A",
                            "expr": "sum by (service) (rate(http_server_requests_total{env=\"prod\", service=~\"$service\"}[$__rate_interval]))"
                        }
                    ]
                },
                {
                    "id": 2,
                    "title": "p99 latency",
                    "targets": [
                        {
                            "refId": "A",
                            "expr": "histogram_quantile(0.99, sum by (le, service) (rate(http_server_request_duration_seconds_bucket{env=\"prod\"}[$__rate_interval])))"
                        }
                    ]
                },
                {
                    "id": 3,
                    "type": "row",
                    "title": "Kafka",
                    "collapsed": true,
                    "panels": [
                        {
                            "id": 4,
                            "title": "Lag by topic",
                            "targets": [
                                {
                                    "refId": "A",
                                    "expr": "max by (topic) (kafka_consumer_lag)"
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ],
    "ruleFiles": [
        "groups:\n  - name: service-alerts\n    rules:\n      - alert: HighErrorRate\n        expr: |\n          sum by (service) (rate(http_server_requests_total{env=\"prod\", status_code=~\"5..\"}[5m]))\n            / sum by (service) (rate(http_server_requests_total{env=\"prod\"}[5m])) > 0.05\n        for: 10m\n        annotations:\n          summary: \"High error rate on {{ $labels.service }}\"\n"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("michiganpublictech/metric-usage-optimizer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "metricInventory": """{
  \"metrics\": [
    {
      \"name\": \"http_server_requests_total\",
      \"type\": \"counter\",
      \"series\": 12000,
      \"scrapeInterval\": \"30s\",
      \"labels\": {
        \"service\": 8,
        \"route\": 40,
        \"method\": 5,
        \"status_code\": 6,
        \"pod\": 60,
        \"env\": [
          \"prod\",
          \"staging\"
        ]
      }
    },
    {
      \"name\": \"http_server_request_duration_seconds\",
      \"type\": \"histogram\",
      \"buckets\": 14,
      \"series\": 32000,
      \"labels\": {
        \"service\": 8,
        \"route\": 40,
        \"method\": 5,
        \"pod\": 60,
        \"env\": [
          \"prod\",
          \"staging\"
        ]
      }
    },
    {
      \"name\": \"grpc_server_handled_total\",
      \"type\": \"counter\",
      \"series\": 6000,
      \"labels\": {
        \"grpc_service\": 10,
        \"grpc_method\": 50,
        \"grpc_code\": 17,
        \"pod\": 60
      }
    },
    {
      \"name\": \"go_memstats_alloc_bytes\",
      \"type\": \"gauge\",
      \"series\": 60,
      \"labels\": {
        \"service\": 8,
        \"pod\": 60
      }
    },
    {
      \"name\": \"go_memstats_heap_inuse_bytes\",
      \"type\": \"gauge\",
      \"series\": 60,
      \"labels\": {
        \"service\": 8,
        \"pod\": 60
      }
    },
    {
      \"name\": \"go_memstats_sys_bytes\",
      \"type\": \"gauge\",
      \"series\": 60,
      \"labels\": {
        \"service\": 8,
        \"pod\": 60
      }
    },
    {
      \"name\": \"kafka_consumer_lag\",
      \"type\": \"gauge\",
      \"series\": 3000,
      \"labels\": {
        \"topic\": 30,
        \"partition\": 50,
        \"consumer_group\": 4,
        \"pod\": 20
      }
    }
  ]
}""",
    "dashboards": [{
            "title": "Service Overview",
            "time": {
                "from": "now-6h",
                "to": "now",
            },
            "templating": { "list": [{
                        "name": "service",
                        "type": "query",
                        "query": "label_values(http_server_requests_total, service)",
                    }] },
            "panels": [
                {
                    "id": 1,
                    "title": "Request rate",
                    "targets": [{
                            "refId": "A",
                            "expr": "sum by (service) (rate(http_server_requests_total{env=\"prod\", service=~\"$service\"}[$__rate_interval]))",
                        }],
                },
                {
                    "id": 2,
                    "title": "p99 latency",
                    "targets": [{
                            "refId": "A",
                            "expr": "histogram_quantile(0.99, sum by (le, service) (rate(http_server_request_duration_seconds_bucket{env=\"prod\"}[$__rate_interval])))",
                        }],
                },
                {
                    "id": 3,
                    "type": "row",
                    "title": "Kafka",
                    "collapsed": True,
                    "panels": [{
                            "id": 4,
                            "title": "Lag by topic",
                            "targets": [{
                                    "refId": "A",
                                    "expr": "max by (topic) (kafka_consumer_lag)",
                                }],
                        }],
                },
            ],
        }],
    "ruleFiles": ["""groups:
  - name: service-alerts
    rules:
      - alert: HighErrorRate
        expr: |
          sum by (service) (rate(http_server_requests_total{env=\"prod\", status_code=~\"5..\"}[5m]))
            / sum by (service) (rate(http_server_requests_total{env=\"prod\"}[5m])) > 0.05
        for: 10m
        annotations:
          summary: \"High error rate on {{ $labels.service }}\"
"""],
}

# Run the Actor and wait for it to finish
run = client.actor("michiganpublictech/metric-usage-optimizer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "metricInventory": "{\\n  \\"metrics\\": [\\n    {\\n      \\"name\\": \\"http_server_requests_total\\",\\n      \\"type\\": \\"counter\\",\\n      \\"series\\": 12000,\\n      \\"scrapeInterval\\": \\"30s\\",\\n      \\"labels\\": {\\n        \\"service\\": 8,\\n        \\"route\\": 40,\\n        \\"method\\": 5,\\n        \\"status_code\\": 6,\\n        \\"pod\\": 60,\\n        \\"env\\": [\\n          \\"prod\\",\\n          \\"staging\\"\\n        ]\\n      }\\n    },\\n    {\\n      \\"name\\": \\"http_server_request_duration_seconds\\",\\n      \\"type\\": \\"histogram\\",\\n      \\"buckets\\": 14,\\n      \\"series\\": 32000,\\n      \\"labels\\": {\\n        \\"service\\": 8,\\n        \\"route\\": 40,\\n        \\"method\\": 5,\\n        \\"pod\\": 60,\\n        \\"env\\": [\\n          \\"prod\\",\\n          \\"staging\\"\\n        ]\\n      }\\n    },\\n    {\\n      \\"name\\": \\"grpc_server_handled_total\\",\\n      \\"type\\": \\"counter\\",\\n      \\"series\\": 6000,\\n      \\"labels\\": {\\n        \\"grpc_service\\": 10,\\n        \\"grpc_method\\": 50,\\n        \\"grpc_code\\": 17,\\n        \\"pod\\": 60\\n      }\\n    },\\n    {\\n      \\"name\\": \\"go_memstats_alloc_bytes\\",\\n      \\"type\\": \\"gauge\\",\\n      \\"series\\": 60,\\n      \\"labels\\": {\\n        \\"service\\": 8,\\n        \\"pod\\": 60\\n      }\\n    },\\n    {\\n      \\"name\\": \\"go_memstats_heap_inuse_bytes\\",\\n      \\"type\\": \\"gauge\\",\\n      \\"series\\": 60,\\n      \\"labels\\": {\\n        \\"service\\": 8,\\n        \\"pod\\": 60\\n      }\\n    },\\n    {\\n      \\"name\\": \\"go_memstats_sys_bytes\\",\\n      \\"type\\": \\"gauge\\",\\n      \\"series\\": 60,\\n      \\"labels\\": {\\n        \\"service\\": 8,\\n        \\"pod\\": 60\\n      }\\n    },\\n    {\\n      \\"name\\": \\"kafka_consumer_lag\\",\\n      \\"type\\": \\"gauge\\",\\n      \\"series\\": 3000,\\n      \\"labels\\": {\\n        \\"topic\\": 30,\\n        \\"partition\\": 50,\\n        \\"consumer_group\\": 4,\\n        \\"pod\\": 20\\n      }\\n    }\\n  ]\\n}",
  "dashboards": [
    {
      "title": "Service Overview",
      "time": {
        "from": "now-6h",
        "to": "now"
      },
      "templating": {
        "list": [
          {
            "name": "service",
            "type": "query",
            "query": "label_values(http_server_requests_total, service)"
          }
        ]
      },
      "panels": [
        {
          "id": 1,
          "title": "Request rate",
          "targets": [
            {
              "refId": "A",
              "expr": "sum by (service) (rate(http_server_requests_total{env=\\"prod\\", service=~\\"$service\\"}[$__rate_interval]))"
            }
          ]
        },
        {
          "id": 2,
          "title": "p99 latency",
          "targets": [
            {
              "refId": "A",
              "expr": "histogram_quantile(0.99, sum by (le, service) (rate(http_server_request_duration_seconds_bucket{env=\\"prod\\"}[$__rate_interval])))"
            }
          ]
        },
        {
          "id": 3,
          "type": "row",
          "title": "Kafka",
          "collapsed": true,
          "panels": [
            {
              "id": 4,
              "title": "Lag by topic",
              "targets": [
                {
                  "refId": "A",
                  "expr": "max by (topic) (kafka_consumer_lag)"
                }
              ]
            }
          ]
        }
      ]
    }
  ],
  "ruleFiles": [
    "groups:\\n  - name: service-alerts\\n    rules:\\n      - alert: HighErrorRate\\n        expr: |\\n          sum by (service) (rate(http_server_requests_total{env=\\"prod\\", status_code=~\\"5..\\"}[5m]))\\n            / sum by (service) (rate(http_server_requests_total{env=\\"prod\\"}[5m])) > 0.05\\n        for: 10m\\n        annotations:\\n          summary: \\"High error rate on {{ $labels.service }}\\"\\n"
  ]
}' |
apify call michiganpublictech/metric-usage-optimizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,michiganpublictech/metric-usage-optimizer"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/qxXDbQgFwFX4cXasB/builds/LcYMuA88b38AzVloM/openapi.json
