Skip to main content

Creating your own templates

A template is just YAML. Build it top-down: decide what to measure (tasks), how to extract the numbers (filters), which modules to expose (agents/modules), and how to deliver (transfer).

Step 1 — Start from a shipped example

templates/local_commands_example.yml is the shortest complete template and demonstrates all three regexp result shapes. Copy it and replace the tasks:

cp templates/local_commands_example.yml templates/my_template.yml

Step 2 — Write the tasks

Each task runs one command or one HTTP request and stores its (optionally filtered) output in a variable.

tasks:
  # A scalar: one value, no filter (trimmed raw output is stored).
  - name: uptime_seconds
    type: local
    local: { command: "awk '{print int($1)}' /proc/uptime" }
    variable: uptime

  # A scalar via a single-capture regexp.
  - name: load_avg
    type: local
    local: { command: "cat /proc/loadavg" }
    filter:
      type: regexp
      expression: '^(\d+\.\d+)'
    variable: load1

  # An array of objects via named capture groups.
  - name: df
    type: local
    local: { command: "df -kTP" }
    filter:
      type: regexp
      expression: '(?m)^(?P<fs>\S+)\s+(?P<fstype>\S+)\s+(?P<total>\d+)\s+(?P<used>\d+)\s+\d+\s+(?P<pct>\d+)%\s+(?P<mount>.+)$'
    variable: filesystems

Step 3 — Turn variables into modules

A variable that is a scalar feeds one module directly; an array feeds for_each blocks (one module per element).

modules:
  - name: "Uptime seconds"
    type: generic_data
    data: "{{ uptime }}"

  - name: "Load average 1m"
    type: generic_data
    data: "{{ load1 }}"

  # One module per filesystem; value.* reads the named capture groups.
  - for_each: filesystems
    when: 'value.fstype != "tmpfs"'
    name: "DiskUsed_{{ value.mount }}"
    type: generic_data
    data: "{{ value.pct }}"
    unit: "%"

Inside a for_each block:

  • {{ value }} is the current element, {{ index }} its 0-based position.
  • Named groups make each element an object → access fields as {{ value.mount }}.
  • 2+ unnamed groups make each element a positional array → access as {{ value[0] }}, {{ value[1] }}, ...; write several blocks with the same for_each to emit several modules per element.

Step 4 — Pick the transfer mode

transfer:
  mode: agent_plugin            # modules-only output; run as a software-agent plugin
transfer:
  mode: tentacle                # full agent XML to a Pandora server
  tentacle: { address: "pandora.example.com" }
transfer:
  mode: local                   # full agent XML written to a directory
  local: { directory: "/var/spool/pandora/data_in" }

Step 5 — Validate before shipping

pandora-plugin_exec -t templates/my_template.yml --dry-run -v

--dry-run prints the generated XML without transferring; -v shows which variables were stored and which modules were skipped. The parser is strict, so any typo in a field name fails immediately.

Common pitfalls

  • Flags after arguments don't work. Go flag parsing stops at the first non-flag token: put --dry-run before any positional argument.
  • Undefined variables skip the module, they don't render empty. Use {{ params.x ?? "default" }} or when: "'x' in params" for optional parameters.
  • filter.expression is not expr. It is jq or an RE2 regexp depending on filter.type. expr is only used inside {{ }} and in when:.
  • Double the backslash in regexp literals inside when: — expr string literals process escapes, so \\d delivers \d to the regexp engine.
  • RE2 has no lookahead/backtracking — use when: for exclusions (e.g. when: 'not (value.mount matches "^/DB")').