elm is a CLI tool that queries the LogicMonitor REST API and outputs data in multiple formats: CSV, JSON, HTML, Markdown, XML, LaTeX, RST, tab, jira, raw, txt, and api. It uses Click for the CLI framework and pandas for output formatting.
Do not edit generated files directly.
The build pipeline is:
- make init -- downloads the LM Swagger JSON spec, splits it into per-endpoint definition files in _defs/
- make render -- runs Jinja2 templates from _jnja/ against those definitions to generate Python source files in _cmds/
- make render also generates _cmds/init.py (via touch) so PyInstaller can treat _cmds/ as a proper package and collect it with --collect-all
- elm.py uses LazyGroup to defer _cmds/ module imports until a subcommand is actually invoked -- --version, --help, and --list load without importing any _cmds/ module or heavy library (pandas, requests, etc.)
The source of truth for command logic is _jnja/ (templates) and _defs/ (per-endpoint JSON definitions). If you fix a bug in a _cmds/ file, the fix will be overwritten next time make render runs. Fix it in the template instead.
_jnja/ Jinja2 templates -- EDIT THESE, not the output
_defs/ Per-endpoint JSON definitions (generated by make init)
_cmds/ Generated Python command modules (DO NOT EDIT)
_build/ PyInstaller build artefacts (DO NOT EDIT)
_dist/ Compiled binary output (DO NOT EDIT)
elm.py Main entry point -- imports from _cmds/
setup.py Package setup
requirements.txt Python dependencies
Makefile Build orchestration -- source of truth for build steps
swagger.undocumented.json Snapshot of LM Swagger spec (checked in)
config.example.ini Example credentials config
make Full build (init + render + cfg)
make init Download swagger, create _defs/
make render Generate _cmds/ from _jnja/ templates
make install Build binary via PyInstaller into _dist/
make clean Remove all generated files
make test Quick offline tests
make testbasic Test CLI flags (mostly offline; the -f api/curl/wget/sqlite
assertions send a real request and need the live config profile)
make testverb Test verbose flags (requires LM connection)
Dev loop: make clean && make && make install
(make install already copies both the elm executable and its _internal/
dependency dir into ~/bin — see the $(bindir)/$(name) recipe — so no manual
cp afterwards is needed.)
Sandbox note: in a Linux sandbox with the host tree mounted directly, running
make writes a Linux binary into _dist/ (and regenerates _cmds/,
engine.py, elm.py, etc.). These are gitignored, so nothing is committed,
but they overwrite the host's macOS build — _dist/elm/elm will then fail on
macOS with "cannot execute binary file". Rebuild on the Mac (make clean && make && make install) to restore it. Also note make re-downloads the
documented swagger from logicmonitor.com; if that host is firewalled a 127-byte
block page silently clobbers _defs/swagger.json and the build proceeds with
only the ~15 undocumented commands. To build from source in a sandbox without
touching the host venv/ (which is macOS-arch), point make at an isolated
venv: make VENV=/path/to/lxvenv JINJA=/path/to/lxvenv/bin/jinja2 (PyInstaller
on Linux additionally needs binutils and a shared libpython).
There is no pytest/unittest suite — ignore the generic pytest guidance in the parent ../CLAUDE.md. Tests are Makefile-driven and run against the built PyInstaller binary (_dist/elm/elm):
make testbasicis mostly offline — CLI flags, help/version,--list,--ai, profile/config resolution, and the per-command<cmd> --helploop never contact LM. But its-f api,-f curl,-f wgetand-f sqliteassertions are NOT offline: those formats print the URL/command only after a successfulresponse.raise_for_status()+response.json(), so they send a real request and need the live defaultconfigprofile (with dummy creds they 403 and fail). Run it after any template change, but expect those lines to need LM. (The old claim that they "build the URL without sending it" is wrong — the request is sent first.)make testfmtcontentasserts each output format really is that format. The api/curl/wget assertions also send a real request (same reason as above); the rest hit LM via MetricsUsage.- Targets marked "(connects to LM)" (testfmts, testcount, testtotal, testverb,
testid, testH/I, testhead/foot) need live credentials and the default
configprofile.
Run make test for the quick suite; make testlong for the slow ones.
- Uses a Python venv at venv/ (created by make)
- Config file: ~/.config/logicmonitor/credentials/config.ini (unrepr format, see configobj docs)
- Required config keys: access_id, access_key, account_name
- Shell completion: elm-completion.bash is a generated file (rendered from _jnja/elm-completion.bash.j2 by make render). It is not committed to the repo. make completion installs it to $XDG_DATA_HOME/bash-completion/completions/elm (default ~/.local/share/bash-completion/completions/elm). bash-completion 2.x sources it automatically.
elm supports multiple credential profiles:
--profile NAMEresolves to~/.config/logicmonitor/credentials/<NAME>.ini--config PATHaccepts a full path to any .ini file in any directoryelm --list(orelm -l) lists all available profiles and exits, marking the active one with*. Use this instead of reading the credentials directory.
Profile naming convention:
config.iniis always the default and should always point to the safest environment (sandbox/test). Do not put production credentials here.- Non-default environments get explicit names (e.g.
preprod.ini,prod.ini) and must be selected deliberately with--profile. config.example.iniis not a real profile — it is skipped byelm --list.
At the start of any session involving live API calls:
-
Run
elm --listto see what profiles are available and which is active. -
For testing, development, and learning: always use the default
configprofile (i.e. no --profile flag). -
For audits or cross-platform work: run
elm --list, then ask "Which platform do you want to check?" and pass--profile NAMEfor each non-default environment. Example: elm --profile preprod AuditLogList -s0 ... -
Never read the contents of .ini files — they contain credentials. Only
elm --listto discover what profiles are available.
- REST API v2 (v1 also supported via apiversion=1 make variable)
- Auth: HMAC-SHA256 signed requests, not OAuth
- Base URL: https://{account_name}.logicmonitor.com/santaba/rest
- Swagger spec: fetched fresh by make init, snapshot in swagger.undocumented.json
If you see audit log entries where username is the literal string
(update), that is the actual access_id value configured in
whatever tool or integration is making those requests. LM logs the
access_id as the username — it does not substitute or redact
deleted tokens.
Confirmed by test: running elm -i "(update)" -k "..." DeviceList
produced an audit log entry with username: "(update)" immediately.
This means: somewhere in the target portal, a script or integration has
access_id = (update) (likely a misconfigured or placeholder
credential). Track down the source by looking at the IP field in the
audit log entry and the access path in the description:
elm AuditLogList -F 'username:(update)' -s0
The ip field will show the originating host; the description will
show which API path it is hitting.
csv, tsv, html, prettyhtml, jira, json, jsonl, prettyjson, xml, prettyxml, latex, md, rst, gfm, pipe, tab, raw, txt, api (api makes the request then prints the URL and Authorization header — header contains HMAC signature, do not share)
Key packages: click, pandas, requests, configobj, pyinstaller, jinja2. See requirements.txt for pinned versions.
-
engine.py, elm.py, and elm-completion.bash are all generated files, rendered from _jnja/engine.py.j2, _jnja/elm.py.j2, and _jnja/elm-completion.bash.j2 respectively. None are committed to the repo. Fix bugs in the _jnja/ templates, not in the generated files directly.
-
_cmds/*.py files look like source but are generated artefacts. Linters and type checkers will flag issues in them that should be fixed in _jnja/ templates, not in the files themselves.
-
elm.py uses LazyGroup to dynamically import _cmds/ modules via importlib.import_module() at dispatch time. Static analysis will show import errors that are not real errors. PyInstaller requires --collect-all=_cmds and _cmds/init.py to bundle them correctly.
-
The Makefile does a lot of heavy lifting. Read it before assuming something is missing from the Python code.
-
swagger.undocumented.json contains endpoints not in the official LM docs. This is intentional.
The project builds and runs. It uses the pip + venv workflow (a past partial Poetry migration was abandoned and removed — do not reintroduce Poetry unless doing a deliberate packaging migration).
Environment notes:
- Python 3.14 is validated: a full clean build runs cleanly on 3.14.6 with
the current pins (pandas 2.3.3, numpy 2.5.0, pyinstaller 6.21.0). If a
future dependency bump surfaces incompatibilities, fall back to a
conservative interpreter with
make PYTHON=python3.12. setup.pyships the generated top-level modules viapy_modules=['elm', 'engine', '_version'](notfind_packages()).
Resolved (kept here so the history is not re-investigated):
- Slow startup — fixed in v1.8.0 via LazyGroup + deferred heavy imports
(
--versionnow loads in ~0.2s). make -ninfinite recursion / duplicated build from a recursive$(MAKE)in the_defs/commands.jsonrecipe — fixed; the build now calls explicit sub-targets ($(MAKE) _render _build).- Query params are sent structured (
requests.get(..., params=...)in_jnja/engine.py.j2), not hand-concatenated.
Removed:
-x/--export(export a query as a standalone script) was a never-finished stub carried over from an older project — its handler referenced anelm.flagsattribute that was never set, used jinja2 symbols it never imported, and rendered a template that did not exist, so the advertised flag crashed on use. It was removed from_jnja/elm.py.j2,_jnja/engine.py.j2, and the README. The reproduce -a-request need it was meant to serve is already covered by-f api(prints the exact request URL + auth header) plus the PyInstaller binary.
Deferred work lives in todo.md. Agreed follow-ups from the 2026-07 audit
live in RECOMMENDATIONS.md — work items are written there in full so any
assistant can execute them; do them in order, one at a time.
- Edit files in _cmds/ directly
- Edit files in _defs/ directly
- Edit files in _build/ or _dist/
- Edit elm-completion.bash directly — it is generated from _jnja/elm-completion.bash.j2
- Regenerate _cmds/ and commit without also committing the template change in _jnja/ that produced them