# iCalendar Timeseries Server This project is a small service that scrapes iCalendar files served over HTTP, parses their contents and returns the data in a timeseries format compatible to the `/api/v1/query` API endpoint of a Prometheus server. This allows e.g. a Grafana administrator to add a Prometheus data source pointing at this server, returning calendar events in the `event` metric and todos in the `todo` metric. ## Example Consider the following iCalendar file: ``` BEGIN:VCALENDAR VERSION:2.0 PRODID:-//ACME//NONSGML Rocket Powered Anvil//EN BEGIN:VEVENT UID:20190603T032500CEST-foo SUMMARY:Foo DESCRIPTION:An example event DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20190603T032500 DTEND;TZID=Europe/Zurich;VALUE=DATE-TIME:20190603T040000 END:VEVENT BEGIN:VEVENT UID:20190603T032500CEST-bar SUMMARY:Bar DESCRIPTION:Another example event DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20190603T032500 DTEND;TZID=Europe/Zurich;VALUE=DATE-TIME:20190603T040000 END:VEVENT END:VCALENDAR ``` The server would transform this into the following API response: ```json { "status": "success", "data": { "resultType": "vector", "result": [ { "metric": { "__name__": "event", "calendar": "0", "uid": "20190603T032500CEST-foo", "summary": "Foo", "description": "An example event" }, "value": [ 1560043497, 1 ] }, { "metric": { "__name__": "event", "calendar": "1", "uid": "20190603T032500CEST-bar", "summary": "Bar", "description": "Another example event" }, "value": [ 1560043497, 1 ] } ] } } ``` ## Dependencies - `bottle`: Serve the results - `dateutil`: Work with recurring events - `icalendar`: Parse iCalendar - `isodate`: Parse ISO-8601 time periods - `jinja2`: Template value replacements ## Configuration Configuration is done through a JSON config file: ### Example ```json { "addr": "127.0.0.1", "port": 8090, "start_delta": "-PT3H", "end_delta": "P30D", "tz": "Europe/Zurich", "calendars": { "private": { "url": "https://example.cloud/dav/me/private.ics", "auth": { "type": "basic", "username": "me", "password": "mysupersecurepassword" } }, "public": { "interval": "P1D", "url": "https://example.cloud/dav/me/public.ics" }, "confidential": { "url": "https://example.cloud/dav/me/confidential.ics", "ca": "/etc/ssl/ca.pem", "auth": { "type": "tls", "keyfile": "/etc/ssl/client.pem", "passphrase": "mysupersecurepassword" } } }, "key_replace": { "summary": "01_summary", "description": "02_description" }, "value_replace": { "summary": "{{ summary|truncate(100) }}", "description": "{{ description|truncate(100) }}" } } ``` ### Explanation | JMESPath | Type | Description | |----------|------|-------------| | `addr` | string | The address to listen on. | | `port` | int | The port to listen on. | | `start_delta` | string | A signed ISO 8601 duration string, describing the event range start offset relative to the current time. | | `end_delta` | string | An unsigned ISO 8601 duration string, describing the event range end offset relative to the current time. | | `tz` | string | The local timezone. | | `calendars` | dict | The calendars to scrape. | | `keys(calendars)` | string | Name of the calendar. | | `calendars.*.url` | string | The HTTP or HTTPS URL to scrape. | | `calendars.*.interval` | string | An unsigned ISO 8601 duration string, describing the scrape interval for this calendar. | | `calendars.*.ca` | string | Path to the CA certificate file to validate the server's TLS certificate against, in PEM format (optional). | | `calendars.*.auth` | dict | Authorization config for the calendar. | | `calendars.*.auth[].type` | string | Authorization type, one of `none` (no authorization), `basic` (HTTP Basic Authentication), `tls` (TLS client certificate). | | `calendars.*.auth[?type=='basic'].username` | string | The Basic Auth username to authenticate with. | | `calendars.*.auth[?type=='basic'].password` | string | The Basic Auth password to authenticate with. | | `calendars.*.auth[?type=='tls'].keyfile` | string | Path to the key file containing the TLS private key, client certificate and certificate chain, in PEM format. | | `calendars.*.auth[?type=='tls'].passphrase` | string | Passphrase for the private key (optional). | | `key_replace` | dict | Labels to rename, might be necessary e.g. for column ordering in Grafana 6 and earlier. | | `keys(key_replace)` | string | The labels to rename. | | `key_replace.*` | string | The names to rename the labels to. | | `value_replace` | dict | Label values to postprocess. | | `keys(value_replace)` | string | Original label name to postprocess, also may introduce new labels. | | `value_replace.*` | string | The new value for the label. Supports Jinja2 templates. All original label values can be accessed by using their original name as a Jinja2 variable name. | ## Queries The most basic query is simply the metric name: ``` event ``` In addition, PromQL label filters can be used. ``` event{calendar="public",foo=~".*"} ``` Alongside with events, todos are exported in a second time series: ``` todo{status!="COMPLETED"} ``` ## Why Prometheus API - It's JSON. A JSON generator is builtin in Python, so no further dependency. - The Prometheus Data Source is builtin into Grafana. - The API is simple.