Compare commits

..

No commits in common. "189050d7c5417be3af57f36c5b0b49d57447e7b8" and "e9d51fe79463e9a80dbbbb5257b010bf9f4a4332" have entirely different histories.

7 changed files with 122 additions and 261 deletions

View file

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

View file

@ -1,27 +0,0 @@
{
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
}
}
}
}
}

66
app.py
View file

@ -3,12 +3,10 @@ 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
@ -34,20 +32,12 @@ def fastapi_serve(dir: str, ref: str, indexes: List[str] = ["index.html", "index
return FileResponse(path) return FileResponse(path)
ctx = {} app = FastAPI()
@asynccontextmanager @app.get("/cloudflare")
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 })
@ -64,54 +54,14 @@ 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": ctx["daily"], "query": query,
"variables": variables "variables": variables
}) })
) )
json = result.json() res = JSONResponse(result.json())
res = JSONResponse(json) res.headers["Cache-Control"] = f"public, max-age=60, s-maxage=60"
if "data" in json and not json["errors"]: res.headers["CDN-Cache-Control"] = f"max-age=60"
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
@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.3 requests==2.32.2
fastapi[standard]==0.115.5 fastapi==0.111.0

File diff suppressed because one or more lines are too long

View file

@ -5,15 +5,10 @@
<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="lib/chart.js"></script> <script src="chart-js-4-4-3.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>

View file

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