Sign inSign up

mcarvin8/sfmon

By mcarvin8

Updated 23 days ago

Monitor a Salesforce org and expose Prometheus metrics from a long-running Python service.

Image
0

3.7K

mcarvin8/sfmon repository overview

Salesforce Monitoring (SFMon)

Docker Image Version Docker Pulls Docker Image Size PyPI Version PyPI - Python Version PyPI Downloads GitHub Marketplace Coverage

SFMon is a long-running Python application that connects to your Salesforce org(s) on a schedule and exposes a standard /metrics endpoint compatible with Prometheus — so you can monitor your orgs with the same tech stack as the rest of your infrastructure.

Metrics are instrumented with OpenTelemetry and structured logs are emitted as JSON; both can optionally push over OTLP instead of only being scraped/tailed — see Metrics and logs.


How it works

One process, no database, no UI:

  1. On startup SFMon authenticates to your org (OAuth2 refresh token flow) and starts an internal APScheduler cron loop.
  2. Each collector job runs on its own schedule (every 5 minutes, hourly, or once daily off-peak — see Presets), queries the org via SOQL/REST/Tooling API, and sets Prometheus gauges.
  3. Those gauges are served on :9001/metrics, so Prometheus (or a compatible tool) scrapes them.

There's no persistence and no historical storage inside SFMon itself — your Prometheus-compatible backend owns the time series. Restarting it just re-authenticates and resumes the schedule.


Who is this for

SFMon is aimed at SRE and DevOps teams who already operate a Prometheus-compatible observability stack (Prometheus, Victoria Metrics, Grafana Cloud, or an OTel Collector pipeline) and are also responsible for one or more Salesforce orgs — teams who define alerts in PromQL, route pages through Alertmanager, and want Salesforce signals to behave like any other scrape target. Don't run a scrape-based stack? Push metrics and logs over OTLP instead — see Metrics and logs.

It is not a Salesforce admin tool. It has no UI of its own; all visibility comes from your existing observability stack.


What you get

CategoryWhat is measured
Governor limitsAll org limits (API requests, bulk queries, data storage, etc.) — usage %, used, and max, every 5 minutes
Apex healthFlex queue depth, long-running requests, concurrency errors, uncaught exceptions, async job status and summaries
Bulk APIDaily summaries and hourly in-flight activity across Bulk API 1.0 and 2.0
LicensesUser licenses, permission set licenses, and usage-based entitlements — consumed vs. total, % used
Instance & trustYour org's pod, active incidents from trust.salesforce.com, and scheduled maintenance windows
Security & complianceForbidden profile assignments, login volumes, geolocation anomalies, suspicious audit trail activity, report exports, large SOQL queries, org-wide sharing settings
Tech debtDormant users (Salesforce + portal), deprecated Apex API versions, unassigned/minimal permission sets, workflow rules, empty queues/groups, PMD static analysis violations
DeploymentsIn-flight metadata deployment status

Everything runs on a default schedule with no config file required. See docs/CONFIGURATION.md to scope down to a preset or tune individual jobs.


Quick start

Prerequisites

On your local machine, you need to have the Salesforce CLI (sf) installed and logged in to the target org(s) via applicable monitoring users. The monitoring user in each Salesforce org must have API access enabled and have the appropriate permissions to monitor the various metrics. Preferably, the monitoring user should have the "Password Does Not Expire" and "Api Only User" system permissions granted either via a profile or permission set.

Get your auth URL for each org (force://PlatformCLI::...): sf org auth show-sfdx-auth-url --target-org my-org --json > authFile.json

SFMon itself doesn't use the Salesforce CLI at runtime — it only needs the auth URL, provided via environment variable or AWS Secrets Manager.

Run via Docker
  1. Run container:
docker run -d \
  --name sfmon \
  -p 9001:9001 \
  -e SALESFORCE_AUTH_URL="force://PlatformCLI::..." \
  -e ORG_NAME="production" \
  mcarvin8/sfmon:latest
  1. Verify: curl http://localhost:9001/metrics
  2. Scrape — add to prometheus.yml:
scrape_configs:
  - job_name: sfmon
    static_configs:
      - targets: ["<host>:9001"]

No config file is required: all collectors run on default schedules out of the box.

Run via Python pip

Prefer running on bare metal/VM instead of Docker, or want to import individual collectors in your own scripts? SFMon is also published to PyPI:

  1. Install and run:
pip install sfmon
SALESFORCE_AUTH_URL="force://PlatformCLI::..." ORG_NAME="production" sfmon
  1. Verify: curl http://localhost:9001/metrics
  2. Scrape — add to prometheus.yml:
scrape_configs:
  - job_name: sfmon
    static_configs:
      - targets: ["<host>:9001"]

The sfmon console script runs the same always-on daemon as the Docker image. CONFIG_FILE_PATH defaults to /app/sfmon/config.json (the Docker path) — set it explicitly for non-Docker installs. sfmon is also an importable library, e.g. from sfmon.core.limits import salesforce_limits_descriptions.

Optional Tuning
  • Environment variables (timeouts, org label, compliance lists, thresholds, log level) → docs/ENVIRONMENT.md
  • Config file (schedules, presets, disable jobs, exclude_users) → docs/CONFIGURATION.md · template config.example.json
  • Secrets backend — fetch SALESFORCE_AUTH_URL/SALESFORCE_AUTH_URL_<NAME> from AWS Secrets Manager instead of the environment (SECRETS_BACKEND=aws) → docs/ENVIRONMENT.md

Multiple orgs

Two ways to monitor more than one org. Both label every metric with org so a single Prometheus-compatible backend can scrape and filter/aggregate across orgs in PromQL.

Fleet mode (one container, several orgs)

Add an orgs array to config.json and the same container polls every org on its own schedule:

{
  "orgs": ["prod", "sandbox-uat"],
  "schedules": { "monitor_salesforce_limits": "*/5" },
  "org_overrides": {
    "sandbox-uat": { "schedules": { "monitor_salesforce_limits": "*/15" } }
  }
}

Each name resolves to a SALESFORCE_AUTH_URL_<NAME> env var (uppercased, non-alphanumerics → _):

docker run -d \
  --name sfmon \
  -p 9001:9001 \
  -v /host/path/config.json:/app/sfmon/config.json \
  -e SALESFORCE_AUTH_URL_PROD="force://PlatformCLI::[email protected]" \
  -e SALESFORCE_AUTH_URL_SANDBOX_UAT="force://PlatformCLI::[email protected]" \
  mcarvin8/sfmon:latest

org_overrides is optional and lets one org diverge from the fleet-wide schedules. An org whose credentials fail to authenticate is logged and skipped at startup — it doesn't block the rest of the fleet. ORG_NAME is ignored once orgs is set. See docs/CONFIGURATION.md · template config.example.fleet.json.

One container per org (alternate)

Run a separate container per org, each with a distinct ORG_NAME and SALESFORCE_AUTH_URL:

# prometheus.yml
scrape_configs:
  - job_name: sfmon
    static_configs:
      - targets: ["sfmon-prod:9001"]
        labels: { org: "production" }
      - targets: ["sfmon-uat:9001"]
        labels: { org: "uat" }

Prefer this when you want full process/resource isolation per org (independent restarts, separate resource limits) rather than a shared scheduler.


Metrics and logs

Two output shapes, matched to two different questions:

  • Metrics (:9001/metrics) answer "is something wrong right now" — governor limit %, license usage, active incidents, aggregate counts of suspicious activity by action/section/user group. Low-cardinality labels only.
  • Logs (stdout, JSON lines) answer "who did it and what exactly happened" — the per-record detail behind those aggregates: user, timestamp, display text, deployment IDs, login coordinates. Anything that would otherwise blow up metric cardinality goes here instead, under a structured event field. Pipe stdout to any log backend that reads JSON (Loki, Vector, Fluent Bit, CloudWatch Logs, Datadog Logs).

Both default to pull/tail with no extra setup: metrics are scraped from /metrics, logs are read from container stdout. If you'd rather push — no Prometheus in your stack, or the container sits somewhere scraping is awkward — set OTEL_EXPORTER_OTLP_ENDPOINT and both metrics and logs also push to an OTLP collector or backend (Datadog, Honeycomb, Grafana Alloy, an OTel Collector, ...) in addition to /metrics and stdout. Unset, behavior is unchanged. See docs/ENVIRONMENT.md.

No traces — there's no request-tracing use case here, so that OTel signal isn't used.


Alerting in PromQL

Because metrics live in Prometheus, alerts are just PromQL rules — same toolchain as the rest of your stack:

# Daily API limit over 80 % consumed
sfmon_api_usage_percentage{limit_name="DailyApiRequests"} > 80

# Active incident on this org's pod
sfmon_incident_gauge{environment="production"} == 1

# User license saturation
sfmon_percent_user_licenses_used{license_name="Salesforce"} > 90

Route these through Alertmanager with the same receivers (PagerDuty, Slack, etc.) you use for every other service.

Built-in Slack alerting (optional, no Alertmanager required)

For teams that don't run a full PromQL/Alertmanager stack, SFMon can also post directly to a Slack incoming webhook. Set SLACK_WEBHOOK_URL and it's on; leave it unset and there's no behavior change at all (no cache reads/writes, no HTTP calls).

Alerts are edge-triggered — a Slack message fires once when a breach opens and once when it resolves, not on every scheduler tick while it stays active — using an on-disk cache (SLACK_ALERT_CACHE_DIR) keyed per org so state survives both the long-lived daemon and --once CI-cron restarts.

Currently wired into governor limits (LIMIT_ALERT_THRESHOLD_PERCENT, default 80), Salesforce Trust API incidents (an active incident on your org's pod posts on open and again on resolve), license seat usage (LICENSE_ALERT_THRESHOLD_PERCENT, default 90), the org-wide Apex character limit (APEX_CHARACTER_ALERT_THRESHOLD_PERCENT, default 80), and Apex Flex Queue depth (FLEX_QUEUE_ALERT_THRESHOLD_PERCENT, default 80, critical once the queue actually hits its 100-job cap) — all critical at 95%+ except where noted. See docs/ENVIRONMENT.md.

The underlying sync_alerts() API is generic and other collectors can adopt it over time. Please create a GitHub issue to request any new alerts.


One-shot mode — run from a CI cron job or GitHub Action instead of a daemon

Don't want an always-on container? sfmon --once runs every enabled job a single time, prints the resulting Prometheus exposition text to stdout, and exits — the same model sfdx-hardis's org monitoring uses (a scheduled CI/CD pipeline instead of a long-running process). Works with either distribution:

# Docker
docker run --rm -e SALESFORCE_AUTH_URL="force://PlatformCLI::..." mcarvin8/sfmon:latest --once

# pip install
SALESFORCE_AUTH_URL="force://PlatformCLI::..." sfmon --once
  • Exit code is 0 if every job that ran succeeded, 1 if any job (or the initial org connection) failed — so a scheduled pipeline goes red on a real problem, the same way a failed CI step would.
  • Add --job JOB_ID to run exactly one job (its id from the tables in docs/CONFIGURATION.md) instead of everything currently enabled — --job forces that job to run regardless of its opt-in/disabled state in config.json, useful for ad-hoc checks. --job requires --once.
  • Without --job, --once respects the same config as the daemon (presets, opt-in schedules, disabled entries) — it runs whatever would run at container startup, just without then staying up to serve /metrics or wait for the next cron tick.
  • Pipe the output wherever it's useful: archive it as a pipeline artifact, curl --data-binary it to a Pushgateway, or grep it for a threshold check.
GitHub Action — run a single job as a CI step

Same one-shot model, packaged as a reusable action so a workflow step can gate on the result without shelling out to docker run itself:

- name: Check Salesforce API limits
  id: limits
  uses: mcarvin8/sfmon@v3
  with:
    job-id: monitor_salesforce_limits
    auth-url: ${{ secrets.SF_AUTH_URL }}

- name: Use the parsed metrics
  run: echo '${{ steps.limits.outputs.metrics-json }}' | jq .
InputRequiredMaps to
job-idyessfmon --once --job <id>
auth-urlyesSALESFORCE_AUTH_URL
org-namenoORG_NAME (org label on the metrics)
OutputDescription
exit-code0/1/2 from sfmon --once — the step also fails naturally on 1/2
metrics-rawFull Prometheus exposition text for the run
metrics-jsonSame metrics parsed into a flat {"metric_name{labels}": value} object

Any other env var sfmon reads (thresholds, compliance lists, SECRETS_BACKEND, etc. — see docs/ENVIRONMENT.md) can be set directly on the step's own env: block; it's passed through to the container. Fleet mode (SALESFORCE_AUTH_URL_<ORG>) isn't wired into the action's inputs yet — single-org only.


Presets — scope down without a full config

If you only want a focused slice of monitoring, set a preset in config.json instead of listing every job:

{ "preset": "ops" }
PresetFocus
opsApex health, Bulk API, deployments, EPT/APT
auditLogin events, geolocation, suspicious activity, report exports, sharing settings
tech-debtDormant users, deprecated APIs, permission sets, workflow rules, queues, security health

Governor limits, instance/trust health, and license metrics are always on regardless of preset — they are the baseline signals you always want without having to ask.

See docs/CONFIGURATION.md for the full scheduling reference.


How it compares

SFMonSalesforce proactive monitoring (paid)sfdx-hardis org monitoring
ModelAlways-on container/process (Prometheus /metrics), or a scheduled CI job via --onceSalesforce TAM/CSM engagement + event log filesScheduled CI jobs (GitHub Actions / GitLab CI)
OutputTime-series metrics scraped by PrometheusSalesforce-native reports and guided reviewsGit diffs, Slack/Teams notifications, pipeline artifacts
AlertingPromQL + Alertmanager (same as rest of infra), or built-in Slack webhook alerting with no Alertmanager neededSalesforce notifications and Success Plan reviewsSlack/Teams webhooks from CI
Data stays in your stackYesNo (Salesforce-hosted)Partially (metadata to Git; notifications to Slack/Teams)
Extra costFree (open source) — compute to run the container, or a CI cron job via --onceSalesforce edition / add-on feeFree (open source) — compute for the CI cron job
Best forSRE/DevOps teams already on Prometheus who want Salesforce as just another scrape targetTeams buying Salesforce-managed oversight and guidanceTeams wanting metadata drift detection and CI-integrated checks

SFMon and sfdx-hardis are complementary, not competitors: Hardis handles metadata backup and change detection via CI; SFMon provides continuous time-series for the same signals your infrastructure monitoring already tracks.


PMD + minimal permission sets (optional, file-based)

The published mcarvin8/sfmon image does not include an Apex ruleset, pmd-report.xml, or minimal-perm-sets.json (they stay in your repo/CI only; see .dockerignore). Collectors monitor_pmd_code_smells and monitor_minimal_perm_sets need those files inside the container at fixed paths:

FileIn-container path
PMD ruleset (XML)Any path you choose; set PMD_RULESET_PATH to it
PMD report/app/sfmon/tech_debt/pmd-report.xml
Minimal perm set report/app/sfmon/tech_debt/minimal-perm-sets.json

Typical flow:

  1. In your fork/clone (with org access), refresh reports in CI so pmd-report.xml and minimal-perm-sets.json are produced under src/sfmon/tech_debt/ and pushed to your default branch. Maintain manifest/package.xml and apexruleset.xml in that folder.

    • GitHub Actions: Workflow .github/workflows/update-local-reports.yml — repository secret SALESFORCE_AUTH_URL (SFDX URL). Optional workflow_dispatch input manifest_path (default manifest/package.xml).
    • GitLab CI: Example job .gitlab/workflows/update-local-reports.yml — include it from .gitlab-ci.yml (define a query stage). Create a project access token with at least Developer role (and repository write scope), then set SALESFORCE_AUTH_URL, GITLAB_PUSH_USERNAME, GITLAB_PUSH_EMAIL, and GITLAB_PUSH_TOKEN. The sample rules run only for a scheduled pipeline on the default branch when JOB_NAME is codeSmells (add that variable on the schedule in GitLab, or change rules). Adjust tags for your runners. Details: docs/ENVIRONMENT.md and GitLab — project access token in the same section.
  2. Get those files into the runtime container (pick one):

    • Mounts (no image rebuild): copy or sync the committed files to the host/CI artifact store, then bind-mount or use a ConfigMap / volume (watch size limits for very large pmd-report.xml). Set PMD_RULESET_PATH and redeploy the same public image tag when you refresh reports.
    • Private image: docker build -f docker/Dockerfile from a branch that contains the refreshed files and remove or trim the src/sfmon/tech_debt/* lines in .dockerignore so COPY src/sfmon/ bakes them in; push to your registry and redeploy when reports change.
  3. Opt in via config.json: These collectors have no default schedule. Add monitor_pmd_code_smells and monitor_minimal_perm_sets under schedules with a cron string (see docs/CONFIGURATION.md). If your file uses a non-empty schedules block, list every other job you still want as well.

If PMD_RULESET_PATH is unset or the ruleset file is missing, PMD metrics are skipped (quiet at INFO). If minimal-perm-sets.json is missing, that collector logs a warning and exits. More detail: docs/ENVIRONMENT.md.


When you need your own image

The default image covers standard monitoring: env vars + optional JSON config. It excludes org-specific PMD/perm-set files by default (see .dockerignore) — for baking those in, see PMD + minimal permission sets above.

Build and run your own image if you need to change application code — new checks, different logic, pinned dependencies, private registry policy, or anything not covered by env/config.

docker build \
  -f docker/Dockerfile \
  -t your-registry/sfmon:latest .

docker push your-registry/sfmon:latest   # if using a registry

Then run your-registry/sfmon:latest the same way as above (-e SALESFORCE_AUTH_URL=..., ports, volumes).


Grafana

Import the JSON dashboards under grafana/ and point them at your Prometheus data source.


Authors

Originally developed by Deep Suthar and Matt Carvin (e.g. ECS / Kubernetes at Avalara).

Tag summary

Content type

Image

Digest

sha256:9772c8a3d

Size

119.2 MB

Last updated

23 days ago

docker pull mcarvin8/sfmon