plotpoint

One request and you have a time series.

No signup and no setup step. Your first write picks your account name and key.

curl https://plotpoint.torvald.no/m/your-account/pick-a-key/g/cpu_temp/41.5

Then open https://plotpoint.torvald.no/d/your-account/pick-a-key to see your chart.

Live: writes to this service

counter · writes per minute, all accounts, last hour

This is a counter: how many write requests the service received each minute, from everyone. It is not the cpu_temp value from the example above. Counter charts always show at least 0–5 on the y-axis, so a quiet minute with one or two writes doesn't fill the chart.

How keys work

Endpoints

RequestWhat it does
GET|POST /m/{acct}/{key}/g/{metric}/{value}Gauge: records a value at a point in time
GET|POST /m/{acct}/{key}/c/{metric}[/{n}]Counter: adds n, default 1. Charted as the total per time step.
GET|POST /m/{acct}/{key}/t/{metric}/{ms}Timing, in milliseconds
GET|POST /m/{acct}/{key}/e/{name}/{text}Event: URL-encoded text, drawn as a marker on every chart
?host=nas&room=atticQuery parameters become labels (max 8)
?ts=1767225600Sets the timestamp (unix seconds or milliseconds)
GET /d/{acct}/{key}Your dashboard
GET /q/{acct}/{key}/{metric}?from=-24h&to=nowSeries as JSON: {"t":[…],"v":[…],"events":[…]}. Optional &step=60 (10, 15, 30, 60, 120, 300 … seconds) sets the resolution.
GET /s/{acct}/{key}Your metrics, with last value and when each was last seen
GET /p/{token}Read-only shared view (create one from the dashboard)
GET /badge/{token}/{metric}.svgStatus badge

Responses: 204 stored (empty body) · 400 invalid input · 403 wrong key · 429 over a limit, with the reason in a JSON body. Names (account, metric, label key) must match ^[a-zA-Z_][a-zA-Z0-9_.:-]{0,63}$. The label name acct is reserved.

Examples

Replace your-account/pick-a-key with your own. Every snippet has a copy button.

Shell basics
U=https://plotpoint.torvald.no/m/your-account/pick-a-key
curl -s "$U/g/disk_free_gb/$(df -BG --output=avail / | tail -1 | tr -dc 0-9)?host=$(hostname)"
curl -s "$U/c/backups_ok"
curl -s "$U/e/deploy/$(printf 'v1.4 shipped' | jq -sRr @uri)"

POST works too

curl -s -X POST "https://plotpoint.torvald.no/m/your-account/pick-a-key/c/signups/3"
Linux host health

Drop this in /usr/local/bin/pp-health and run it from cron every minute. It sends six gauges per host.

#!/bin/sh
U=https://plotpoint.torvald.no/m/your-account/pick-a-key
H="host=$(hostname)"
read l1 l5 l15 _ < /proc/loadavg
mem=$(awk '/MemAvailable/ {print int($2/1024)}' /proc/meminfo)
disk=$(df --output=pcent / | tail -1 | tr -dc 0-9)
up=$(cut -d' ' -f1 /proc/uptime)
temp=$(( $(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0) / 1000 ))
for kv in load1=$l1 load5=$l5 mem_avail_mb=$mem disk_used_pct=$disk uptime_s=$up cpu_temp=$temp; do
  curl -s -o /dev/null "$U/g/${kv%%=*}/${kv#*=}?$H"
done
* * * * * /usr/local/bin/pp-health

Failed systemd units

curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/failed_units/$(systemctl --failed --no-legend | wc -l)?host=$(hostname)"

Pending apt upgrades (Debian/Ubuntu)

curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/apt_upgradable/$(apt list --upgradable 2>/dev/null | grep -c upgradable)?host=$(hostname)"
Cron jobs and backups

Count successes and failures separately, and record how long the job took.

#!/bin/sh
U=https://plotpoint.torvald.no/m/your-account/pick-a-key
t0=$(date +%s%3N)
if restic backup /home; then
  curl -s "$U/c/backup_ok?job=home"
else
  curl -s "$U/c/backup_failed?job=home"
fi
curl -s "$U/t/backup_ms/$(( $(date +%s%3N) - t0 ))?job=home"

Every 5 minutes: 1-minute load average

*/5 * * * * curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/load1/$(cut -d' ' -f1 /proc/loadavg)?host=$(hostname)" >/dev/null
GitHub Actions

Put the write URL in a repository secret named PLOTPOINT_URL (for example https://plotpoint.torvald.no/m/your-account/pick-a-key) so the key stays out of logs.

      - run: echo "T0=$(date +%s)" >> "$GITHUB_ENV"   # first step of the job

      # … your build and test steps …

      - name: Report to Plotpoint
        if: always()
        env:
          U: ${{ secrets.PLOTPOINT_URL }}
        run: |
          curl -s "$U/c/ci_${{ job.status }}?repo=${{ github.event.repository.name }}"
          curl -s "$U/t/ci_ms/$(( ($(date +%s) - T0) * 1000 ))?repo=${{ github.event.repository.name }}"

Gives you ci_success and ci_failure counters plus a ci_ms timing, labelled by repository.

Mark deploys on every chart

      - name: Deploy marker
        run: curl -s "${{ secrets.PLOTPOINT_URL }}/e/deploy/$(printf '%s' "${{ github.sha }}" | cut -c1-7)"
Python

Standard library only.

import time, urllib.request
U = "https://plotpoint.torvald.no/m/your-account/pick-a-key"
t0 = time.monotonic()
do_the_work()
urllib.request.urlopen(f"{U}/t/job_ms/{(time.monotonic() - t0) * 1000:.0f}?job=nightly")

A small helper that never breaks your program

import urllib.parse, urllib.request

U = "https://plotpoint.torvald.no/m/your-account/pick-a-key"

def pp(kind, metric, value="", **labels):
    url = f"{U}/{kind}/{metric}"
    if value != "":
        url += "/" + urllib.parse.quote(str(value), safe="")
    if labels:
        url += "?" + urllib.parse.urlencode(labels)
    try:
        urllib.request.urlopen(url, timeout=2)
    except Exception:
        pass   # metrics must never take the app down

pp("g", "queue_len", 17, worker="a")
pp("c", "emails_sent")
pp("e", "deploy", "v1.4 shipped")

With requests

import requests
requests.get("https://plotpoint.torvald.no/m/your-account/pick-a-key/g/room_temp/21.4", params={"room": "office"}, timeout=2)
Docker

Running containers

curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/containers_running/$(docker ps -q | wc -l)?host=$(hostname)"

Unhealthy containers

curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/containers_unhealthy/$(docker ps -q --filter health=unhealthy | wc -l)?host=$(hostname)"
Uptime and HTTP checks

Response time from wherever the check runs, and a counter for failures.

#!/bin/sh
U=https://plotpoint.torvald.no/m/your-account/pick-a-key
SITE=https://example.com
ms=$(curl -s -o /dev/null -w '%{time_total}' --max-time 10 "$SITE" | awk '{print int($1*1000)}')
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "$SITE")
curl -s "$U/t/http_ms/$ms?site=example"
[ "$code" = 200 ] || curl -s "$U/c/http_fail?site=example&code=$code"

TLS certificate days left

end=$(echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/cert_days/$(( ($(date -d "$end" +%s) - $(date +%s)) / 86400 ))?site=example"
Home and IoT

Raspberry Pi temperature

curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/cpu_temp/$(vcgencmd measure_temp | tr -dc 0-9.)?host=pi"

MicroPython (ESP32, Pico W)

import urequests
r = urequests.get("https://plotpoint.torvald.no/m/your-account/pick-a-key/g/humidity/%.1f?room=attic" % sensor.humidity())
r.close()

Home Assistant rest_command

rest_command:
  plotpoint:
    url: "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/{{ metric }}/{{ value }}?room={{ room }}"
Routers and network
# busybox wget needs to be built with TLS support for https
wget -q -O /dev/null "https://plotpoint.torvald.no/m/your-account/pick-a-key/g/wan_rx_bytes/$(cat /sys/class/net/eth0/statistics/rx_bytes)?host=router"

Ping latency

ms=$(ping -c 3 -q 1.1.1.1 | awk -F/ '/^rtt|^round-trip/ {print int($5)}')
curl -s "https://plotpoint.torvald.no/m/your-account/pick-a-key/t/ping_ms/$ms?target=cloudflare"
JavaScript and Node.js
const U = "https://plotpoint.torvald.no/m/your-account/pick-a-key";
const pp = (kind, metric, value = "", labels = {}) =>
  fetch(`${U}/${kind}/${metric}${value === "" ? "" : "/" + encodeURIComponent(value)}?${new URLSearchParams(labels)}`)
    .catch(() => {});   // never let metrics break the app

pp("c", "orders");
pp("t", "render_ms", Math.round(performance.now()), { page: "home" });
PowerShell (Windows)
$U = "https://plotpoint.torvald.no/m/your-account/pick-a-key"
$free = [math]::Round((Get-PSDrive C).Free / 1GB, 1)
Invoke-RestMethod "$U/g/disk_free_gb/$($free)?host=$($env:COMPUTERNAME)" | Out-Null

Limits

Active series per account50 (a series is a metric plus its labels; it stays active until 30 days with no writes)
Samples per account per day10 000 (resets at UTC midnight)
Retention30 days
Request rate per account10/s sustained, burst 60
Labels per series8, values up to 128 characters
Chart resolutionat most 2000 points per response

Writing a new series past the limit returns 429 with the reason. Nothing is dropped without telling you.

Link previews from chat apps (Slack, Discord, WhatsApp and others) get a 204 but store nothing, so pasting a write URL into a chat can't create an account or record a value.