# Grafana Dashboard Extractor (`datamule/grafana-dashboard-extractor`) Actor

Point at any Grafana instance and extract its dashboard + folder inventory, version and health via the open HTTP API. One row per dashboard/folder with title, uid, tags, folder; plus health and discovery modes. No per-host scraper.

- **URL**: https://apify.com/datamule/grafana-dashboard-extractor.md
- **Developed by:** [Datamule](https://apify.com/datamule) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.35 / 1,000 records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Grafana Dashboard Extractor

A GENERIC **"point at any Grafana instance"** Apify actor — the dashboard-inventory
sibling of the netdata / gatus / uptime-kuma observability runners, over a different,
far larger platform population. **Grafana** (grafana/grafana, ~66k★) is THE self-hosted
dashboarding / observability platform, on virtually every observability stack, and a
large fraction of instances leaves the HTTP API anonymous-readable — so ONE actor spans
every instance, no per-host scraper and no host enumeration.

### Input

| Field | Mode | Meaning |
|-------|------|---------|
| `sources` (**required**) | all | One or more Grafana base URLs — `https://play.grafana.org`, `https://grafana.wikimedia.org`, or any self-hosted instance. The `/api/...` path is appended for you. |
| `mode` | — | `search` (default), `health`, or `discovery`. |
| `query` | search | Case-insensitive substring matched against dashboard/folder titles. |
| `type` | search | `dash-db` (dashboards) or `dash-folder` (folders). Empty = both. |
| `tag` | search | One or more tags (AND-ed by Grafana). |
| `maxRecords` | all | Global cap across every source (one row = one billable event). |
| `bearer` | all | Optional API key / service-account token (`Authorization: Bearer`). Never logged. |
| `extraHeaders` | all | Optional extra request headers (JSON). |

### Modes

- **search** (default) — paginated `/api/search` → one flat row per dashboard/folder:
  `id`, `uid`, `title`, `kind` (dashboard/folder/other), `type`, `url`, `tags`,
  `isStarred`, `folderId`/`folderUid`/`folderTitle`, plus the instance `_version` /
  `_grafanaBuildCommit` (best-effort from a cheap health probe) and a lossless `_raw`.
- **health** — `/api/health` (+ best-effort `/api/frontend/settings`) → one row per
  instance: `database`, `version`, `commit`, `edition`, and the non-secret auth
  fingerprint (`disableLoginForm`, `samlEnabled`, `ldapEnabled`, `oauthProviders`
  \[names only], `featureToggleCount`).
- **discovery** — health + settings + a bounded search count → one summary row per
  instance: `version`, `edition`, `dashboardCount`, and `authDisabled` (did anonymous
  `/api/search` return 200?). A cheap way to fingerprint many instances at once.

### Behaviour

- Every field is read by **key presence** (`dict.get`) — Grafana's `/api/search` +
  `/api/health` shapes drift across the 8→13 major versions and instances populate
  different optional fields, so an entry omitting `folderTitle` / `tags` / `sortMeta`
  yields `null` / `[]` for that column, never a crash. Unknown fields survive in `_raw`.
- Many instances **gate `/api/search` (401/403) but leave `/api/health` open**. In
  search mode, such a source falls back to a **discovery row** (the instance
  fingerprint) instead of yielding nothing.
- A source behind an **anti-bot interstitial** (a non-JSON HTTP 200), unreachable
  (Cloudflare 52x), or returning a non-Grafana body is a clean **skip-with-warning** —
  the runner continues with the other sources. The run only **fails fast (exit 91)**
  when *every* source was skipped, so nothing broken ships and the cloud smoke gate
  stays honest.
- A valid instance with 0 matching dashboards → 0 records and a clean exit 0.
  Unpopular is fine; the actor never fabricates rows.

### Pricing

Pay-per-event: one `record` charge per emitted dataset row.

### Development

```bash
python3.12 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
python tests_logic.py          # pure-logic tests (no network)
apify validate-schema
apify run                       # local run (set storage/key_value_stores/default/INPUT.json)
```

# Actor input Schema

## `sources` (type: `array`):

One or more Grafana server base URLs. Grafana exposes an HTTP API and a large fraction of instances leaves it anonymous-readable, so ONE actor works against any install: https://play.grafana.org (the public demo), https://grafana.wikimedia.org (Wikimedia's public Grafana), or any self-hosted instance. Paste the server root as given (the /api/... path is appended for you). When several are listed, each is queried in turn and a source that is unreachable, behind an anti-bot wall, or fully auth-gated is skipped with a warning.

## `mode` (type: `string`):

What to extract. search (default): the dashboard + folder inventory from /api/search — optionally filtered by Query, Type or Tag. health: one row per instance from /api/health (database/version/commit) enriched with the auth fingerprint from /api/frontend/settings. discovery: one summary row per instance — version, edition, dashboard count, and whether anonymous read is enabled — a cheap way to fingerprint many instances at once.

## `query` (type: `string`):

SEARCH MODE filter. A case-insensitive substring matched against dashboard/folder titles (passed to /api/search?query=). Leave empty to list the whole inventory.

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

SEARCH MODE filter. Restrict to dashboards (dash-db) or folders (dash-folder). Leave empty to return both.

## `tag` (type: `array`):

SEARCH MODE filter. Restrict to dashboards carrying one or more tags (passed as repeatable /api/search?tag= params; multiple tags are AND-ed by Grafana). Leave empty for no tag filter.

## `maxRecords` (type: `integer`):

A GLOBAL cap on the number of rows to emit across ALL sources (each row is one dashboard/folder/instance and one billable event). Pagination stops as soon as the cap is reached, so a small value is a cheap, deterministic sample. Leave empty to fetch every matching record — a broad search on a large instance can return very many rows.

## `bearer` (type: `string`):

Optional Grafana API key or service-account token for reading a private instance (sent as the Authorization: Bearer header). NOT required for anonymous instances. Never logged.

## `extraHeaders` (type: `object`):

Optional extra HTTP headers as a JSON object, e.g. {"X-Grafana-Org-Id": "2"} for a multi-org or gateway-fronted instance. Not required for the public servers. Header values are never logged.

## Actor input object example

```json
{
  "sources": [
    "https://play.grafana.org"
  ],
  "mode": "search",
  "type": "",
  "maxRecords": 100
}
```

# Actor output Schema

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

No description

# 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 = {
    "sources": [
        "https://play.grafana.org"
    ],
    "maxRecords": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("datamule/grafana-dashboard-extractor").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 = {
    "sources": ["https://play.grafana.org"],
    "maxRecords": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("datamule/grafana-dashboard-extractor").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "sources": [
    "https://play.grafana.org"
  ],
  "maxRecords": 100
}' |
apify call datamule/grafana-dashboard-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=datamule/grafana-dashboard-extractor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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