Compare commits

...

10 commits

Author SHA1 Message Date
yuuki
189050d7c5 fastapiの更新 2024-11-15 17:40:20 +09:00
yuuki
730b984a27 チャートの作成を簡素化 2024-11-11 13:26:46 +09:00
yuuki
1c5a1b2c64 update chart.js 2024-11-11 13:20:56 +09:00
yuuki
817975e3e6 fastapiを更新 2024-10-31 14:37:49 +09:00
yuuki
55df834734 chart.js 4.4.3->4.4.5 2024-10-22 19:57:48 +09:00
yuuki
d4a373b29a pprint 2024-10-22 19:32:42 +09:00
yuuki
b5ac53f0e6 コンテキストの読み込み最適化 2024-10-22 18:01:25 +09:00
yuuki
75a95ae07d Python 3.13 対応 2024-10-16 04:02:26 +00:00
yuuki
06550e28e3 エラーハンドリング 2024-06-27 01:41:35 +09:00
yuuki
1f8259f04d 1時間ごとの統計を追加 2024-06-22 19:33:50 +09:00
7 changed files with 261 additions and 122 deletions

View file

@ -1,4 +1,4 @@
FROM python:3 FROM python:3.13.0
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN pip install -r requirements.txt RUN pip install -r requirements.txt

27
analytics_hourly.txt Normal file
View file

@ -0,0 +1,27 @@
{
viewer {
zones(filter: {
zoneTag: $zoneTag
}) {
httpRequests1hGroups(
orderBy: [datetime_ASC],
limit: $limit,
filter: {
datetime_gt: $from,
datetime_leq: $to
}
) {
dimensions {
datetime
}
sum {
bytes
cachedBytes
}
uniq {
uniques
}
}
}
}
}

62
app.py
View file

@ -3,10 +3,12 @@ from pathlib import Path
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from urllib.parse import urlparse from urllib.parse import urlparse
from json import dumps from json import dumps
from pprint import pprint
from requests import post from requests import post
from fastapi import FastAPI, Response, Header, status from fastapi import FastAPI, Response, Header, status
from fastapi.responses import JSONResponse, PlainTextResponse, FileResponse from fastapi.responses import JSONResponse, PlainTextResponse, FileResponse
from contextlib import asynccontextmanager
def fastapi_serve(dir: str, ref: str, indexes: List[str] = ["index.html", "index.htm"]) -> Response: def fastapi_serve(dir: str, ref: str, indexes: List[str] = ["index.html", "index.htm"]) -> Response:
url_path = urlparse(ref or "/").path url_path = urlparse(ref or "/").path
@ -32,12 +34,20 @@ def fastapi_serve(dir: str, ref: str, indexes: List[str] = ["index.html", "index
return FileResponse(path) return FileResponse(path)
app = FastAPI() ctx = {}
@app.get("/cloudflare") @asynccontextmanager
async def lifespan(app: FastAPI):
ctx["daily"] = Path("analytics_daily.txt").read_text("UTF-8")
ctx["hourly"] = Path("analytics_hourly.txt").read_text("UTF-8")
pprint(ctx)
yield
ctx.clear()
app = FastAPI(lifespan=lifespan)
@app.get("/api/cloudflare")
async def cloudflare(zone_id: str, x_token: Union[str, None] = Header()): async def cloudflare(zone_id: str, x_token: Union[str, None] = Header()):
query = Path("analytics_daily.txt").read_text("UTF-8")
now = datetime.now() now = datetime.now()
before = now - timedelta(**{ "days": 30 }) before = now - timedelta(**{ "days": 30 })
@ -54,14 +64,54 @@ async def cloudflare(zone_id: str, x_token: Union[str, None] = Header()):
"Authorization": f"Bearer {x_token}" "Authorization": f"Bearer {x_token}"
}, },
data=dumps({ data=dumps({
"query": query, "query": ctx["daily"],
"variables": variables "variables": variables
}) })
) )
res = JSONResponse(result.json()) json = result.json()
res = JSONResponse(json)
if "data" in json and not json["errors"]:
res.headers["Cache-Control"] = f"public, max-age=60, s-maxage=60" res.headers["Cache-Control"] = f"public, max-age=60, s-maxage=60"
res.headers["CDN-Cache-Control"] = f"max-age=60" res.headers["CDN-Cache-Control"] = f"max-age=60"
else:
res.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
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("/api/cloudflare2")
async def cloudflare2(zone_id: str, x_token: Union[str, None] = Header()):
now = datetime.now()
before = now - timedelta(**{ "hours": 72 })
variables = {
"zoneTag": zone_id,
"from": before.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"to": now.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"limit": 72
}
result = post(
url="https://api.cloudflare.com/client/v4/graphql",
headers={
"Authorization": f"Bearer {x_token}"
},
data=dumps({
"query": ctx["hourly"],
"variables": variables
})
)
json = result.json()
res = JSONResponse(json)
if "data" in json and not json["errors"]:
res.headers["Cache-Control"] = f"public, max-age=60, s-maxage=60"
res.headers["CDN-Cache-Control"] = f"max-age=60"
else:
res.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
res.headers["Cache-Control"] = f"public, max-age=0, s-maxage=0"
res.headers["CDN-Cache-Control"] = f"max-age=0"
return res return res
@app.get("/") @app.get("/")

View file

@ -1,2 +1,2 @@
requests==2.32.2 requests==2.32.3
fastapi==0.111.0 fastapi[standard]==0.115.5

View file

@ -5,10 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>クラウドフレアの統計情報</title> <title>クラウドフレアの統計情報</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<script src="chart-js-4-4-3.js"></script> <script src="lib/chart.js"></script>
<script src="script.js"></script> <script src="script.js"></script>
</head> </head>
<body> <body>
<div id="loading-2">読み込み中…</div>
<canvas id="users-2" width="10" height="10"></canvas>
<canvas id="bytes-2" width="10" height="10"></canvas>
<canvas id="bytes2-2" width="10" height="10"></canvas>
<hr>
<div id="loading">読み込み中…</div> <div id="loading">読み込み中…</div>
<canvas id="users" width="10" height="10"></canvas> <canvas id="users" width="10" height="10"></canvas>
<canvas id="bytes" width="10" height="10"></canvas> <canvas id="bytes" width="10" height="10"></canvas>

File diff suppressed because one or more lines are too long

View file

@ -28,7 +28,7 @@ const websites = [
]; ];
async function worker(item) { async function worker(item) {
const res = await fetch(`/cloudflare?zone_id=${encodeURIComponent(item.zoneId)}`, { const res = await fetch(`/api/cloudflare?zone_id=${encodeURIComponent(item.zoneId)}`, {
headers: { headers: {
"X-Token": token, "X-Token": token,
}, },
@ -42,6 +42,48 @@ async function worker(item) {
}; };
} }
async function worker2(item) {
const res = await fetch(`/api/cloudflare2?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,
};
}
function createChart(ctx, labels, datasets, title) {
new Chart(ctx, {
data: {
labels,
datasets,
},
options: {
plugins: {
title: {
display: true,
text: title,
},
legend: {
labels: {
usePointStyle: true,
},
},
},
scales: {
y: {
beginAtZero: true,
},
},
},
});
}
addEventListener("load", () => { addEventListener("load", () => {
Promise.all(websites.map((i) => worker(i))) Promise.all(websites.map((i) => worker(i)))
.then((results) => { .then((results) => {
@ -53,10 +95,10 @@ addEventListener("load", () => {
const ctxUsers = document.querySelector("#users").getContext("2d"); const ctxUsers = document.querySelector("#users").getContext("2d");
new Chart(ctxUsers, { createChart(
data: { ctxUsers,
labels: range.map((d) => d.toISOString().slice(0, 10)).reverse(), range.map((d) => d.toISOString().slice(0, 10)).reverse(),
datasets: results.map((r) => { results.map((r) => {
return { return {
type: "line", type: "line",
label: r.domain, label: r.domain,
@ -68,33 +110,15 @@ addEventListener("load", () => {
pointStyle: "star", pointStyle: "star",
}; };
}), }),
}, "過去30日のユーザー数の推移",
options: { );
plugins: {
title: {
display: true,
text: "過去30日のユーザー数の推移",
},
legend: {
labels: {
usePointStyle: true,
},
},
},
scales: {
y: {
beginAtZero: true,
},
},
},
});
const ctxBytes = document.querySelector("#bytes").getContext("2d"); const ctxBytes = document.querySelector("#bytes").getContext("2d");
new Chart(ctxBytes, { createChart(
data: { ctxBytes,
labels: range.map((d) => d.toISOString().slice(0, 10)).reverse(), range.map((d) => d.toISOString().slice(0, 10)).reverse(),
datasets: results.map((r) => { results.map((r) => {
return { return {
type: "line", type: "line",
label: r.domain, label: r.domain,
@ -106,33 +130,15 @@ addEventListener("load", () => {
pointStyle: "star", pointStyle: "star",
}; };
}), }),
}, "過去30日の送受信データ量(GB)の推移",
options: { );
plugins: {
title: {
display: true,
text: "過去30日の送受信データ量(GB)の推移",
},
legend: {
labels: {
usePointStyle: true,
},
},
},
scales: {
y: {
beginAtZero: true,
},
},
},
});
const ctxBytes2 = document.querySelector("#bytes2").getContext("2d"); const ctxBytes2 = document.querySelector("#bytes2").getContext("2d");
new Chart(ctxBytes2, { createChart(
data: { ctxBytes2,
labels: range.map((d) => d.toISOString().slice(0, 10)).reverse(), range.map((d) => d.toISOString().slice(0, 10)).reverse(),
datasets: results.map((r) => { results.map((r) => {
return { return {
type: "line", type: "line",
label: r.domain, label: r.domain,
@ -144,25 +150,76 @@ addEventListener("load", () => {
pointStyle: "star", pointStyle: "star",
}; };
}), }),
}, "過去30日のキャッシュ済み送受信データ量(GB)の推移",
options: { );
plugins: {
title: {
display: true,
text: "過去30日のキャッシュ済み送受信データ量(GB)の推移",
},
legend: {
labels: {
usePointStyle: true,
},
},
},
scales: {
y: {
beginAtZero: true,
},
},
},
}); });
Promise.all(websites.map((i) => worker2(i)))
.then((results) => {
document.querySelector("#loading-2").remove();
const scale = Math.max(...results.map((r) => r.json["data"]["viewer"]["zones"][0]["httpRequests1hGroups"].length));
const endedAt = Math.max(...results.map((r) => r.json["data"]["viewer"]["zones"][0]["httpRequests1hGroups"].map((g) => new Date(g["dimensions"]["datetime"]).getTime())).flat());
const range = Array.from({ length: scale }, (_, k) => new Date(endedAt - k * 60 * 60 * 1000));
const ctxUsers = document.querySelector("#users-2").getContext("2d");
createChart(
ctxUsers,
range.map((d) => d.toLocaleString()).reverse(),
results.map((r) => {
return {
type: "line",
label: r.domain,
data: range.map((d) => r.json["data"]["viewer"]["zones"][0]["httpRequests1hGroups"].find((g) => g["dimensions"]["datetime"] === d.toISOString().slice(0, 19) + "Z")?.["uniq"]?.["uniques"] ?? null).reverse(),
borderColor: r.fg,
borderWidth: 1,
fill: "origin",
backgroundColor: r.bg,
pointStyle: "star",
};
}),
"過去72時間のユーザー数の推移",
)
const ctxBytes = document.querySelector("#bytes-2").getContext("2d");
createChart(
ctxBytes,
range.map((d) => d.toLocaleString()).reverse(),
results.map((r) => {
return {
type: "line",
label: r.domain,
data: range.map((d) => r.json["data"]["viewer"]["zones"][0]["httpRequests1hGroups"].find((g) => g["dimensions"]["datetime"] === d.toISOString().slice(0, 19) + "Z")?.["sum"]?.["bytes"] / 1000 ** 3 ?? null).reverse(),
borderColor: r.fg,
borderWidth: 1,
fill: "origin",
backgroundColor: r.bg,
pointStyle: "star",
};
}),
"過去72時間の送受信データ量(GB)の推移",
)
const ctxBytes2 = document.querySelector("#bytes2-2").getContext("2d");
createChart(
ctxBytes2,
range.map((d) => d.toLocaleString()).reverse(),
results.map((r) => {
return {
type: "line",
label: r.domain,
data: range.map((d) => r.json["data"]["viewer"]["zones"][0]["httpRequests1hGroups"].find((g) => g["dimensions"]["datetime"] === d.toISOString().slice(0, 19) + "Z")?.["sum"]?.["cachedBytes"] / 1000 ** 3 ?? null).reverse(),
borderColor: r.fg,
borderWidth: 1,
fill: "origin",
backgroundColor: r.bg,
pointStyle: "star",
};
}),
"過去72時間のキャッシュ済み送受信データ量(GB)の推移",
);
}); });
}); });