Compare commits
No commits in common. "189050d7c5417be3af57f36c5b0b49d57447e7b8" and "e9d51fe79463e9a80dbbbb5257b010bf9f4a4332" have entirely different histories.
189050d7c5
...
e9d51fe794
7 changed files with 122 additions and 261 deletions
|
@ -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
|
||||||
|
|
|
@ -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
66
app.py
|
@ -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("/")
|
||||||
|
|
|
@ -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
|
@ -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>
|
||||||
|
|
269
static/script.js
269
static/script.js
|
@ -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)の推移",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue