feat: initial commit
CI / Build and Push (push) Successful in 6s

This commit is contained in:
2026-01-31 14:28:04 +00:00
parent 8887106656
commit 8b88c2689f
10 changed files with 213 additions and 1532 deletions
+2 -1
View File
@@ -1,2 +1,3 @@
.devcontainer
deployment.yml
deployment.yml
README.md
+3
View File
@@ -0,0 +1,3 @@
LOKI_URL=https://loki.example.com
LOKI_USERNAME=your_username
LOKI_PASSWORD=your_password
+19
View File
@@ -0,0 +1,19 @@
name: CI
on:
push:
branches: [ main ]
jobs:
build-and-push:
name: Build and Push
runs-on: build-htz-01
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build and push Docker image
run: |
docker buildx build --push -t cr.jdbnet.co.uk/public/discord-to-loki:latest .
-33
View File
@@ -1,33 +0,0 @@
name: CI
on:
# push:
# branches: [ main ]
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
runs-on: build-htz-01
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build and push Docker image
run: |
docker build -t cr.jdbnet.co.uk/jdbnet/project:latest .
docker push cr.jdbnet.co.uk/jdbnet/project:latest
deploy:
name: Deploy to Kubernetes
needs: build-and-push
runs-on: k3s-internal-htz-01
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Apply manifests
run: |
sudo kubectl replace -f deployment.yml --grace-period=60 --force
+2 -1
View File
@@ -1 +1,2 @@
__pycache__/
__pycache__/
.env
+37
View File
@@ -0,0 +1,37 @@
# Webhook to Loki Logger
Accepts Discord-style webhook POSTs and forwards the extracted content to a Loki instance (HTTP basic auth). Intended for FiveM scripts that only support Discord webhooks—point them at this service instead and logs go to Loki.
## Environment variables
| Variable | Description |
|----------------|--------------------------------------|
| `LOKI_URL` | Loki base URL (e.g. `https://loki.example.com`) |
| `LOKI_USERNAME`| HTTP basic auth username for Loki |
| `LOKI_PASSWORD`| HTTP basic auth password for Loki |
Copy `.env.example` to `.env` and set these values for local runs.
## Running locally
```bash
pip install -r requirements.txt
cp .env.example .env # edit with your Loki URL and credentials
python app.py
```
The app listens on port 5000 by default.
## Running with Docker
```bash
docker run -p 5000:5000 \
-e LOKI_URL=https://loki.example.com \
-e LOKI_USERNAME=your_username \
-e LOKI_PASSWORD=your_password \
cr.jdbnet.co.uk/public/discord-to-loki:latest
```
## Usage
Send a `POST /` request with a JSON body in Discord webhook format (`content` and/or `embeds`). The service extracts the text and pushes it to Loki. Use this URL as the “webhook” target in your FiveM script instead of a Discord webhook URL.
+126
View File
@@ -0,0 +1,126 @@
"""
Discord webhook to Loki logger.
Accepts Discord-style webhook POSTs and pushes extracted log lines to Loki (HTTP basic auth).
"""
import json
import logging
import time
from dotenv import load_dotenv
from flask import Flask, request
import os
import requests
load_dotenv()
LOKI_URL = os.environ.get("LOKI_URL")
LOKI_USERNAME = os.environ.get("LOKI_USERNAME")
LOKI_PASSWORD = os.environ.get("LOKI_PASSWORD")
for name, value in [("LOKI_URL", LOKI_URL), ("LOKI_USERNAME", LOKI_USERNAME), ("LOKI_PASSWORD", LOKI_PASSWORD)]:
if not value:
raise RuntimeError(f"Missing required environment variable: {name}")
PUSH_URL = f"{LOKI_URL.rstrip('/')}/loki/api/v1/push"
AUTH = (LOKI_USERNAME, LOKI_PASSWORD)
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
app = Flask(__name__)
def discord_payload_to_log_lines(payload: dict) -> list[str]:
"""Extract log lines from a Discord-style webhook payload (content + embeds)."""
lines = []
if not payload:
return lines
content = payload.get("content")
if isinstance(content, str) and content.strip():
lines.append(f"content: {content.strip()}")
for embed in payload.get("embeds") or []:
if not isinstance(embed, dict):
continue
title = embed.get("title")
if isinstance(title, str) and title.strip():
lines.append(f"embed: {title.strip()}")
description = embed.get("description")
if isinstance(description, str) and description.strip():
lines.append(f"embed: {description.strip()}")
for field in embed.get("fields") or []:
if not isinstance(field, dict):
continue
name = field.get("name") or ""
value = field.get("value") or ""
if isinstance(name, str) and isinstance(value, str):
lines.append(f"field: {name.strip()} | {value.strip()}")
return lines
def push_to_loki(log_lines: list[str]) -> None:
"""Send log lines to Loki via the push API (HTTP basic auth)."""
if not log_lines:
return
now_ns = str(int(time.time() * 1_000_000_000))
values = [[now_ns, line] for line in log_lines]
body = {
"streams": [
{
"stream": {"source": "webhook", "job": "webhook-to-loki"},
"values": values,
}
]
}
resp = requests.post(
PUSH_URL,
json=body,
auth=AUTH,
headers={"Content-Type": "application/json"},
timeout=30,
)
resp.raise_for_status()
@app.route("/health")
def health():
"""Health check for k8s/Docker (no Loki call)."""
return "", 200
@app.route("/ready")
def ready():
"""Readiness check for k8s/Docker (no Loki call)."""
return "", 200
@app.route("/", methods=["POST"])
def webhook():
"""Accept Discord-style webhook POST; extract content/embeds and push to Loki."""
if request.content_type and "application/json" not in request.content_type:
return "", 400
try:
payload = request.get_json(force=True, silent=False)
except (json.JSONDecodeError, TypeError):
return "", 400
if payload is None:
return "", 400
log_lines = discord_payload_to_log_lines(payload)
if not log_lines:
return "", 400
try:
push_to_loki(log_lines)
except requests.RequestException as e:
logger.exception("Loki push failed: %s", e)
return "", 502
return "", 204
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
+21 -14
View File
@@ -1,34 +1,41 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: project
namespace: project
name: discord-to-loki
namespace: discord-to-loki
spec:
replicas: 1
selector:
matchLabels:
app: project
app: discord-to-loki
template:
metadata:
labels:
app: project
app: discord-to-loki
spec:
containers:
- name: project
image: ghcr.io/jdb-net/project:latest
- name: discord-to-loki
image: cr.jdbnet.co.uk/public/discord-to-loki:latest
imagePullPolicy: Always
ports:
- containerPort: 5000
name: "project"
name: "discord-to-loki"
env:
- name: LOKI_URL
value: "https://loki.example.com"
- name: LOKI_USERNAME
value: "your_username"
- name: LOKI_PASSWORD
value: "your_password"
---
apiVersion: v1
kind: Service
metadata:
name: project-ingress-service
namespace: project
name: discord-to-loki-ingress-service
namespace: discord-to-loki
spec:
selector:
app: project
app: discord-to-loki
ports:
- protocol: TCP
port: 80
@@ -37,17 +44,17 @@ spec:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: project-ingress
namespace: project
name: discord-to-loki-ingress
namespace: discord-to-loki
spec:
rules:
- host: project.jdbnet.co.uk
- host: discord-to-loki.jdbnet.co.uk
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: project-ingress-service
name: discord-to-loki-ingress-service
port:
number: 80
+3 -1
View File
@@ -1,2 +1,4 @@
Flask
gunicorn
gunicorn
python-dotenv
requests
File diff suppressed because it is too large Load Diff