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