Monitor a Salesforce org and expose Prometheus metrics from a long-running Python service.
3.7K
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.
One process, no database, no UI:
: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.
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.
| Category | What is measured |
|---|---|
| Governor limits | All org limits (API requests, bulk queries, data storage, etc.) — usage %, used, and max, every 5 minutes |
| Apex health | Flex queue depth, long-running requests, concurrency errors, uncaught exceptions, async job status and summaries |
| Bulk API | Daily summaries and hourly in-flight activity across Bulk API 1.0 and 2.0 |
| Licenses | User licenses, permission set licenses, and usage-based entitlements — consumed vs. total, % used |
| Instance & trust | Your org's pod, active incidents from trust.salesforce.com, and scheduled maintenance windows |
| Security & compliance | Forbidden profile assignments, login volumes, geolocation anomalies, suspicious audit trail activity, report exports, large SOQL queries, org-wide sharing settings |
| Tech debt | Dormant users (Salesforce + portal), deprecated Apex API versions, unassigned/minimal permission sets, workflow rules, empty queues/groups, PMD static analysis violations |
| Deployments | In-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.
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.
docker run -d \
--name sfmon \
-p 9001:9001 \
-e SALESFORCE_AUTH_URL="force://PlatformCLI::..." \
-e ORG_NAME="production" \
mcarvin8/sfmon:latest
curl http://localhost:9001/metricsprometheus.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.
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:
pip install sfmon
SALESFORCE_AUTH_URL="force://PlatformCLI::..." ORG_NAME="production" sfmon
curl http://localhost:9001/metricsprometheus.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.
exclude_users) → docs/CONFIGURATION.md · template config.example.jsonSALESFORCE_AUTH_URL/SALESFORCE_AUTH_URL_<NAME> from AWS Secrets Manager instead of the environment (SECRETS_BACKEND=aws) → docs/ENVIRONMENT.mdTwo 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.
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.
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.
Two output shapes, matched to two different questions:
: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.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.
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.
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.
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
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.--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.--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.curl --data-binary it to a Pushgateway, or grep it for a threshold check.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 .
| Input | Required | Maps to |
|---|---|---|
job-id | yes | sfmon --once --job <id> |
auth-url | yes | SALESFORCE_AUTH_URL |
org-name | no | ORG_NAME (org label on the metrics) |
| Output | Description |
|---|---|
exit-code | 0/1/2 from sfmon --once — the step also fails naturally on 1/2 |
metrics-raw | Full Prometheus exposition text for the run |
metrics-json | Same 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.
If you only want a focused slice of monitoring, set a preset in config.json instead of listing every job:
{ "preset": "ops" }
| Preset | Focus |
|---|---|
ops | Apex health, Bulk API, deployments, EPT/APT |
audit | Login events, geolocation, suspicious activity, report exports, sharing settings |
tech-debt | Dormant 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.
| SFMon | Salesforce proactive monitoring (paid) | sfdx-hardis org monitoring | |
|---|---|---|---|
| Model | Always-on container/process (Prometheus /metrics), or a scheduled CI job via --once | Salesforce TAM/CSM engagement + event log files | Scheduled CI jobs (GitHub Actions / GitLab CI) |
| Output | Time-series metrics scraped by Prometheus | Salesforce-native reports and guided reviews | Git diffs, Slack/Teams notifications, pipeline artifacts |
| Alerting | PromQL + Alertmanager (same as rest of infra), or built-in Slack webhook alerting with no Alertmanager needed | Salesforce notifications and Success Plan reviews | Slack/Teams webhooks from CI |
| Data stays in your stack | Yes | No (Salesforce-hosted) | Partially (metadata to Git; notifications to Slack/Teams) |
| Extra cost | Free (open source) — compute to run the container, or a CI cron job via --once | Salesforce edition / add-on fee | Free (open source) — compute for the CI cron job |
| Best for | SRE/DevOps teams already on Prometheus who want Salesforce as just another scrape target | Teams buying Salesforce-managed oversight and guidance | Teams 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.
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:
| File | In-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:
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/workflows/update-local-reports.yml — repository secret SALESFORCE_AUTH_URL (SFDX URL). Optional workflow_dispatch input manifest_path (default manifest/package.xml)..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.Get those files into the runtime container (pick one):
pmd-report.xml). Set PMD_RULESET_PATH and redeploy the same public image tag when you refresh reports.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.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.
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).
Import the JSON dashboards under grafana/ and point them at your Prometheus data source.
Originally developed by Deep Suthar and Matt Carvin (e.g. ECS / Kubernetes at Avalara).
Content type
Image
Digest
sha256:9772c8a3d…
Size
119.2 MB
Last updated
23 days ago
docker pull mcarvin8/sfmon