- Add queryTimed() wrapper and logPerf() for per-endpoint timing metrics - Add api_perf_log table flush mechanism with background thread batching - Add application-scope cache (appCacheGet/Put/Invalidate) with TTL - Cache businesses/get (5m), addresses/states (24h), menu/items (2m) - Fix N+1 queries in orders/history, orders/listForKDS (batch fetch) - Fix correlated subquery in orders/getDetail (LEFT JOIN) - Combine 4 queries into 1 in portal/stats (subselects) - Optimize getForBuilder tree building with pre-indexed parent lookup - Add cache invalidation in update, saveBrandColor, updateHours, saveFromBuilder - New admin/perf.cfm dashboard (localhost-protected) - Instrument top 10 endpoints with queryTimed + logPerf Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
179 lines
5.3 KiB
Text
179 lines
5.3 KiB
Text
<cfsetting showdebugoutput="false">
|
|
<cfsetting enablecfoutputonly="true">
|
|
<cfcontent type="application/json; charset=utf-8" reset="true">
|
|
<cfheader name="Cache-Control" value="no-store">
|
|
|
|
<cfscript>
|
|
function apiAbort(payload) {
|
|
writeOutput(serializeJSON(payload));
|
|
abort;
|
|
}
|
|
|
|
// Localhost-only protection
|
|
remoteAddr = cgi.REMOTE_ADDR;
|
|
if (remoteAddr != "127.0.0.1" && remoteAddr != "::1" && remoteAddr != "0:0:0:0:0:0:0:1" && remoteAddr != "10.10.0.2") {
|
|
apiAbort({ "OK": false, "ERROR": "forbidden" });
|
|
}
|
|
|
|
try {
|
|
// Parse parameters
|
|
view = structKeyExists(url, "view") ? lcase(url.view) : "count";
|
|
hours = structKeyExists(url, "hours") ? val(url.hours) : 24;
|
|
if (hours <= 0 || hours > 720) hours = 24;
|
|
limitRows = structKeyExists(url, "limit") ? val(url.limit) : 20;
|
|
if (limitRows <= 0 || limitRows > 100) limitRows = 20;
|
|
|
|
// Flush any buffered metrics first
|
|
flushPerfBuffer();
|
|
|
|
response = { "OK": true, "VIEW": view, "HOURS": hours };
|
|
|
|
if (view == "count") {
|
|
// Top endpoints by call count
|
|
q = queryExecute("
|
|
SELECT
|
|
Endpoint,
|
|
COUNT(*) as Calls,
|
|
ROUND(AVG(TotalMs)) as AvgMs,
|
|
ROUND(AVG(DbMs)) as AvgDbMs,
|
|
ROUND(AVG(AppMs)) as AvgAppMs,
|
|
MAX(TotalMs) as MaxMs,
|
|
ROUND(AVG(QueryCount), 1) as AvgQueries,
|
|
ROUND(AVG(ResponseBytes)) as AvgBytes
|
|
FROM api_perf_log
|
|
WHERE LoggedAt > DATE_SUB(NOW(), INTERVAL :hours HOUR)
|
|
GROUP BY Endpoint
|
|
ORDER BY Calls DESC
|
|
LIMIT :lim
|
|
", {
|
|
hours: { value: hours, cfsqltype: "cf_sql_integer" },
|
|
lim: { value: limitRows, cfsqltype: "cf_sql_integer" }
|
|
}, { datasource: "payfrit" });
|
|
|
|
rows = [];
|
|
for (row in q) {
|
|
arrayAppend(rows, {
|
|
"Endpoint": row.Endpoint,
|
|
"Calls": row.Calls,
|
|
"AvgMs": row.AvgMs,
|
|
"AvgDbMs": row.AvgDbMs,
|
|
"AvgAppMs": row.AvgAppMs,
|
|
"MaxMs": row.MaxMs,
|
|
"AvgQueries": row.AvgQueries,
|
|
"AvgBytes": row.AvgBytes
|
|
});
|
|
}
|
|
response["DATA"] = rows;
|
|
|
|
} else if (view == "latency") {
|
|
// Top endpoints by average latency
|
|
q = queryExecute("
|
|
SELECT
|
|
Endpoint,
|
|
COUNT(*) as Calls,
|
|
ROUND(AVG(TotalMs)) as AvgMs,
|
|
ROUND(AVG(DbMs)) as AvgDbMs,
|
|
ROUND(AVG(AppMs)) as AvgAppMs,
|
|
MAX(TotalMs) as MaxMs,
|
|
ROUND(AVG(QueryCount), 1) as AvgQueries
|
|
FROM api_perf_log
|
|
WHERE LoggedAt > DATE_SUB(NOW(), INTERVAL :hours HOUR)
|
|
GROUP BY Endpoint
|
|
HAVING Calls >= 3
|
|
ORDER BY AvgMs DESC
|
|
LIMIT :lim
|
|
", {
|
|
hours: { value: hours, cfsqltype: "cf_sql_integer" },
|
|
lim: { value: limitRows, cfsqltype: "cf_sql_integer" }
|
|
}, { datasource: "payfrit" });
|
|
|
|
rows = [];
|
|
for (row in q) {
|
|
arrayAppend(rows, {
|
|
"Endpoint": row.Endpoint,
|
|
"Calls": row.Calls,
|
|
"AvgMs": row.AvgMs,
|
|
"AvgDbMs": row.AvgDbMs,
|
|
"AvgAppMs": row.AvgAppMs,
|
|
"MaxMs": row.MaxMs,
|
|
"AvgQueries": row.AvgQueries
|
|
});
|
|
}
|
|
response["DATA"] = rows;
|
|
|
|
} else if (view == "slow") {
|
|
// Slowest individual requests
|
|
q = queryExecute("
|
|
SELECT Endpoint, TotalMs, DbMs, AppMs, QueryCount, ResponseBytes,
|
|
BusinessID, UserID, LoggedAt
|
|
FROM api_perf_log
|
|
WHERE LoggedAt > DATE_SUB(NOW(), INTERVAL :hours HOUR)
|
|
ORDER BY TotalMs DESC
|
|
LIMIT :lim
|
|
", {
|
|
hours: { value: hours, cfsqltype: "cf_sql_integer" },
|
|
lim: { value: limitRows, cfsqltype: "cf_sql_integer" }
|
|
}, { datasource: "payfrit" });
|
|
|
|
rows = [];
|
|
for (row in q) {
|
|
arrayAppend(rows, {
|
|
"Endpoint": row.Endpoint,
|
|
"TotalMs": row.TotalMs,
|
|
"DbMs": row.DbMs,
|
|
"AppMs": row.AppMs,
|
|
"QueryCount": row.QueryCount,
|
|
"ResponseBytes": row.ResponseBytes,
|
|
"BusinessID": row.BusinessID,
|
|
"UserID": row.UserID,
|
|
"LoggedAt": dateTimeFormat(row.LoggedAt, "yyyy-mm-dd HH:nn:ss")
|
|
});
|
|
}
|
|
response["DATA"] = rows;
|
|
|
|
} else if (view == "summary") {
|
|
// Overall summary stats
|
|
q = queryExecute("
|
|
SELECT
|
|
COUNT(*) as TotalRequests,
|
|
COUNT(DISTINCT Endpoint) as UniqueEndpoints,
|
|
ROUND(AVG(TotalMs)) as OverallAvgMs,
|
|
MAX(TotalMs) as OverallMaxMs,
|
|
ROUND(AVG(DbMs)) as OverallAvgDbMs,
|
|
ROUND(AVG(AppMs)) as OverallAvgAppMs,
|
|
ROUND(AVG(QueryCount), 1) as OverallAvgQueries,
|
|
MIN(LoggedAt) as FirstLog,
|
|
MAX(LoggedAt) as LastLog
|
|
FROM api_perf_log
|
|
WHERE LoggedAt > DATE_SUB(NOW(), INTERVAL :hours HOUR)
|
|
", {
|
|
hours: { value: hours, cfsqltype: "cf_sql_integer" }
|
|
}, { datasource: "payfrit" });
|
|
|
|
response["DATA"] = {
|
|
"TotalRequests": q.TotalRequests,
|
|
"UniqueEndpoints": q.UniqueEndpoints,
|
|
"OverallAvgMs": q.OverallAvgMs,
|
|
"OverallMaxMs": q.OverallMaxMs,
|
|
"OverallAvgDbMs": q.OverallAvgDbMs,
|
|
"OverallAvgAppMs": q.OverallAvgAppMs,
|
|
"OverallAvgQueries": q.OverallAvgQueries,
|
|
"FirstLog": isDate(q.FirstLog) ? dateTimeFormat(q.FirstLog, "yyyy-mm-dd HH:nn:ss") : "",
|
|
"LastLog": isDate(q.LastLog) ? dateTimeFormat(q.LastLog, "yyyy-mm-dd HH:nn:ss") : ""
|
|
};
|
|
|
|
} else {
|
|
apiAbort({ "OK": false, "ERROR": "invalid_view", "MESSAGE": "Use ?view=count|latency|slow|summary" });
|
|
}
|
|
|
|
apiAbort(response);
|
|
|
|
} catch (any e) {
|
|
apiAbort({
|
|
"OK": false,
|
|
"ERROR": "server_error",
|
|
"MESSAGE": e.message,
|
|
"DETAIL": e.detail
|
|
});
|
|
}
|
|
</cfscript>
|