Initial loki skill with promtail auto-discovery

This commit is contained in:
Azat
2026-02-02 22:35:36 +01:00
commit 2aac1371b2
3 changed files with 363 additions and 0 deletions

87
SKILL.md Normal file
View File

@@ -0,0 +1,87 @@
---
name: loki
description: Log aggregation with Loki and Promtail
metadata:
version: "1.0.0"
vibestack:
main: false
metrics-port: 3100
---
# Loki Skill
Log aggregation using [Grafana Loki](https://grafana.com/oss/loki/) with Promtail for log shipping.
## Features
- Lightweight log storage (indexes labels, not content)
- Auto-discovers skill logs from supervisor
- Prometheus-compatible metrics
- LogQL query language
- Auto-registers with Caddy if present
## Architecture
```
supervisor logs ──► promtail ──► loki ──► /loki/api/v1/query
/var/log/supervisor/*.log :3100
```
## Configuration
| Variable | Description | Default |
|----------|-------------|---------|
| `LOKI_PORT` | Loki API port | `3100` |
| `LOKI_DATA_DIR` | Data storage directory | `/data/loki` |
| `LOKI_RETENTION` | Log retention period | `168h` (7 days) |
| `LOKI_DOMAIN` | Domain for Caddy auto-config | (none) |
| `SUPERVISOR_LOG_DIR` | Where to find skill logs | `/var/log/supervisor` |
## Querying Logs
### Via API
```bash
# All logs from last hour
curl -G "http://localhost:3100/loki/api/v1/query_range" \
--data-urlencode 'query={job="skills"}' \
--data-urlencode 'start=1h'
# Logs from specific skill
curl -G "http://localhost:3100/loki/api/v1/query_range" \
--data-urlencode 'query={skill="caddy"}'
# Search for errors
curl -G "http://localhost:3100/loki/api/v1/query_range" \
--data-urlencode 'query={job="skills"} |= "error"'
```
### LogQL Examples
```logql
# All skill logs
{job="skills"}
# Specific skill
{skill="ttyd"}
# Filter by content
{job="skills"} |= "error"
{job="skills"} |~ "error|warn"
# JSON parsing (if logs are JSON)
{job="skills"} | json | level="error"
```
## Integration with Grafana
Add Loki as a data source:
- URL: `http://loki:3100` (or via Caddy proxy)
- Access: Server
## Metrics
Loki exposes Prometheus metrics at `:3100/metrics`:
- `loki_ingester_streams_created_total`
- `loki_distributor_bytes_received_total`
- `loki_querier_query_duration_seconds`

226
scripts/autorun.sh Normal file
View File

@@ -0,0 +1,226 @@
#!/bin/bash
set -e
LOKI_VERSION="${LOKI_VERSION:-3.0.0}"
SKILLS_DIR="${SKILLS_DIR:-/skills}"
LOKI_DATA_DIR="${LOKI_DATA_DIR:-/data/loki}"
SUPERVISOR_LOG_DIR="${SUPERVISOR_LOG_DIR:-/var/log/supervisor}"
SKILL_DIR="$(dirname "$(dirname "$0")")"
# Detect architecture
get_arch() {
case "$(uname -m)" in
x86_64) echo "amd64" ;;
aarch64) echo "arm64" ;;
armv7l) echo "arm" ;;
*)
echo "Unsupported architecture: $(uname -m)" >&2
exit 1
;;
esac
}
# Install Loki
install_loki() {
if command -v loki &>/dev/null; then
echo "loki already installed"
return 0
fi
echo "Installing Loki v${LOKI_VERSION}..."
local arch=$(get_arch)
local url="https://github.com/grafana/loki/releases/download/v${LOKI_VERSION}/loki-linux-${arch}.zip"
apt-get update && apt-get install -y unzip
curl -sSL "$url" -o /tmp/loki.zip
unzip -o /tmp/loki.zip -d /tmp
mv /tmp/loki-linux-${arch} /usr/local/bin/loki
chmod +x /usr/local/bin/loki
rm /tmp/loki.zip
echo "Loki installed"
}
# Install Promtail
install_promtail() {
if command -v promtail &>/dev/null; then
echo "promtail already installed"
return 0
fi
echo "Installing Promtail v${LOKI_VERSION}..."
local arch=$(get_arch)
local url="https://github.com/grafana/loki/releases/download/v${LOKI_VERSION}/promtail-linux-${arch}.zip"
curl -sSL "$url" -o /tmp/promtail.zip
unzip -o /tmp/promtail.zip -d /tmp
mv /tmp/promtail-linux-${arch} /usr/local/bin/promtail
chmod +x /usr/local/bin/promtail
rm /tmp/promtail.zip
echo "Promtail installed"
}
# Setup directories
setup_dirs() {
mkdir -p "$LOKI_DATA_DIR"
mkdir -p "$SKILL_DIR/config"
echo "Data directory: $LOKI_DATA_DIR"
}
# Generate Loki config
generate_loki_config() {
local retention="${LOKI_RETENTION:-168h}"
cat > "$SKILL_DIR/config/loki.yaml" << EOF
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: $LOKI_DATA_DIR
storage:
filesystem:
chunks_directory: $LOKI_DATA_DIR/chunks
rules_directory: $LOKI_DATA_DIR/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: $retention
ruler:
alertmanager_url: http://localhost:9093
analytics:
reporting_enabled: false
EOF
echo "Loki config written to $SKILL_DIR/config/loki.yaml"
}
# Generate Promtail config with auto-discovered log paths
generate_promtail_config() {
cat > "$SKILL_DIR/config/promtail.yaml" << EOF
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://localhost:3100/loki/api/v1/push
scrape_configs:
# Supervisor logs (all skills)
- job_name: skills
static_configs:
- targets:
- localhost
labels:
job: skills
__path__: $SUPERVISOR_LOG_DIR/*.log
pipeline_stages:
# Extract skill name from filename
- match:
selector: '{job="skills"}'
stages:
- regex:
source: filename
expression: '.*/(?P<skill>[^/]+)\\.log$'
- labels:
skill:
# Supervisor error logs
- job_name: skills_errors
static_configs:
- targets:
- localhost
labels:
job: skills
level: error
__path__: $SUPERVISOR_LOG_DIR/*.err
pipeline_stages:
- match:
selector: '{job="skills"}'
stages:
- regex:
source: filename
expression: '.*/(?P<skill>[^/]+)\\.err$'
- labels:
skill:
EOF
echo "Promtail config written to $SKILL_DIR/config/promtail.yaml"
}
# Configure Caddy if present
configure_caddy() {
local caddy_dir="$SKILLS_DIR/caddy"
local loki_port="${LOKI_PORT:-3100}"
local loki_domain="${LOKI_DOMAIN:-}"
if [ ! -d "$caddy_dir" ]; then
echo "Caddy not found - Loki API on port $loki_port"
return 0
fi
echo "Caddy detected - configuring reverse proxy..."
mkdir -p "$caddy_dir/snippets.d"
local snippet="$caddy_dir/snippets.d/loki.caddy"
if [ -n "$loki_domain" ]; then
cat > "$snippet" << EOF
# Auto-generated by loki skill
$loki_domain {
reverse_proxy localhost:$loki_port
}
EOF
echo "Caddy config: $loki_domain -> localhost:$loki_port"
else
cat > "$snippet" << EOF
# Auto-generated by loki skill
# Add to your site block:
# handle /loki/* {
# reverse_proxy localhost:$loki_port
# }
EOF
echo "Caddy snippet created (manual config needed)"
fi
}
install_loki
install_promtail
setup_dirs
generate_loki_config
generate_promtail_config
configure_caddy
echo "Loki setup complete"

50
scripts/run.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
set -e
SKILL_DIR="$(dirname "$(dirname "$0")")"
LOKI_PORT="${LOKI_PORT:-3100}"
# Start Loki in background
start_loki() {
echo "Starting Loki on port $LOKI_PORT..."
loki -config.file="$SKILL_DIR/config/loki.yaml" &
LOKI_PID=$!
sleep 3
if ! kill -0 $LOKI_PID 2>/dev/null; then
echo "Failed to start Loki"
exit 1
fi
echo "Loki running (PID $LOKI_PID)"
}
# Start Promtail in background
start_promtail() {
echo "Starting Promtail..."
promtail -config.file="$SKILL_DIR/config/promtail.yaml" &
PROMTAIL_PID=$!
sleep 2
if ! kill -0 $PROMTAIL_PID 2>/dev/null; then
echo "Failed to start Promtail"
exit 1
fi
echo "Promtail running (PID $PROMTAIL_PID)"
}
# Cleanup on exit
cleanup() {
echo "Shutting down..."
[ -n "$PROMTAIL_PID" ] && kill $PROMTAIL_PID 2>/dev/null
[ -n "$LOKI_PID" ] && kill $LOKI_PID 2>/dev/null
exit 0
}
trap cleanup SIGTERM SIGINT
start_loki
start_promtail
echo "Loki stack running. Query at http://localhost:$LOKI_PORT/loki/api/v1/query"
# Wait for either process to exit
wait