inital commit
This commit is contained in:
commit
aed6d1a64c
7
.env.example
Normal file
7
.env.example
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Copy this file to .env for local use. Do not put passwords in this file.
|
||||||
|
# The directory must contain the three secret files documented in README.md.
|
||||||
|
SECRETS_DIR=./.local/secrets
|
||||||
|
|
||||||
|
# Sandbox-only loopback port for direct Grafana access.
|
||||||
|
GRAFANA_PORT=3000
|
||||||
|
|
||||||
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Local environment and generated configuration
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.local/
|
||||||
|
|
||||||
|
# Secrets and authentication material
|
||||||
|
secrets/
|
||||||
|
*.htpasswd
|
||||||
|
*.password
|
||||||
|
*.token
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
*.crt
|
||||||
|
*.cer
|
||||||
|
|
||||||
|
# Runtime and database state
|
||||||
|
data/
|
||||||
|
postgres-data/
|
||||||
|
grafana-data/
|
||||||
|
loki-data/
|
||||||
|
*.dump
|
||||||
|
*.backup
|
||||||
|
*.sql.gz
|
||||||
|
*.tar.gz
|
||||||
|
|
||||||
|
# Logs and unreviewed application samples
|
||||||
|
*.log
|
||||||
|
testing/logs/incoming/**
|
||||||
|
!testing/logs/incoming/.gitkeep
|
||||||
|
|
||||||
|
# Tooling and operating-system files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
98
README.md
Normal file
98
README.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# Kiosk observability proof of concept
|
||||||
|
|
||||||
|
This repository contains the first local milestone for the kiosk reporting
|
||||||
|
platform: Grafana OSS backed by a dedicated PostgreSQL metadata database. Loki,
|
||||||
|
reporting aggregation, Nginx/TLS, tenant bootstrap, and production deployment
|
||||||
|
will be added in later milestones.
|
||||||
|
|
||||||
|
Grafana is available only through a loopback sandbox port. PostgreSQL has no
|
||||||
|
published host port. Nginx will replace the direct Grafana port when HTTPS is
|
||||||
|
introduced.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker Engine with the Docker Compose plugin
|
||||||
|
- PowerShell 7 or Windows PowerShell 5.1 for the local secret helper
|
||||||
|
|
||||||
|
The pinned images are `grafana/grafana:13.2.2` and
|
||||||
|
`postgres:18.6-alpine`. Do not replace them with `latest`.
|
||||||
|
|
||||||
|
## Prepare local secrets
|
||||||
|
|
||||||
|
Copy the non-secret environment template and create three random secret files:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Copy-Item .env.example .env
|
||||||
|
./scripts/initialize-local-secrets.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The helper creates these ignored files without printing their values:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.local/secrets/grafana_admin_password
|
||||||
|
.local/secrets/grafana_database_password
|
||||||
|
.local/secrets/postgres_admin_password
|
||||||
|
```
|
||||||
|
|
||||||
|
It does not overwrite an existing secret. For a deployed Linux environment,
|
||||||
|
create equivalent restricted files below `/etc/kiosk-observability/secrets` and
|
||||||
|
set `SECRETS_DIR` in the host's untracked `.env` file to that directory.
|
||||||
|
|
||||||
|
## Start and verify
|
||||||
|
|
||||||
|
Start the local stack:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f compose.yaml -f compose.sandbox.yaml up -d
|
||||||
|
docker compose -f compose.yaml -f compose.sandbox.yaml ps
|
||||||
|
```
|
||||||
|
|
||||||
|
Open <http://127.0.0.1:3000> and sign in as `admin` with the value stored in
|
||||||
|
`.local/secrets/grafana_admin_password`. The health endpoint is
|
||||||
|
<http://127.0.0.1:3000/api/health>.
|
||||||
|
|
||||||
|
Confirm that PostgreSQL is not published to the host:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f compose.yaml -f compose.sandbox.yaml port postgres 5432
|
||||||
|
```
|
||||||
|
|
||||||
|
The command should report that no public port exists. To confirm Grafana is
|
||||||
|
using PostgreSQL, inspect the health response and container logs; the health
|
||||||
|
response should report `"database": "ok"`.
|
||||||
|
|
||||||
|
## Stop or reset
|
||||||
|
|
||||||
|
Stop containers while preserving their named volumes:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f compose.yaml -f compose.sandbox.yaml down
|
||||||
|
```
|
||||||
|
|
||||||
|
Starting the stack again should retain Grafana metadata. Removing volumes
|
||||||
|
permanently deletes the local databases and must only be done when an intentional
|
||||||
|
clean reset is required:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f compose.yaml -f compose.sandbox.yaml down --volumes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supply test logs
|
||||||
|
|
||||||
|
Put logs awaiting review in `testing/logs/incoming/`. The directory is present
|
||||||
|
in Git, but its contents are ignored. Read `testing/logs/README.md` before adding
|
||||||
|
files. Only fully sanitized, explicitly approved samples may later be placed in
|
||||||
|
`tests/fixtures/`.
|
||||||
|
|
||||||
|
## Validate configuration
|
||||||
|
|
||||||
|
After preparing `.env` and the local secrets, render the merged configuration:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f compose.yaml -f compose.sandbox.yaml config
|
||||||
|
```
|
||||||
|
|
||||||
|
The output must show secret file paths only. It must not contain the contents of
|
||||||
|
any password file. Do not commit `.env`, `.local/`, raw logs, certificates,
|
||||||
|
private keys, database dumps, or runtime data.
|
||||||
|
|
||||||
5
compose.sandbox.yaml
Normal file
5
compose.sandbox.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
services:
|
||||||
|
grafana:
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${GRAFANA_PORT:-3000}:3000"
|
||||||
|
|
||||||
70
compose.yaml
Normal file
70
compose.yaml
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
name: kiosk-observability
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:18.6-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres_admin
|
||||||
|
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_admin_password
|
||||||
|
secrets:
|
||||||
|
- postgres_admin_password
|
||||||
|
- grafana_database_password
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql
|
||||||
|
- ./postgres/init/10-create-grafana-database.sh:/docker-entrypoint-initdb.d/10-create-grafana-database.sh:ro
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres_admin -d postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:13.2.2
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
GF_DATABASE_TYPE: postgres
|
||||||
|
GF_DATABASE_HOST: postgres:5432
|
||||||
|
GF_DATABASE_NAME: grafana
|
||||||
|
GF_DATABASE_USER: grafana
|
||||||
|
GF_DATABASE_PASSWORD__FILE: /run/secrets/grafana_database_password
|
||||||
|
GF_SECURITY_ADMIN_USER: admin
|
||||||
|
GF_SECURITY_ADMIN_PASSWORD__FILE: /run/secrets/grafana_admin_password
|
||||||
|
secrets:
|
||||||
|
- grafana_admin_password
|
||||||
|
- grafana_database_password
|
||||||
|
volumes:
|
||||||
|
- grafana_data:/var/lib/grafana
|
||||||
|
- ./grafana/grafana.ini:/etc/grafana/grafana.ini:ro
|
||||||
|
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 12
|
||||||
|
start_period: 20s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
internal: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
grafana_data:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
grafana_admin_password:
|
||||||
|
file: ${SECRETS_DIR:-./.local/secrets}/grafana_admin_password
|
||||||
|
grafana_database_password:
|
||||||
|
file: ${SECRETS_DIR:-./.local/secrets}/grafana_database_password
|
||||||
|
postgres_admin_password:
|
||||||
|
file: ${SECRETS_DIR:-./.local/secrets}/postgres_admin_password
|
||||||
38
grafana/grafana.ini
Normal file
38
grafana/grafana.ini
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
app_mode = production
|
||||||
|
|
||||||
|
[server]
|
||||||
|
protocol = http
|
||||||
|
http_addr = 0.0.0.0
|
||||||
|
http_port = 3000
|
||||||
|
domain = localhost
|
||||||
|
root_url = http://localhost:3000/
|
||||||
|
enforce_domain = false
|
||||||
|
|
||||||
|
[database]
|
||||||
|
ssl_mode = disable
|
||||||
|
|
||||||
|
[analytics]
|
||||||
|
reporting_enabled = false
|
||||||
|
check_for_updates = false
|
||||||
|
check_for_plugin_updates = false
|
||||||
|
|
||||||
|
[security]
|
||||||
|
disable_gravatar = true
|
||||||
|
cookie_secure = false
|
||||||
|
cookie_samesite = strict
|
||||||
|
strict_transport_security = false
|
||||||
|
|
||||||
|
[users]
|
||||||
|
allow_sign_up = false
|
||||||
|
allow_org_create = false
|
||||||
|
auto_assign_org = false
|
||||||
|
|
||||||
|
[auth.anonymous]
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
[log]
|
||||||
|
mode = console
|
||||||
|
level = info
|
||||||
|
|
||||||
|
[paths]
|
||||||
|
provisioning = /etc/grafana/provisioning
|
||||||
6
grafana/provisioning/README.md
Normal file
6
grafana/provisioning/README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Grafana provisioning
|
||||||
|
|
||||||
|
Version-controlled data-source and dashboard provisioning will be added here as
|
||||||
|
the Loki and reporting services are implemented. The empty subdirectories are
|
||||||
|
intentional and keep the eventual layout stable.
|
||||||
|
|
||||||
1
grafana/provisioning/dashboards/.gitkeep
Normal file
1
grafana/provisioning/dashboards/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
grafana/provisioning/datasources/.gitkeep
Normal file
1
grafana/provisioning/datasources/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
36
postgres/init/10-create-grafana-database.sh
Normal file
36
postgres/init/10-create-grafana-database.sh
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
password_file=/run/secrets/grafana_database_password
|
||||||
|
|
||||||
|
if [ ! -r "$password_file" ]; then
|
||||||
|
echo "Grafana database password file is missing or unreadable" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
grafana_password=$(cat "$password_file")
|
||||||
|
|
||||||
|
if [ -z "$grafana_password" ]; then
|
||||||
|
echo "Grafana database password must not be empty" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
psql \
|
||||||
|
--set=ON_ERROR_STOP=1 \
|
||||||
|
--set=grafana_password="$grafana_password" \
|
||||||
|
--username "$POSTGRES_USER" \
|
||||||
|
--dbname "$POSTGRES_DB" <<-'EOSQL'
|
||||||
|
SELECT format('CREATE ROLE grafana LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT PASSWORD %L', :'grafana_password')
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'grafana') \gexec
|
||||||
|
|
||||||
|
ALTER ROLE grafana PASSWORD :'grafana_password';
|
||||||
|
|
||||||
|
SELECT 'CREATE DATABASE grafana OWNER grafana'
|
||||||
|
WHERE NOT EXISTS (SELECT FROM pg_catalog.pg_database WHERE datname = 'grafana') \gexec
|
||||||
|
|
||||||
|
REVOKE ALL ON DATABASE grafana FROM PUBLIC;
|
||||||
|
GRANT CONNECT, TEMPORARY ON DATABASE grafana TO grafana;
|
||||||
|
EOSQL
|
||||||
|
|
||||||
|
unset grafana_password
|
||||||
|
|
||||||
39
scripts/initialize-local-secrets.ps1
Normal file
39
scripts/initialize-local-secrets.ps1
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Destination = (Join-Path $PSScriptRoot "..\.local\secrets")
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$secretNames = @(
|
||||||
|
"grafana_admin_password",
|
||||||
|
"grafana_database_password",
|
||||||
|
"postgres_admin_password"
|
||||||
|
)
|
||||||
|
|
||||||
|
$resolvedDestination = [System.IO.Path]::GetFullPath($Destination)
|
||||||
|
[System.IO.Directory]::CreateDirectory($resolvedDestination) | Out-Null
|
||||||
|
|
||||||
|
foreach ($secretName in $secretNames) {
|
||||||
|
$secretPath = Join-Path $resolvedDestination $secretName
|
||||||
|
|
||||||
|
if (Test-Path -LiteralPath $secretPath) {
|
||||||
|
Write-Host "Keeping existing secret: $secretName"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes = New-Object byte[] 32
|
||||||
|
$generator = [System.Security.Cryptography.RandomNumberGenerator]::Create()
|
||||||
|
try {
|
||||||
|
$generator.GetBytes($bytes)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$generator.Dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = [Convert]::ToBase64String($bytes)
|
||||||
|
[System.IO.File]::WriteAllText($secretPath, $value, [System.Text.UTF8Encoding]::new($false))
|
||||||
|
Write-Host "Created secret: $secretName"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Local secrets are ready in $resolvedDestination"
|
||||||
|
|
||||||
16
testing/logs/README.md
Normal file
16
testing/logs/README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# Kiosk log intake
|
||||||
|
|
||||||
|
Place log files for initial review in `incoming/`. Everything below that
|
||||||
|
directory is ignored by Git so an unreviewed file cannot be committed by
|
||||||
|
accident.
|
||||||
|
|
||||||
|
Logs may still contain personal or payment-related information. Before a sample
|
||||||
|
is approved as a version-controlled test fixture, replace guest names, booking
|
||||||
|
references, room numbers, session and correlation identifiers, transaction
|
||||||
|
identifiers, payment details, free-text notes, and any other identifying values.
|
||||||
|
Keep the original line structure, delimiters, timestamps, event names, and field
|
||||||
|
layout so that parser development remains representative.
|
||||||
|
|
||||||
|
Reviewed and fully sanitized samples can later be copied deliberately into
|
||||||
|
`tests/fixtures/`. There is no automatic promotion from this inbox.
|
||||||
|
|
||||||
1
testing/logs/incoming/.gitkeep
Normal file
1
testing/logs/incoming/.gitkeep
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
6
tests/fixtures/README.md
vendored
Normal file
6
tests/fixtures/README.md
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# Approved log fixtures
|
||||||
|
|
||||||
|
Only reviewed, synthetic or fully sanitized log samples belong here. Raw files
|
||||||
|
from `testing/logs/incoming/` must never be copied here without an explicit
|
||||||
|
privacy review.
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user