343 lines
9.7 KiB
JavaScript
343 lines
9.7 KiB
JavaScript
import express from "express";
|
|
import { readFileSync } from "node:fs";
|
|
import {
|
|
CLOUDWATCH_DEFAULT_LIMIT,
|
|
CLOUDWATCH_DEFAULT_WINDOW_MS,
|
|
CLOUDWATCH_ENDPOINT,
|
|
CLOUDWATCH_REGION,
|
|
DEFAULT_REFRESH_MS,
|
|
PORT,
|
|
S3_ENDPOINT,
|
|
S3_REGION,
|
|
SES_ENDPOINT,
|
|
SECRETS_ENDPOINT,
|
|
SECRETS_REGION
|
|
} from "./server/config.js";
|
|
import { getClientConfig, renderHtml } from "./server/page.js";
|
|
import {
|
|
buildAttachmentDisposition,
|
|
buildInlineDisposition,
|
|
clampNumber,
|
|
findSesMessageById,
|
|
loadLogEvents,
|
|
loadLogGroups,
|
|
loadLogStreams,
|
|
loadMessageAttachment,
|
|
loadMessages,
|
|
loadS3Buckets,
|
|
loadS3ObjectDownload,
|
|
loadS3ObjectPreview,
|
|
loadS3Objects,
|
|
loadSecretValue,
|
|
loadSecrets,
|
|
loadServiceHealthSummary
|
|
} from "./server/localstack-service.js";
|
|
|
|
const app = express();
|
|
const CLIENT_APP_PATH = new URL("./public/client-app.js", import.meta.url);
|
|
const CLIENT_APP_SOURCE = readFileSync(CLIENT_APP_PATH, "utf8");
|
|
|
|
app.use((req, res, next) => {
|
|
res.set("Cache-Control", "no-store");
|
|
next();
|
|
});
|
|
|
|
app.get("/", (req, res) => {
|
|
res.type("html").send(renderHtml());
|
|
});
|
|
|
|
app.get("/app.js", (req, res) => {
|
|
res.type("application/javascript").send(`${CLIENT_APP_SOURCE}\n\nclientApp(${JSON.stringify(getClientConfig())});\n`);
|
|
});
|
|
|
|
app.get("/health", (req, res) => {
|
|
res.json({
|
|
ok: true,
|
|
endpoint: SES_ENDPOINT,
|
|
endpoints: {
|
|
ses: SES_ENDPOINT,
|
|
cloudWatchLogs: CLOUDWATCH_ENDPOINT,
|
|
secretsManager: SECRETS_ENDPOINT,
|
|
s3: S3_ENDPOINT
|
|
},
|
|
port: PORT,
|
|
defaultRefreshMs: DEFAULT_REFRESH_MS
|
|
});
|
|
});
|
|
|
|
app.get("/api/service-health", async (req, res) => {
|
|
try {
|
|
res.json(await loadServiceHealthSummary());
|
|
} catch (error) {
|
|
console.error("Error fetching service health:", error);
|
|
res.status(502).json({
|
|
error: "Unable to fetch LocalStack service health",
|
|
details: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/messages", async (req, res) => {
|
|
try {
|
|
res.json(await loadMessages());
|
|
} catch (error) {
|
|
console.error("Error fetching messages:", error);
|
|
res.status(502).json({
|
|
error: "Unable to fetch messages from LocalStack SES",
|
|
details: error.message,
|
|
endpoint: SES_ENDPOINT
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/messages/:id/raw", async (req, res) => {
|
|
try {
|
|
const message = await findSesMessageById(req.params.id);
|
|
|
|
if (!message) {
|
|
res.status(404).type("text/plain").send("Message not found");
|
|
return;
|
|
}
|
|
|
|
res.type("text/plain").send(message.RawData || "");
|
|
} catch (error) {
|
|
console.error("Error fetching raw message:", error);
|
|
res.status(502).type("text/plain").send(`Unable to fetch raw message: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
app.get("/api/messages/:id/attachments/:index", async (req, res) => {
|
|
try {
|
|
const attachmentIndex = Number.parseInt(req.params.index, 10);
|
|
|
|
if (!Number.isInteger(attachmentIndex) || attachmentIndex < 0) {
|
|
res.status(400).type("text/plain").send("Attachment index must be a non-negative integer");
|
|
return;
|
|
}
|
|
|
|
const attachment = await loadMessageAttachment(req.params.id, attachmentIndex);
|
|
|
|
if (!attachment) {
|
|
res.status(404).type("text/plain").send("Attachment not found");
|
|
return;
|
|
}
|
|
|
|
res.setHeader("Content-Type", attachment.contentType);
|
|
res.setHeader("Content-Disposition", buildAttachmentDisposition(attachment.filename));
|
|
res.setHeader("Content-Length", String(attachment.content.length));
|
|
res.send(attachment.content);
|
|
} catch (error) {
|
|
console.error("Error downloading attachment:", error);
|
|
res.status(502).type("text/plain").send(`Unable to download attachment: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
app.get("/api/logs/groups", async (req, res) => {
|
|
try {
|
|
const groups = await loadLogGroups();
|
|
res.json({
|
|
endpoint: CLOUDWATCH_ENDPOINT,
|
|
region: CLOUDWATCH_REGION,
|
|
groups
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching log groups:", error);
|
|
res.status(502).json({
|
|
error: "Unable to fetch CloudWatch log groups from LocalStack",
|
|
details: error.message,
|
|
endpoint: CLOUDWATCH_ENDPOINT
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/logs/streams", async (req, res) => {
|
|
try {
|
|
const logGroupName = String(req.query.group || "");
|
|
|
|
if (!logGroupName) {
|
|
res.status(400).json({ error: "Query parameter 'group' is required" });
|
|
return;
|
|
}
|
|
|
|
res.json({
|
|
logGroupName,
|
|
streams: await loadLogStreams(logGroupName)
|
|
});
|
|
} catch (error) {
|
|
console.error("Error fetching log streams:", error);
|
|
res.status(502).json({
|
|
error: "Unable to fetch CloudWatch log streams from LocalStack",
|
|
details: error.message,
|
|
endpoint: CLOUDWATCH_ENDPOINT
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/logs/events", async (req, res) => {
|
|
try {
|
|
const logGroupName = String(req.query.group || "");
|
|
const logStreamName = String(req.query.stream || "");
|
|
const windowMs = clampNumber(req.query.windowMs, CLOUDWATCH_DEFAULT_WINDOW_MS, 60 * 1000, 24 * 60 * 60 * 1000);
|
|
const limit = clampNumber(req.query.limit, CLOUDWATCH_DEFAULT_LIMIT, 25, 500);
|
|
|
|
if (!logGroupName) {
|
|
res.status(400).json({ error: "Query parameter 'group' is required" });
|
|
return;
|
|
}
|
|
|
|
res.json(await loadLogEvents({ logGroupName, logStreamName, windowMs, limit }));
|
|
} catch (error) {
|
|
console.error("Error fetching log events:", error);
|
|
res.status(502).json({
|
|
error: "Unable to fetch CloudWatch log events from LocalStack",
|
|
details: error.message,
|
|
endpoint: CLOUDWATCH_ENDPOINT
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/secrets", async (req, res) => {
|
|
try {
|
|
res.json(await loadSecrets());
|
|
} catch (error) {
|
|
console.error("Error fetching secrets:", error);
|
|
res.status(502).json({
|
|
error: "Unable to fetch Secrets Manager secrets from LocalStack",
|
|
details: error.message,
|
|
endpoint: SECRETS_ENDPOINT
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/secrets/value", async (req, res) => {
|
|
try {
|
|
const secretId = String(req.query.id || "");
|
|
|
|
if (!secretId) {
|
|
res.status(400).json({ error: "Query parameter 'id' is required" });
|
|
return;
|
|
}
|
|
|
|
res.json(await loadSecretValue(secretId));
|
|
} catch (error) {
|
|
if (error?.name === "ResourceNotFoundException") {
|
|
res.status(404).json({
|
|
error: "Secret not found",
|
|
details: error.message,
|
|
endpoint: SECRETS_ENDPOINT
|
|
});
|
|
return;
|
|
}
|
|
|
|
console.error("Error fetching secret value:", error);
|
|
res.status(502).json({
|
|
error: "Unable to fetch Secrets Manager value from LocalStack",
|
|
details: error.message,
|
|
endpoint: SECRETS_ENDPOINT
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/s3/buckets", async (req, res) => {
|
|
try {
|
|
res.json(await loadS3Buckets());
|
|
} catch (error) {
|
|
console.error("Error fetching S3 buckets:", error);
|
|
res.status(502).json({
|
|
error: "Unable to fetch S3 buckets from LocalStack",
|
|
details: error.message,
|
|
endpoint: S3_ENDPOINT
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/s3/objects", async (req, res) => {
|
|
try {
|
|
const bucket = String(req.query.bucket || "");
|
|
const prefix = String(req.query.prefix || "");
|
|
|
|
if (!bucket) {
|
|
res.status(400).json({ error: "Query parameter 'bucket' is required" });
|
|
return;
|
|
}
|
|
|
|
res.json(await loadS3Objects({ bucket, prefix }));
|
|
} catch (error) {
|
|
console.error("Error fetching S3 objects:", error);
|
|
res.status(502).json({
|
|
error: "Unable to fetch S3 objects from LocalStack",
|
|
details: error.message,
|
|
endpoint: S3_ENDPOINT
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/s3/object", async (req, res) => {
|
|
try {
|
|
const bucket = String(req.query.bucket || "");
|
|
const key = String(req.query.key || "");
|
|
|
|
if (!bucket || !key) {
|
|
res.status(400).json({ error: "Query parameters 'bucket' and 'key' are required" });
|
|
return;
|
|
}
|
|
|
|
res.json(await loadS3ObjectPreview({ bucket, key }));
|
|
} catch (error) {
|
|
if (error?.name === "NoSuchKey" || error?.name === "NotFound") {
|
|
res.status(404).json({
|
|
error: "Object not found",
|
|
details: error.message,
|
|
endpoint: S3_ENDPOINT
|
|
});
|
|
return;
|
|
}
|
|
|
|
console.error("Error fetching S3 object preview:", error);
|
|
res.status(502).json({
|
|
error: "Unable to fetch S3 object preview from LocalStack",
|
|
details: error.message,
|
|
endpoint: S3_ENDPOINT
|
|
});
|
|
}
|
|
});
|
|
|
|
app.get("/api/s3/download", async (req, res) => {
|
|
try {
|
|
const bucket = String(req.query.bucket || "");
|
|
const key = String(req.query.key || "");
|
|
const inline = String(req.query.inline || "") === "1";
|
|
|
|
if (!bucket || !key) {
|
|
res.status(400).type("text/plain").send("Query parameters 'bucket' and 'key' are required");
|
|
return;
|
|
}
|
|
|
|
const object = await loadS3ObjectDownload({ bucket, key });
|
|
|
|
res.setHeader("Content-Type", object.contentType);
|
|
res.setHeader(
|
|
"Content-Disposition",
|
|
inline ? buildInlineDisposition(object.filename) : buildAttachmentDisposition(object.filename)
|
|
);
|
|
res.setHeader("Content-Length", String(object.content.length));
|
|
res.send(object.content);
|
|
} catch (error) {
|
|
if (error?.name === "NoSuchKey" || error?.name === "NotFound") {
|
|
res.status(404).type("text/plain").send("Object not found");
|
|
return;
|
|
}
|
|
|
|
console.error("Error downloading S3 object:", error);
|
|
res.status(502).type("text/plain").send(`Unable to download S3 object: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
app.listen(PORT, () => {
|
|
console.log(`LocalStack inspector is running on http://localhost:${PORT}`);
|
|
console.log(`Watching LocalStack SES endpoint at ${SES_ENDPOINT}`);
|
|
console.log(`Watching LocalStack CloudWatch Logs endpoint at ${CLOUDWATCH_ENDPOINT} (${CLOUDWATCH_REGION})`);
|
|
console.log(`Watching LocalStack Secrets Manager endpoint at ${SECRETS_ENDPOINT} (${SECRETS_REGION})`);
|
|
console.log(`Watching LocalStack S3 endpoint at ${S3_ENDPOINT} (${S3_REGION})`);
|
|
});
|