Skip to content

Deployment & Operations

This exporter is distributed as a container image, plus an official Helm chart for Kubernetes. Use the Getting Started guide for initial setup and the provided docker-compose.yml as a baseline for production deployments.

Distribution

For v1, the exporter ships as a container image + Helm chart only — there is no PyPI package, and pip install meraki-dashboard-exporter is not a supported install path. If you need to run it outside a container, clone the repository and run it with uv as described in Getting Started.

Kubernetes (Helm)

A Helm chart (charts/meraki-dashboard-exporter) is published to the GHCR OCI registry on every release, alongside the container image:

helm install meraki-dashboard-exporter \
  oci://ghcr.io/rknightion/charts/meraki-dashboard-exporter \
  --version <exporter-version> \
  --set meraki.apiKey=your_api_key_here

Chart versions track exporter release versions (e.g. 0.31.0); an edge chart tracking main is also published on every push, versioned 0.0.0-main.*. The chart defaults to a hardened securityContext (non-root, read-only root filesystem) and fails render-time validation unless exactly one of meraki.apiKey / meraki.existingSecret is set — prefer existingSecret in production. See values.yaml for the full set of configurable settings.

The exporter is a single-writer singleton (no leader election), so replicaCount must stay 1 — the chart hard-fails the render otherwise, and its Deployment uses the Recreate strategy so a rollout never briefly runs two pods. Optional resources, all off by default: ingress.enabled (webhook TLS termination, see below), networkPolicy.enabled (locks egress to DNS + Meraki API 443 + the OTLP port), serviceMonitor.enabled (Prometheus Operator), and autoscaling.enabled (an HPA that manages the single pod; maxReplicas is capped at 1). Read the resources: sizing guidance in values.yaml before deploying at scale — the 512Mi default is sized for small orgs only. See the Scaling Guide for the API-budget sizing formula, per-scale resource tiers, and shard-by-org / HA recipes.

Shutdown behaviour and grace period

On SIGTERM, the exporter starts an orderly shutdown: in-flight HTTP requests are allowed to finish and running collector work is given a chance to wind down before the process exits. This is best-effort, not guaranteed, because collector fetches run the synchronous Meraki SDK on a background thread pool (asyncio.to_thread, sized by executor_workers) — a thread genuinely blocked inside an SDK HTTP call cannot be cancelled mid-flight from the asyncio event loop, so shutdown has to wait for that call to return (either normally or via its own timeout) rather than being able to kill it instantly.

Two settings bound how long a single blocked fetch can hold things up:

Setting Default Description
single_request_timeout (MERAKI_EXPORTER_API__TIMEOUT) 30s Bounds one HTTP request to the Meraki API.
per_fetch_deadline_seconds 120s (not yet exposed as a chart value) Bounds a whole logical fetch, including every page requested under total_pages="all" pagination — so a bulk fetch that keeps making slow page requests still fails fast at 120s instead of hanging for the full per-collector timeout.

Kubernetes only gives a pod terminationGracePeriodSeconds after SIGTERM before force-killing it with SIGKILL. If that grace period is shorter than the worst-case blocked fetch, Kubernetes kills the pod mid-shutdown, which is harmless (the exporter is stateless and safely restartable) but shows up as noisy SIGKILL/terminated: Error events instead of a clean exit. The Helm chart's terminationGracePeriodSeconds value defaults to 150s — comfortably above the per_fetch_deadline_seconds default (120s) with a ~30s margin (matching single_request_timeout) for the deadline to actually fire and the fetch to unwind before Kubernetes gives up. If you raise per_fetch_deadline_seconds above its default (via extraEnv, e.g. MERAKI_EXPORTER_API__PER_FETCH_DEADLINE_SECONDS), raise terminationGracePeriodSeconds to match — as a rule of thumb, keep it at per_fetch_deadline_seconds + ~30s.

Endpoints

The exporter exposes endpoints for metrics (/metrics), liveness (/health), readiness (/ready), an exporter self-health dashboard (/status), cardinality reports (/cardinality), and optional client (/clients) and webhook (POST /api/webhooks/meraki) features. See the HTTP Endpoints reference for the authoritative list and enablement notes.

Webhook receiver (HTTPS / TLS termination)

The exporter can receive Meraki alert webhooks (config.webhooksEnabled: true, endpoint POST /api/webhooks/meraki). Meraki only delivers webhooks over HTTPS — it rejects plain http:// receiver URLs — but the exporter itself serves plain HTTP. So you must terminate TLS in front of it and forward HTTP to the exporter. The receiver URL you configure in the Meraki Dashboard (Network-wide → Alerts → Webhooks) must therefore be https://…/api/webhooks/meraki, never http://. Always set the shared secret (MERAKI_EXPORTER_WEBHOOKS__SECRET) so payloads are validated — in the Helm chart, inject it via extraEnv (sourced from a Secret) rather than a plain value.

Pick whichever terminator fits your environment; all three terminate TLS and proxy plain HTTP to the exporter's service port (9099 by default).

Kubernetes Ingress (bundled in the Helm chart)

The chart ships an optional Ingress (ingress.enabled: true) that fronts the Service. Terminate TLS at the ingress (e.g. via cert-manager) and it forwards HTTP to the exporter:

# values.yaml
config:
  webhooksEnabled: "true"
extraEnv:
  - name: MERAKI_EXPORTER_WEBHOOKS__SECRET
    valueFrom:
      secretKeyRef:
        name: meraki-webhook-secret
        key: secret
ingress:
  enabled: true
  className: nginx
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  hosts:
    - host: meraki-exporter.example.com
      paths:
        - path: /api/webhooks/meraki
          pathType: Prefix
  tls:
    - secretName: meraki-exporter-tls
      hosts:
        - meraki-exporter.example.com

Meraki receiver URL: https://meraki-exporter.example.com/api/webhooks/meraki.

nginx (standalone reverse proxy)

server {
    listen 443 ssl;
    server_name meraki-exporter.example.com;

    ssl_certificate     /etc/nginx/tls/fullchain.pem;
    ssl_certificate_key /etc/nginx/tls/privkey.pem;

    # Only expose the webhook receiver publicly; scrape /metrics in-cluster/privately.
    location /api/webhooks/meraki {
        proxy_pass http://meraki-dashboard-exporter:9099;
        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Traefik (IngressRoute / dynamic config)

apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
  name: meraki-exporter-webhook
spec:
  entryPoints:
    - websecure          # the TLS entrypoint
  routes:
    - match: Host(`meraki-exporter.example.com`) && PathPrefix(`/api/webhooks/meraki`)
      kind: Rule
      services:
        - name: meraki-dashboard-exporter
          port: 9099      # plain HTTP to the exporter
  tls:
    certResolver: letsencrypt

In every case Meraki talks HTTPS to the terminator, and the terminator talks HTTP to the exporter.

Monitoring

Prometheus and Grafana integration examples live in the Integration & Dashboards guide.

Updating

Pull the latest image and restart the container:

docker compose pull
docker compose up -d

Troubleshooting

  • Check container logs with docker compose logs meraki_dashboard_exporter.
  • Verify the API key and network connectivity.
  • Metrics meraki_exporter_collector_errors_total help identify failing collectors.
  • Open /status for an at-a-glance view of tier health, last collection durations, and the effective NetworkFilter state (parsed rules plus per-org total/resolved network counts, read from the same meraki_network_filter_* gauges — see Network Filter below).
  • See Troubleshooting for a symptom-driven decision tree covering auth errors, empty /metrics, readiness gating, per-org backoff, rate-limit storms, cardinality shedding, and webhook misconfiguration.

Alerting on partial collection failures

A collector cycle can succeed overall/ready stays 200, and the coordinator does not raise — while one or more sub-phases inside it fail and are silently tolerated. This is deliberate resilience (a single broken endpoint, e.g. one MX VPN fetch or one org's config sub-collection, must never abort the rest of that cycle's collection), but it means the honest top-level failure signals introduced for #509 (a raised _collect_impl(), /ready flipping, meraki_exporter_org_collection_status) can stay green for cycles that are nonetheless degraded.

Every one of those tolerated sub-phase failures increments the same counter used for full collector failures — meraki_exporter_collector_errors_total (labels: collector, tier, error_type) — so it is the only signal that surfaces this class of degradation. Alert on its rate rather than waiting for a full-cycle failure:

# Any tolerated sub-phase failure in the last 15 minutes, broken out by
# collector/tier/error_type so you can see which sub-system is degraded.
sum by (collector, tier, error_type) (
  rate(meraki_exporter_collector_errors_total[15m])
) > 0
# Sustained partial failure: a collector/tier has logged errors continuously
# for the last hour. Good candidate for a paging alert (vs. the query above,
# which is better suited to a dashboard panel or a low-severity notification).
min_over_time(
  (sum by (collector, tier) (rate(meraki_exporter_collector_errors_total[15m])) > 0)[1h:]
) == 1

Because this counter increments for tolerated and fatal failures alike, corroborate a firing alert against the #509 health signals before treating it as critical:

  • up{job="meraki-dashboard-exporter"} / /health - process liveness (unaffected either way).
  • /ready and its backing readiness gate - only trips for a total cycle failure in a collector owning an enabled priority-<=3 endpoint group (config-only, priority-4 collectors like ConfigCollector are excluded from readiness by design).
  • meraki_exporter_org_collection_status{org_id="..."} - per-organization gauge, 0 only when every sub-collection failed for that org this cycle (see OrganizationCollector/OrgHealthTracker in core/org_health.py).

If meraki_exporter_collector_errors_total is climbing but /ready is 200 and meraki_exporter_org_collection_status is 1, the cycle is completing with partial data loss for the affected sub-phase (e.g. one MR/MS/MX sub-collection, one config sub-collection, or one org's sensor gateway connections) - worth investigating, but not an outage. If /ready also flips or the org status gauge drops to 0, treat it as the higher-severity, already-covered #509 failure case instead.

Network Filter

For large organisations, restrict scraping to a subset of networks via the MERAKI_EXPORTER_NETWORK_FILTER__* settings (include/exclude by name glob, ID, or tag). The filter is inactive by default; if a filter is configured but resolves to zero networks across all configured orgs at startup, the exporter exits with an error so typos fail loudly. See .env.example and the Configuration guide for details.

Log Aggregation

The exporter outputs structured logs via structlog, rendered by MERAKI_EXPORTER_LOGGING__LOG_FORMATlogfmt (the default, ideal for Loki's | logfmt parser stage) or json (for JSON-native pipelines such as Datadog, ELK, or CloudWatch). Both formats carry the same fields (level, event, timestamp, plus any structured kwargs the log call attaches); only the wire encoding differs. See Configuration for the setting.

Grafana Alloy Configuration

To ship logs to Loki using Grafana Alloy, add this to your Alloy configuration:

local.file_match "meraki_exporter" {
  path_targets = [{"__path__" = "/var/log/meraki-exporter/*.log"}]
}

loki.source.file "meraki_exporter" {
  targets    = local.file_match.meraki_exporter.targets
  forward_to = [loki.write.default.receiver]
}

loki.write "default" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

For Docker or Kubernetes deployments, use the container log discovery instead:

discovery.docker "meraki" {
  host = "unix:///var/run/docker.sock"
  filter {
    name   = "name"
    values = ["meraki-dashboard-exporter"]
  }
}

loki.source.docker "meraki" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.docker.meraki.targets
  forward_to = [loki.write.default.receiver]
}

Example LogQL Queries

These example queries assume the default MERAKI_EXPORTER_LOGGING__LOG_FORMAT=logfmt, where the | logfmt parser stage extracts each structured field (e.g. collector, duration) as a label you can filter or group on. The line-filter (|=/|~) portion of every query below also works unchanged against log_format=json output — the raw text still contains the same substrings — but the | logfmt parser stage does not understand JSON, so swap it for | json if you run with log_format=json, as shown in the first example.

Collector failures:

{container="meraki-dashboard-exporter"} |= "Failed to collect" | logfmt
With log_format=json instead:
{container="meraki-dashboard-exporter"} |= "Failed to collect" | json

Rate limit events:

{container="meraki-dashboard-exporter"} |~ "rate limit|429" | logfmt

Slow collections (utilization warnings, logged when a collector uses >80% of its cadence):

{container="meraki-dashboard-exporter"} |= "Collector utilization high" | logfmt | duration > 60

Error summary by collector:

sum by (collector) (count_over_time({container="meraki-dashboard-exporter"} |= "error" | logfmt [1h]))

For configuration options see the Configuration guide. A list of exported metrics is available in the Metrics Reference.