commit
This commit is contained in:
commit
31b68eb474
20 changed files with 336 additions and 0 deletions
6
Dockerfile
Normal file
6
Dockerfile
Normal file
|
@ -0,0 +1,6 @@
|
|||
FROM python:3
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
RUN pip install -r requirements.txt
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
CMD ["fastapi", "run", "--workers", "5"]
|
27
analytics_daily.txt
Normal file
27
analytics_daily.txt
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
viewer {
|
||||
zones(filter: {
|
||||
zoneTag: $zoneTag
|
||||
}) {
|
||||
httpRequests1dGroups(
|
||||
orderBy: [date_ASC],
|
||||
limit: $limit,
|
||||
filter: {
|
||||
date_gt: $from,
|
||||
date_leq: $to
|
||||
}
|
||||
) {
|
||||
dimensions {
|
||||
date
|
||||
}
|
||||
sum {
|
||||
bytes
|
||||
cachedBytes
|
||||
}
|
||||
uniq {
|
||||
uniques
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
73
app.py
Normal file
73
app.py
Normal file
|
@ -0,0 +1,73 @@
|
|||
from typing import List, Union
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from urllib.parse import urlparse
|
||||
from json import dumps
|
||||
|
||||
from requests import post
|
||||
from fastapi import FastAPI, Response, Header, status
|
||||
from fastapi.responses import JSONResponse, PlainTextResponse, FileResponse
|
||||
|
||||
def fastapi_serve(dir: str, ref: str, indexes: List[str] = ["index.html", "index.htm"]) -> Response:
|
||||
url_path = urlparse(ref or "/").path
|
||||
root = Path(dir)
|
||||
|
||||
try_files = []
|
||||
|
||||
if url_path.endswith("/"):
|
||||
try_files += [root / url_path.lstrip("/") / i for i in indexes]
|
||||
|
||||
try_files += [root / url_path]
|
||||
|
||||
try_files_tried = [t for t in try_files if t.is_file()]
|
||||
|
||||
print(try_files, try_files_tried)
|
||||
|
||||
if not try_files_tried:
|
||||
return PlainTextResponse("指定されたファイルが見つかりません", status.HTTP_404_NOT_FOUND)
|
||||
|
||||
path = try_files_tried[0]
|
||||
|
||||
print(path, "をサーブ中")
|
||||
|
||||
return FileResponse(path)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/cloudflare")
|
||||
async def cloudflare(zone_id: str, x_token: Union[str, None] = Header()):
|
||||
query = Path("analytics_daily.txt").read_text("UTF-8")
|
||||
|
||||
now = datetime.now()
|
||||
before = now - timedelta(**{ "days": 30 })
|
||||
|
||||
variables = {
|
||||
"zoneTag": zone_id,
|
||||
"from": before.astimezone(timezone.utc).strftime("%Y-%m-%d"),
|
||||
"to": now.astimezone(timezone.utc).strftime("%Y-%m-%d"),
|
||||
"limit": 30
|
||||
}
|
||||
|
||||
result = post(
|
||||
url="https://api.cloudflare.com/client/v4/graphql",
|
||||
headers={
|
||||
"Authorization": f"Bearer {x_token}"
|
||||
},
|
||||
data=dumps({
|
||||
"query": query,
|
||||
"variables": variables
|
||||
})
|
||||
)
|
||||
|
||||
res = JSONResponse(result.json())
|
||||
res.headers["Cache-Control"] = f"public, max-age=0, s-maxage=0"
|
||||
res.headers["CDN-Cache-Control"] = f"max-age=0"
|
||||
return res
|
||||
|
||||
@app.get("/")
|
||||
@app.get("/{ref:path}")
|
||||
async def home(ref: str = None):
|
||||
res = fastapi_serve("static", ref)
|
||||
res.headers["Cache-Control"] = "public, max-age=3600, s-maxage=3600"
|
||||
res.headers["CDN-Cache-Control"] = "max-age=3600"
|
||||
return res
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
requests==2.32.2
|
||||
fastapi==0.111.0
|
20
static/chart-js-4-4-3.js
Normal file
20
static/chart-js-4-4-3.js
Normal file
File diff suppressed because one or more lines are too long
17
static/index.html
Normal file
17
static/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ja">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>クラウドフレアの統計情報</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="chart-js-4-4-3.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="loading">読み込み中…</div>
|
||||
<canvas id="users" width="10" height="10"></canvas>
|
||||
<canvas id="bytes" width="10" height="10"></canvas>
|
||||
<canvas id="bytes2" width="10" height="10"></canvas>
|
||||
</body>
|
||||
</html>
|
BIN
static/roboto/Roboto-Black.ttf
Normal file
BIN
static/roboto/Roboto-Black.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-BlackItalic.ttf
Normal file
BIN
static/roboto/Roboto-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-Bold.ttf
Normal file
BIN
static/roboto/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-BoldItalic.ttf
Normal file
BIN
static/roboto/Roboto-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-Italic.ttf
Normal file
BIN
static/roboto/Roboto-Italic.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-Light.ttf
Normal file
BIN
static/roboto/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-LightItalic.ttf
Normal file
BIN
static/roboto/Roboto-LightItalic.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-Medium.ttf
Normal file
BIN
static/roboto/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-MediumItalic.ttf
Normal file
BIN
static/roboto/Roboto-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-Regular.ttf
Normal file
BIN
static/roboto/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-Thin.ttf
Normal file
BIN
static/roboto/Roboto-Thin.ttf
Normal file
Binary file not shown.
BIN
static/roboto/Roboto-ThinItalic.ttf
Normal file
BIN
static/roboto/Roboto-ThinItalic.ttf
Normal file
Binary file not shown.
168
static/script.js
Normal file
168
static/script.js
Normal file
|
@ -0,0 +1,168 @@
|
|||
const token = "FFCH7nhjGM6ey6VF8wtPkFJQ_Vn65DvozpIStEER";
|
||||
const websites = [
|
||||
{
|
||||
zoneId: "3ace6ae0587033b37c79e168cf60c234",
|
||||
domain: "yuuk1.tk",
|
||||
color: [255, 182, 193],
|
||||
},
|
||||
{
|
||||
zoneId: "068e85c0bc67ef053660c7d2ceca7b89",
|
||||
domain: "yuuk1.uk",
|
||||
color: [173, 216, 230],
|
||||
},
|
||||
{
|
||||
zoneId: "176677a44c89b3aa8ab0a33f2d7108c3",
|
||||
domain: "taikoapp.uk",
|
||||
color: [152, 255, 152],
|
||||
},
|
||||
{
|
||||
zoneId: "9d4b398a23e094448b287b84947f58ff",
|
||||
domain: "forgejo.win",
|
||||
color: [255, 160, 122],
|
||||
},
|
||||
{
|
||||
zoneId: "09990364a38b739e3de9338f908d584f",
|
||||
domain: "litey.trade",
|
||||
color: [255, 215, 0],
|
||||
},
|
||||
];
|
||||
|
||||
async function worker(item) {
|
||||
const res = await fetch(`/cloudflare?zone_id=${encodeURIComponent(item.zoneId)}`, {
|
||||
headers: {
|
||||
"X-Token": token,
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
return {
|
||||
domain: item.domain,
|
||||
fg: `rgb(${item.color[0]}, ${item.color[1]}, ${item.color[2]})`,
|
||||
bg: `rgba(${item.color[0]}, ${item.color[1]}, ${item.color[2]}, 0.2)`,
|
||||
json,
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener("load", () => {
|
||||
Promise.all(websites.map((i) => worker(i)))
|
||||
.then((results) => {
|
||||
document.querySelector("#loading").remove();
|
||||
|
||||
const scale = Math.max(...results.map((r) => r.json["data"]["viewer"]["zones"][0]["httpRequests1dGroups"].length));
|
||||
const endedAt = Math.max(...results.map((r) => r.json["data"]["viewer"]["zones"][0]["httpRequests1dGroups"].map((g) => new Date(g["dimensions"]["date"]).getTime())).flat());
|
||||
const range = Array.from({ length: scale }, (_, k) => new Date(endedAt - k * 24 * 60 * 60 * 1000));
|
||||
|
||||
const ctxUsers = document.querySelector("#users").getContext("2d");
|
||||
|
||||
new Chart(ctxUsers, {
|
||||
data: {
|
||||
labels: range.map((d) => d.toISOString().slice(0, 10)).reverse(),
|
||||
datasets: results.map((r) => {
|
||||
return {
|
||||
type: "line",
|
||||
label: r.domain,
|
||||
data: range.map((d) => r.json["data"]["viewer"]["zones"][0]["httpRequests1dGroups"].find((g) => g["dimensions"]["date"] === d.toISOString().slice(0, 10))?.["uniq"]?.["uniques"] ?? null).reverse(),
|
||||
borderColor: r.fg,
|
||||
borderWidth: 1,
|
||||
fill: "origin",
|
||||
backgroundColor: r.bg,
|
||||
pointStyle: "star",
|
||||
};
|
||||
}),
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: "過去30日のユーザー数の推移",
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ctxBytes = document.querySelector("#bytes").getContext("2d");
|
||||
|
||||
new Chart(ctxBytes, {
|
||||
data: {
|
||||
labels: range.map((d) => d.toISOString().slice(0, 10)).reverse(),
|
||||
datasets: results.map((r) => {
|
||||
return {
|
||||
type: "line",
|
||||
label: r.domain,
|
||||
data: range.map((d) => r.json["data"]["viewer"]["zones"][0]["httpRequests1dGroups"].find((g) => g["dimensions"]["date"] === d.toISOString().slice(0, 10))?.["sum"]?.["bytes"] / 1000 ** 3 ?? null).reverse(),
|
||||
borderColor: r.fg,
|
||||
borderWidth: 1,
|
||||
fill: "origin",
|
||||
backgroundColor: r.bg,
|
||||
pointStyle: "star",
|
||||
};
|
||||
}),
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: "過去30日の送受信データ量(GB)の推移",
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const ctxBytes2 = document.querySelector("#bytes2").getContext("2d");
|
||||
|
||||
new Chart(ctxBytes2, {
|
||||
data: {
|
||||
labels: range.map((d) => d.toISOString().slice(0, 10)).reverse(),
|
||||
datasets: results.map((r) => {
|
||||
return {
|
||||
type: "line",
|
||||
label: r.domain,
|
||||
data: range.map((d) => r.json["data"]["viewer"]["zones"][0]["httpRequests1dGroups"].find((g) => g["dimensions"]["date"] === d.toISOString().slice(0, 10))?.["sum"]?.["cachedBytes"] / 1000 ** 3 ?? null).reverse(),
|
||||
borderColor: r.fg,
|
||||
borderWidth: 1,
|
||||
fill: "origin",
|
||||
backgroundColor: r.bg,
|
||||
pointStyle: "star",
|
||||
};
|
||||
}),
|
||||
},
|
||||
options: {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: "過去30日のキャッシュ済み送受信データ量(GB)の推移",
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
usePointStyle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
23
static/style.css
Normal file
23
static/style.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
@font-face {
|
||||
font-family: "Roboto";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(roboto/Roboto-Regular.ttf) format("truetype");
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
body * {
|
||||
margin: 0;
|
||||
font-family: "Roboto", sans-serif;
|
||||
word-break: break-all;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
body > :not(:last-child) {
|
||||
margin-bottom: 2rem;
|
||||
}
|
Loading…
Add table
Reference in a new issue