{"id":37135305,"url":"https://github.com/containeroo/heartbeats","last_synced_at":"2026-01-14T15:46:53.982Z","repository":{"id":63622539,"uuid":"568790227","full_name":"containeroo/heartbeats","owner":"containeroo","description":"Wait for heartbeats and notify if they are missing","archived":false,"fork":false,"pushed_at":"2025-12-03T21:36:37.000Z","size":1512,"stargazers_count":4,"open_issues_count":4,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-12-04T00:59:51.252Z","etag":null,"topics":["golang","heartbeats","monitoring"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/containeroo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-11-21T12:17:50.000Z","updated_at":"2025-12-01T12:38:27.000Z","dependencies_parsed_at":"2023-12-18T22:08:29.828Z","dependency_job_id":"c3df871b-9bcc-4008-bd5f-57a12eafc8c8","html_url":"https://github.com/containeroo/heartbeats","commit_stats":{"total_commits":188,"total_committers":2,"mean_commits":94.0,"dds":"0.058510638297872286","last_synced_commit":"867abed88e1aae7fd9e86bbed1c91158eaa70d03"},"previous_names":[],"tags_count":109,"template":false,"template_full_name":null,"purl":"pkg:github/containeroo/heartbeats","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/containeroo%2Fheartbeats","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/containeroo%2Fheartbeats/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/containeroo%2Fheartbeats/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/containeroo%2Fheartbeats/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/containeroo","download_url":"https://codeload.github.com/containeroo/heartbeats/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/containeroo%2Fheartbeats/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28424780,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T15:24:48.085Z","status":"ssl_error","status_checked_at":"2026-01-14T15:23:41.940Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["golang","heartbeats","monitoring"],"created_at":"2026-01-14T15:46:53.449Z","updated_at":"2026-01-14T15:46:53.969Z","avatar_url":"https://github.com/containeroo.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Heartbeats\n\n![heartbeats.png](./web/static/icons/apple-touch-icon.png)\n\n[![Go Report Card](https://goreportcard.com/badge/github.com/containeroo/heartbeats?style=flat-square)](https://goreportcard.com/report/github.com/containeroo/heartbeats)\n[![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/containeroo/heartbeats)\n[![Release](https://img.shields.io/github/release/containeroo/heartbeats.svg?style=flat-square)](https://github.com/containeroo/heartbeats/releases/latest)\n[![GitHub tag](https://img.shields.io/github/tag/containeroo/heartbeats.svg?style=flat-square)](https://github.com/containeroo/heartbeats/releases/latest)\n![Tests](https://github.com/containeroo/heartbeats/actions/workflows/tests.yml/badge.svg)\n[![Build](https://github.com/containeroo/heartbeats/actions/workflows/release.yml/badge.svg)](https://github.com/containeroo/heartbeats/actions/workflows/release.yml)\n[![license](https://img.shields.io/github/license/containeroo/heartbeats.svg?style=flat-square)](LICENSE)\n\n---\n\nA lightweight HTTP service for monitoring periodic \"heartbeat\" pings (\"bumps\") and notifying configured receivers when a heartbeat goes missing or recovers. Includes an in‐browser read-only dashboard showing current heartbeats, receivers, and historical events.\n\n## 🚀 Features\n\n- **Heartbeat monitoring** with configurable `interval` \u0026 `grace` periods\n- **Pluggable notifications** via Slack, Email, or MS Teams\n- **In-memory history** of recent events (received, failed, state changes, notifications, API requests)\n  - Stores up to `--history-size` entries (default: 10,000)\n  - Not persisted across restarts (~1.73 MB at max size)\n- **Dashboard** with:\n  - **Heartbeats**: status, URL, last bump, receivers, quick-links\n  - **Receivers**: type, destination, last sent, status\n  - **History**: timestamped events, filter by heartbeat\n  - Text-search filters \u0026 copy-to-clipboard URLs\n- `/healthz` and `/metrics` endpoints for health checks \u0026 Prometheus\n- YAML configuration with variable resolution via [containeroo/resolver](https://github.com/containeroo/resolver)\n\n## 🏁 Flags\n\n| Flag                  | Shorthand | Default                 | Environment Variable           | Description                                                                                 |\n| --------------------- | --------- | ----------------------- | ------------------------------ | ------------------------------------------------------------------------------------------- |\n| `--config`            | `-c`      | `heartbeats.yml`        | `HEARTBEATS_CONFIG`            | Path to configuration file                                                                  |\n| `--listen-address`    | `-a`      | `:8080`                 | `HEARTBEATS_LISTEN_ADDRESS`    | Address to listen on (host:port)                                                            |\n| `--route-prefix`      |           | ``                      | `HEARTBEATS_ROUTE_PREFIX`      | ath prefix to mount the app (e.g., /heartbeats).                                            |\n| `--site-root`         | `-r`      | `http://localhost:8080` | `HEARTBEATS_SITE_ROOT`         | Base URL for dashboard and link rendering                                                   |\n| `--history-size`      | `-s`      | `10000`                 | `HEARTBEATS_HISTORY_SIZE`      | Number of historical events to keep in memory (default ~1.73 MB for 10,000). Not persisted. |\n| `--skip-tls`          | -         | `false`                 | `HEARTBEATS_SKIP_TLS`          | Skip TLS verification for all receivers (can be overridden per receiver)                    |\n| `--debug`             | `-d`      | `false`                 | `HEARTBEATS_DEBUG`             | Enable debug-level logging                                                                  |\n| `--debug-server-port` | `-p`      | `8081`                  | `HEARTBEATS_DEBUG_SERVER_PORT` | Port for the debug server                                                                   |\n| `--log-format`        | `-l`      | `text`                  | `HEARTBEATS_LOG_FORMAT`        | Log format (`json` or `text`)                                                               |\n| `--retry-count`       | -         | `3`                     | `HEARTBEATS_RETRY_COUNT`       | Number of times to retry a failed notification. Use `-1` for infinite.                      |\n| `--retry-delay`       | -         | `2s`                    | `HEARTBEATS_RETRY_DELAY`       | Delay between retries. Must be ≥ 1s.                                                        |\n| `--help`              | `-h`      | -                       | -                              | Show help and exit                                                                          |\n| `--version`           | -         | -                       | -                              | Print version and exit                                                                      |\n\n### Proxy Environment Variables\n\n- `HTTP_PROXY`: URL of the proxy server to use for HTTP requests\n- `HTTPS_PROXY`: URL of the proxy server to use for HTTPS requests\n\n## 🌐 Endpoints\n\n| Path              | Method        | Description                       |\n| :---------------- | :------------ | :-------------------------------- |\n| `/`               | `GET`         | Dashboard home page               |\n| `/bump/{id}`      | `POST`, `GET` | Create a new heartbeat            |\n| `/bump/{id}/fail` | `POST`, `GET` | Manually mark heartbeat as failed |\n| `/healthz`        | `GET`         | Liveness probe                    |\n| `/metrics`        | `GET`         | Prometheus metrics endpoint       |\n\n## ⚙️ Configuration\n\n`heartbeats` and `receivers` must be defined in your YAML file (default `config.yaml`).\n\n### Examples (`config.yaml`)\n\n```yaml\n---\nreceivers:\n  dev-crew-int:\n    slack_configs:\n      - channel: integration\n        token: env:SLACK_TOKEN\nheartbeats:\n  prometheus-int:\n    description: \"Prometheus → Alertmanager test\"\n    interval: 15m\n    grace: 90s\n    receivers:\n      - dev-crew-int\n```\n\n### Heartbeats\n\nA **heartbeat** waits for periodic pings (\"bumps\"). If no bump arrives within `interval + grace`, notifications are sent.\n\nTo reduce noise from race conditions (e.g. pings arriving milliseconds after grace timeout), Heartbeats adds a short internal delay before transitioning to `grace` or `missing`. This ensures smoother handling of near-expiry bumps without affecting responsiveness.\n\n| Key           | Type       | Description                                                                     |\n| :------------ | :--------- | :------------------------------------------------------------------------------ |\n| `description` | `string`   | (optional) Human-friendly description                                           |\n| `interval`    | `duration` | Required. Go duration (e.g. `15m`, `90s`) for expected interval between pings   |\n| `grace`       | `duration` | Required. Go duration after `interval` before marking missing                   |\n| `receivers`   | `[]string` | Required. List of receiver IDs (keys under `receivers:`) to notify upon missing |\n\n#### Example\n\n```yaml\nheartbeats:\n  prometheus-int:\n    description: \"Prometheus → Alertmanager test\"\n    interval: 15m\n    grace: 90s\n    receivers:\n      - dev-crew-int\n```\n\n### Receivers\n\nEach **receiver** can have multiple notifier configurations. Supported under `receivers:`:\n\n- `slack_configs`\n- `email_configs`\n- `msteams_configs`\n\nYou may use any template variable from the heartbeat (e.g. `{{ .ID }}`, `{{ .Status }}`), and these helper functions:\n\n- **`upper`**: `{{ upper .ID }}`\n- **`lower`**: `{{ lower .ID }}`\n- **`formatTime`**: `{{ formatTime .LastBump \"2006-01-02 15:04:05\" }}`\n- **`ago`**: `{{ ago .LastBump }}`\n- **`isRecent`**: `{{ isRecent .LastBump }}` // isRecent returns true if the last bump was less than 2 seconds ago\n- **`join`**: `{{ join .Tags \", \" }}`\n\n#### Variable Resolution\n\n`Heartbeats` uses [containeroo/resolver](https://github.com/containeroo/resolver) for variable resolving.\n\nResolver supports:\n\n- **Plain**: literal value\n- **Environment**: `env:VAR_NAME`\n- **File**: `file:/path/to/file`\n- **Within-file**: `file:/path/to/file//KEY`, also supported `yaml:`,`json:`,`ini:` and `toml:`. For more details see [containeroo/resolver](https://github.com/containeroo/resolver).\n\n#### Slack\n\n_Defaults:_\n\n- subject_tmpl: `[{{ upper .Status }}] {{ .ID }}\"`\n- text_tmpl: `{{ .ID }} is {{ .Status }} (last bump: {{ ago .LastBump }})\"`\n\n```yaml\nreceivers:\n  dev-crew-int:\n    slack_configs:\n      - channel: \"#integration\"\n        token: env:SLACK_TOKEN\n        # optional custom templates:\n        title_tmpl: \"[{{ upper .Status }}] {{ .ID }}\"\n        text_tmpl: \"{{ .ID }} status: {{ .Status }}\"\n        # optional: override global skip TLS\n        skip_tls: true\n```\n\n\u003e `Heartbeats` adds a custom `User-Agent: Heartbeats/\u003cversion\u003e` header to all outbound HTTP requests.\n\u003e The `Content-Type` header is also set to `application/json`.\n\n#### Email\n\n_Defaults:_\n\n- subject_tmpl: `\"[HEARTBEATS]: {{ .ID }} {{ upper .Status }}\"`\n- body_tmpl: `\"\u003cb\u003eDescription:\u003c/b\u003e {{ .Description }}\u003cbr\u003eLast bump: {{ ago .LastBump }}\"`\n\n```yaml\nemail_configs:\n  - smtp:\n      host: smtp.gmail.com\n      port: 587\n      from: admin@example.com\n      username: env:EMAIL_USER\n      password: env:EMAIL_PASS\n      # optional\n      start_tls: true\n      # optional: override global skip TLS\n      skip_insecure_verify: true\n    email:\n      is_html: true\n      to: [\"ops@example.com\"]\n      # optional custom templates:\n      subject_tmpl: \"[HB] {{ .ID }} {{ upper .Status }}\"\n      body_tmpl: \"Last bump: {{ ago .LastBump }}\"\n```\n\n#### MS Teams (incomming webhook)\n\n_Defaults:_\n\n- title_tmpl: `\"[{{ upper .Status }}] {{ .ID }}\"`\n- text_tmpl: `\"{{ .ID }} is {{ .Status }} (last bump: {{ ago .LastBump }})\"`\n\n```yaml\nmsteams_configs:\n  - webhook_url: file:/secrets/teams/webhook//prod\n    # optional custom templates:\n    title_tmpl: \"[{{ upper .Status }}] {{ .ID }}\"\n    text_tmpl: \"{{ .ID }} status: {{ .Status }}\"\n    # optional: override global skip TLS\n    skip_tls: true\n```\n\n\u003e `Heartbeats` adds a custom `User-Agent: Heartbeats/\u003cversion\u003e` header to all outbound HTTP requests.\n\u003e The `Content-Type` header is also set to `application/json`.\n\n#### MS Teams (Graph API) (NOT TESTED)\n\n_Defaults:_\n\n- title_tmpl: `\"[{{ upper .Status }}] {{ .ID }}\"`\n- text_tmpl: `\"{{ .ID }} is {{ .Status }} (last bump: {{ ago .LastBump }})\"`\n\n```yaml\nmsteamsgraph_configs:\n  - channel_id: env:MSTEAMSGRAPH_CHANNEL_ID\n    team_id: env:MSTEAMSGRAPH_TEAM_ID\n    # optional custom templates:\n    title_tmpl: \"[{{ upper .Status }}] {{ .ID }}\"\n    text_tmpl: \"{{ .ID }} status: {{ .Status }}\"\n    # optional: override global skip TLS\n    skip_tls: true\n```\n\n\u003e `Heartbeats` adds a custom `User-Agent: Heartbeats/\u003cversion\u003e` header to all outbound HTTP requests.\n\u003e The `Content-Type` header is also set to `application/json`.\n\n## 📈 Metrics\n\nHeartbeats exposes the following Prometheus metrics via the `/metrics` endpoint:\n\n### Heartbeat Metrics\n\n| Name                                  | Type      | Labels                       | Description                                                                  |\n| ------------------------------------- | --------- | ---------------------------- | ---------------------------------------------------------------------------- |\n| `heartbeats_heartbeat_last_status`    | `gauge`   | `heartbeat`                  | Most recent status of each heartbeat (0 = DOWN, 1 = UP)                      |\n| `heartbeats_heartbeat_received_total` | `counter` | `heartbeat`                  | Total number of received heartbeats per ID                                   |\n| `heartbeats_receiver_last_status`     | `counter` | `receiver`, `type`, `target` | Reports the status of the last notification attempt (1 = ERROR, 0 = SUCCESS) |\n\n### History Store Metrics\n\n| Name                           | Type    | Description                                          |\n| ------------------------------ | ------- | ---------------------------------------------------- |\n| `heartbeats_history_byte_size` | `gauge` | Current size of the in-memory history store in bytes |\n\n## 🧪 Development\n\nDownload the binary and update the example [config.yaml](./tests/config.yaml) according your needs.\nIf you prefer to run heartbeats in docker, you find a `docker-compose.yaml` \u0026 `config.yaml` [here](./deploy/).\nFor a kubernetes deployment you find the manifests [here](./deploy/kubernetes).\n\n## 🔧 Debugging\n\nHeartbeats includes optional internal endpoints for testing receiver notifications and simulating heartbeats. These are only enabled when the `--debug` flag is set.\n\n### Internal Endpoints\n\n| Path                       | Method | Description                               |\n| :------------------------- | :----- | :---------------------------------------- |\n| `/internal/receiver/{id}`  | `GET`  | Sends a test notification to the receiver |\n| `/internal/heartbeat/{id}` | `GET`  | Simulates a bump for the given heartbeat  |\n\nThese endpoints listen only on `127.0.0.1`.\n\n#### Kubernetes\n\nForward the debug port to your local machine:\n\n```bash\nkubectl port-forward deploy/heartbeats 8081:8081\n\ncurl http://localhost:8081/internal/receiver/{id}\ncurl http://localhost:8081/internal/heartbeat/{id}\n```\n\n#### Docker\n\nBind the debug port only to your host's loopback interface:\n\n```bash\ndocker run \\\n  -p 127.0.0.1:8081:8081 \\\n  -v ./config.yaml:/config.yaml \\\n  containeroo/heartbeats\n\ncurl http://localhost:8081/internal/receiver/{id}\ncurl http://localhost:8081/internal/heartbeat/{id}\n```\n\n\u003e ✅ This ensures `/internal/*` endpoints are **only reachable from your local machine**, not from other containers or the network.\n\n\u003e ⚠️ **Warning:** These endpoints are meant for local testing and debugging only. Never expose them in production.\n\n## 📝 License\n\nThis project is licensed under the Apache 2.0 License. See the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcontaineroo%2Fheartbeats","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcontaineroo%2Fheartbeats","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcontaineroo%2Fheartbeats/lists"}