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
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
THQ_WS_AUTH_TOKEN=change-me
THQ_WS_AUTH_REQUIRED=true
THQ_WS_AUTH_REQUIRED=true
GF_ADMIN_PASSWORD=change-me # Grafana admin password – use a strong value in production
20 changes: 20 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,25 @@ services:
timeout: 5s
retries: 5

grafana:
image: grafana/grafana:11.6.0
depends_on:
db:
condition: service_healthy
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: ${GF_ADMIN_PASSWORD:?set in .env or shell}
GF_AUTH_ANONYMOUS_ENABLED: "false"
GRAFANA_PG_USER: thq
GRAFANA_PG_PASSWORD: thq
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
- ./grafana/dashboards:/var/lib/grafana/dashboards
ports:
- "3000:3000"
restart: unless-stopped

volumes:
db-data:
grafana-data:
321 changes: 321 additions & 0 deletions grafana/dashboards/thq-overview.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
{
"annotations": { "list": [] },
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"links": [],
"panels": [
{
"id": 1,
"title": "Location Logs Over Time",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"lineWidth": 2,
"fillOpacity": 20,
"pointSize": 5,
"showPoints": "auto"
}
},
"overrides": []
},
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT\n date_trunc('hour', recorded_at) AS time,\n device,\n COUNT(*) AS count\nFROM location_logs\nWHERE recorded_at >= $__timeFrom() AND recorded_at <= $__timeTo()\nGROUP BY time, device\nORDER BY time",
"format": "time_series",
"refId": "A"
}
]
},
{
"id": 2,
"title": "Log Events Over Time",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"lineWidth": 2,
"fillOpacity": 20,
"pointSize": 5,
"showPoints": "auto"
}
},
"overrides": []
},
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT\n date_trunc('hour', recorded_at) AS time,\n log_level,\n COUNT(*) AS count\nFROM log_events\nWHERE recorded_at >= $__timeFrom() AND recorded_at <= $__timeTo()\nGROUP BY time, log_level\nORDER BY time",
"format": "time_series",
"refId": "A"
}
]
},
{
"id": 3,
"title": "Total Location Logs",
"type": "stat",
"gridPos": { "h": 4, "w": 4, "x": 0, "y": 8 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"fieldConfig": {
"defaults": {
"thresholds": {
"steps": [
{ "color": "blue", "value": null }
]
}
},
"overrides": []
},
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT COUNT(*) AS \"Total\" FROM location_logs",
"format": "table",
"refId": "A"
}
]
},
{
"id": 4,
"title": "Total Log Events",
"type": "stat",
"gridPos": { "h": 4, "w": 4, "x": 4, "y": 8 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"fieldConfig": {
"defaults": {
"thresholds": {
"steps": [
{ "color": "green", "value": null }
]
}
},
"overrides": []
},
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT COUNT(*) AS \"Total\" FROM log_events",
"format": "table",
"refId": "A"
}
]
},
{
"id": 5,
"title": "Active Devices (24h)",
"type": "stat",
"gridPos": { "h": 4, "w": 4, "x": 8, "y": 8 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"fieldConfig": {
"defaults": {
"thresholds": {
"steps": [
{ "color": "orange", "value": null }
]
}
},
"overrides": []
},
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT COUNT(DISTINCT device) AS \"Devices\" FROM location_logs WHERE recorded_at >= NOW() - INTERVAL '24 hours'",
"format": "table",
"refId": "A"
}
]
},
{
"id": 6,
"title": "Average Accuracy by Device",
"type": "bargauge",
"gridPos": { "h": 8, "w": 6, "x": 12, "y": 8 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"fieldConfig": {
"defaults": {
"unit": "suffix: m",
"thresholds": {
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 30 },
{ "color": "red", "value": 100 }
]
}
},
"overrides": []
},
"options": {
"orientation": "horizontal",
"displayMode": "gradient"
},
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT\n device,\n ROUND(AVG(accuracy)::numeric, 1) AS \"Avg Accuracy (m)\"\nFROM location_logs\nWHERE accuracy IS NOT NULL\n AND recorded_at >= $__timeFrom() AND recorded_at <= $__timeTo()\nGROUP BY device\nORDER BY \"Avg Accuracy (m)\" DESC\nLIMIT 20",
"format": "table",
"refId": "A"
}
]
},
{
"id": 7,
"title": "Accuracy Over Time",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"fieldConfig": {
"defaults": {
"unit": "suffix: m",
"color": { "mode": "palette-classic" },
"custom": {
"lineWidth": 1,
"fillOpacity": 10,
"pointSize": 3,
"showPoints": "auto"
}
},
"overrides": []
},
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT\n date_trunc('hour', recorded_at) AS time,\n device,\n ROUND(AVG(accuracy)::numeric, 1) AS accuracy\nFROM location_logs\nWHERE accuracy IS NOT NULL\n AND recorded_at >= $__timeFrom() AND recorded_at <= $__timeTo()\nGROUP BY time, device\nORDER BY time",
"format": "time_series",
"refId": "A"
}
]
},
{
"id": 8,
"title": "Battery Level Over Time",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"color": { "mode": "palette-classic" },
"custom": {
"lineWidth": 2,
"fillOpacity": 15,
"pointSize": 3,
"showPoints": "auto"
}
},
"overrides": []
},
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT\n date_trunc('hour', recorded_at) AS time,\n device,\n ROUND((AVG(battery_level) * 100)::numeric, 1) AS battery\nFROM location_logs\nWHERE battery_level IS NOT NULL\n AND recorded_at >= $__timeFrom() AND recorded_at <= $__timeTo()\nGROUP BY time, device\nORDER BY time",
"format": "time_series",
"refId": "A"
}
]
},
{
"id": 9,
"title": "Speed Over Time",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"fieldConfig": {
"defaults": {
"unit": "velocityms",
"color": { "mode": "palette-classic" },
"custom": {
"lineWidth": 1,
"fillOpacity": 10,
"pointSize": 3,
"showPoints": "auto"
}
},
"overrides": []
},
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT\n date_trunc('hour', recorded_at) AS time,\n device,\n ROUND(AVG(speed)::numeric, 2) AS speed\nFROM location_logs\nWHERE speed IS NOT NULL\n AND recorded_at >= $__timeFrom() AND recorded_at <= $__timeTo()\nGROUP BY time, device\nORDER BY time",
"format": "time_series",
"refId": "A"
}
]
},
{
"id": 10,
"title": "Log Events by Level",
"type": "piechart",
"gridPos": { "h": 8, "w": 6, "x": 18, "y": 8 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"transformations": [
{
"id": "rowsToFields",
"options": {
"mappings": [
{ "fieldName": "log_level", "handlerKey": "field.name" },
{ "fieldName": "count", "handlerKey": "field.value" }
]
}
}
],
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT\n log_level,\n COUNT(*) AS count\nFROM log_events\nWHERE recorded_at >= $__timeFrom() AND recorded_at <= $__timeTo()\nGROUP BY log_level\nORDER BY count DESC",
"format": "table",
"refId": "A"
}
]
},
{
"id": 11,
"title": "Recent Location Logs",
"type": "table",
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 28 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT\n recorded_at AS time,\n device,\n state,\n station_id,\n line_id,\n segment_id,\n ROUND(latitude::numeric, 5) AS lat,\n ROUND(longitude::numeric, 5) AS lon,\n ROUND(accuracy::numeric, 1) AS accuracy,\n ROUND(speed::numeric, 1) AS speed,\n ROUND((battery_level * 100)::numeric, 0) AS \"battery%\"\nFROM location_logs\nWHERE recorded_at >= $__timeFrom() AND recorded_at <= $__timeTo()\nORDER BY recorded_at DESC\nLIMIT 100",
"format": "table",
"refId": "A"
}
]
},
{
"id": 12,
"title": "Recent Log Events",
"type": "table",
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 36 },
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"targets": [
{
"datasource": { "type": "postgres", "uid": "thq-postgres" },
"rawSql": "SELECT\n recorded_at AS time,\n device,\n log_type,\n log_level,\n message\nFROM log_events\nWHERE recorded_at >= $__timeFrom() AND recorded_at <= $__timeTo()\nORDER BY recorded_at DESC\nLIMIT 100",
"format": "table",
"refId": "A"
}
]
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": ["thq", "trainlcd"],
"templating": { "list": [] },
"time": { "from": "now-24h", "to": "now" },
"timepicker": {},
"timezone": "Asia/Tokyo",
"title": "THQ Overview",
"uid": "thq-overview"
}
12 changes: 12 additions & 0 deletions grafana/provisioning/dashboards/default.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: 1

providers:
- name: default
orgId: 1
folder: ""
type: file
disableDeletion: false
editable: true
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: false
16 changes: 16 additions & 0 deletions grafana/provisioning/datasources/postgres.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: 1

datasources:
- name: THQ PostgreSQL
type: postgres
uid: thq-postgres
url: db:5432
database: thq
user: $__env{GRAFANA_PG_USER}
jsonData:
sslmode: disable
postgresVersion: 1800
secureJsonData:
password: $__env{GRAFANA_PG_PASSWORD}
isDefault: true
editable: true
8 changes: 8 additions & 0 deletions src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,14 @@ impl Storage {
.execute(pool)
.await?;

sqlx::query("CREATE INDEX IF NOT EXISTS idx_location_logs_recorded_at ON location_logs (recorded_at, device);")
.execute(pool)
.await?;

sqlx::query("CREATE INDEX IF NOT EXISTS idx_log_events_recorded_at ON log_events (recorded_at, log_level);")
.execute(pool)
.await?;

Ok(())
}

Expand Down