Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# ---------------------------------------------------------------------------
# Shared connection settings for every workshop in this repository.
#
# cp .env.example .env
# $EDITOR .env
#
# This ONE file at the repo root serves all workshops. Each workshop's
# reset_demo.sh reads it, and each docker-compose.yml passes it to Grafana, so
# credentials are entered once no matter how many workshops you run.
#
# .env is gitignored. Never commit real credentials.
# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# Tiger Cloud connection
# ---------------------------------------------------------------------------
# Copy these from the service's Connect panel in the Tiger Cloud console.
# PGSSLMODE must be 'require' for a Tiger Cloud service.
PGHOST=<service-id>.<project-id>.tsdb.cloud.timescale.com
PGPORT=<port>
PGDATABASE=tsdb
PGUSER=tsdbadmin
PGPASSWORD=<password>
PGSSLMODE=require

# A single connection string is accepted as an alternative to the six variables
# above. If both are present, the variables win.
# TIMESCALE_SERVICE_URL=postgres://tsdbadmin:<password>@<host>:<port>/tsdb?sslmode=require

# ---------------------------------------------------------------------------
# Grafana
# ---------------------------------------------------------------------------
GF_SECURITY_ADMIN_PASSWORD=admin

# Host port for Grafana. The workshops are designed to be run one at a time, so a
# single value serves all of them.
#
# NOTE: docker compose reads variables used INSIDE a compose file (like this port)
# from its own --env-file, not from `env_file:`. To make an override here take
# effect, start Grafana with:
#
# docker compose --env-file ../.env up -d
#
GRAFANA_PORT=3000
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ Agentic-Postgres-Workshop/schema.sql
BlockChain-Workshop/*.csv.gz
TimeSeries-Workshop-Finance/tutorial_sample_*.csv
TimeSeries-Workshop-Finance/crypto_sample.zip
TimeSeries-Workshop-Wind-Energy/grafana-geojson/wind-regions.geojson
306 changes: 306 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

847 changes: 847 additions & 0 deletions GRAFANA_PLATFORM.md

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ Use the Tiger MCP server with an AI coding assistant (e.g., Claude Code) to desi

**[>> Go to the Agentic Postgres Workshop](./Agentic-Postgres-Workshop)**

### 10. Spatial Time-Series: Wind Energy

PostGIS and hypertables on **fixed-location** assets. Builds a portfolio of wind plants across six
real geographic regions, laying each plant's turbines out on a geodesic grid with `ST_Project` and
modelling how they steal each other's wind (front-row machines lose 3% of output, interior ones 9%).
Covers geography vs geometry, `ST_DWithin` versus filtering on `ST_Distance`, point-in-polygon
dimensions denormalised onto fact rows, and a four-tier continuous-aggregate hierarchy. Ships **two
years** of 15-minute telemetry — 3.4 M rows per hypertable at the defaults — with each region given
its own seasonal wind and temperature cycle, so the Deccan monsoon peaks in July while the North Sea
peaks in January. Fleet size and history depth are parameters (`--plants`,
`--turbines-per-plant`, `--days`), so the same workshop runs at 5,000 rows or 13 million. Four
provisioned Grafana dashboards form a global > region > plant > turbine drill-through with cascading
filters; the turbine view selects its own source — raw hypertable, hourly aggregate or daily
aggregate — from the current zoom level, with the untaken branches pruned out of the query plan.
Optional scattered tiering to object storage, with a different threshold per object derived from how
each dashboard reads it. Self-generating data.

**[>> Go to the Wind Energy Workshop](./TimeSeries-Workshop-Wind-Energy)**

## Getting Started

To get started, navigate to the directory of the workshop you are interested in and follow the instructions in its respective `README.md` file.
Expand Down
1,043 changes: 1,043 additions & 0 deletions TIGER_PLATFORM.md

Large diffs are not rendered by default.

453 changes: 453 additions & 0 deletions TimeSeries-Workshop-Wind-Energy/DATA-GENERATION.md

Large diffs are not rendered by default.

1,440 changes: 1,440 additions & 0 deletions TimeSeries-Workshop-Wind-Energy/README.md

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions TimeSeries-Workshop-Wind-Energy/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Grafana only.
#
# The database for this workshop is a Tiger Cloud service, not a container —
# fill in your connection details in the REPO-ROOT .env (copy ../.env.example) and
# both psql and Grafana read the same credentials.
#
# docker compose --env-file ../.env up -d then open http://localhost:3000 (admin / see .env)

services:
grafana:
image: grafana/grafana:latest
# Named per workshop. The workshops are meant to be run ONE AT A TIME, so this
# is not about running them concurrently — it is that a container name is
# global to the daemon and is held even by a STOPPED container. Sharing one
# would make `up` fail with "container name already in use" when you move to
# the next workshop without a `docker compose down` first.
container_name: wind-workshop-grafana
ports:
- "${GRAFANA_PORT:-3000}:3000"
# Grafana expands ${VAR} inside provisioning files, so the datasource is wired
# straight from the root .env — no hand-editing of datasource.yml required.
#
# `env_file:` puts these in the CONTAINER. It does NOT feed the ${...}
# substitutions in this file, such as GRAFANA_PORT above — compose reads
# those from its own --env-file only. Verified: without --env-file the port
# falls back to the default below, which is why the documented command
# passes `--env-file ../.env`. The default means it also works without it.
env_file: ../.env
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GF_SECURITY_ADMIN_PASSWORD:-admin}
# The dashboards ship read-only geomap panels; allow anonymous viewing so
# a workshop room can follow along without each person logging in.
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Viewer
volumes:
# Provisioning: datasource + dashboard provider definitions.
- ./grafana-provisioning:/etc/grafana/provisioning:ro
# The dashboard JSON files. Deliberately NOT under /var/lib/grafana — that
# path is a named volume below, and nesting a bind mount inside a named
# volume is fragile (it can hang container start on Docker Desktop). Keep
# the two mount trees separate; dashboards.yml points here.
- ./grafana:/etc/grafana/dashboards:ro
# Region boundary polygons for the geomap overlay. Grafana's geomap can only
# read GeoJSON from a STATIC FILE under its public directory — there is no
# query-driven polygon layer — so the region boundaries are exported from
# PostGIS once (step 04 has the query) and mounted here.
- ./grafana-geojson:/usr/share/grafana/public/maps/workshop:ro
# Custom marker symbols. Same constraint as the GeoJSON: the geomap resolves
# `symbol` paths relative to Grafana's public directory, so a custom glyph has
# to live inside it. Referenced as `img/icons/workshop/wind-arrow.svg`.
# Not used by the shipped dashboards any more — the wind arrows are route
# layers, which need no asset. Kept because GRAFANA_PLATFORM.md documents
# the rotated-marker alternative, which does need it.
- ./grafana-icons:/usr/share/grafana/public/img/icons/workshop:ro
# Grafana's own state, so preferences survive `docker compose down`. No need to
# name this per workshop: compose prefixes named volumes with the project
# (directory) name, so each workshop gets its own `<project>_grafana-data`.
- grafana-data:/var/lib/grafana
healthcheck:
test: ["CMD-SHELL", "wget -q -O- http://localhost:3000/api/health || exit 1"]
interval: 10s
timeout: 5s
retries: 12
restart: unless-stopped

volumes:
grafana-data:
14 changes: 14 additions & 0 deletions TimeSeries-Workshop-Wind-Energy/grafana-icons/wind-arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
apiVersion: 1

# Auto-loads every .json in grafana/ (mounted at /etc/grafana/dashboards)
# on startup, so all three dashboards exist the moment `docker compose up` finishes.
#
# The path is deliberately outside /var/lib/grafana: that directory is a named
# volume in docker-compose.yml, and nesting a bind mount inside a named volume is
# fragile — it can hang container start on Docker Desktop.

providers:
- name: postgis-workshop
orgId: 1
folder: PostGIS Workshop
type: file
disableDeletion: false
allowUiUpdates: true
updateIntervalSeconds: 30
options:
path: /etc/grafana/dashboards
foldersFromFilesStructure: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
apiVersion: 1

# Points Grafana at your Tiger Cloud service.
#
# The ${VAR} references are expanded by Grafana at provisioning time from the
# environment, which docker-compose.yml supplies via `env_file: .env`. Fill in
# .env once and this file needs no edits.
#
# Two gotchas worth knowing:
# * `url` must be bare host:port. Including a postgres:// scheme produces
# "error parsing postgres url".
# * On first provision the `database` field occasionally does not take effect
# until you open the datasource and click Save & Test once.

datasources:
- name: TigerData
# Grafana 10+ renamed the Postgres plugin from `postgres` to
# `grafana-postgresql-datasource`. Provisioning still ACCEPTS the old alias and
# silently normalises it — but dashboard JSON does not: a template variable
# declaring `type: postgres` fails to resolve its plugin, so its dropdown comes
# up EMPTY, every panel filtering on it gets LIKE '', and the whole dashboard
# reads "No Data" with no error anywhere. Use the real plugin id in both places.
type: grafana-postgresql-datasource
uid: tigerdata
url: ${PGHOST}:${PGPORT}
database: ${PGDATABASE}
user: ${PGUSER}
isDefault: true
editable: true
secureJsonData:
password: ${PGPASSWORD}
jsonData:
sslmode: ${PGSSLMODE}
postgresVersion: 1700
timescaledb: true
maxOpenConns: 5
maxIdleConns: 2
connMaxLifetime: 14400
Loading