Skip to content

Commit

Permalink
Color log lines in fauna local command. (#524)
Browse files Browse the repository at this point in the history
* Color log lines in fauna local command.

* Remove only

* Test debug

* Test debug

* Test fixes?

* Remove formatting tests
  • Loading branch information
cleve-fauna authored Dec 13, 2024
1 parent 2b9c521 commit 2f32803
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 44 deletions.
1 change: 1 addition & 0 deletions src/commands/local.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ async function startLocal(argv) {
pull: argv.pull,
interval: argv.interval,
maxAttempts: argv.maxAttempts,
color: argv.color,
});
}

Expand Down
67 changes: 36 additions & 31 deletions src/lib/docker-containers.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { container } from "../cli.mjs";
import { CommandError, SUPPORT_MESSAGE } from "./errors.mjs";
import { colorize, Format } from "./formatting/colorize.mjs";

const IMAGE_NAME = "fauna/faunadb:latest";
let color = false;

/**
* Ensures the container is running
Expand All @@ -23,8 +25,9 @@ export async function ensureContainerRunning({
pull,
interval,
maxAttempts,
color: _color,
}) {
const logger = container.resolve("logger");
color = _color;
if (pull) {
await pullImage(IMAGE_NAME);
}
Expand All @@ -35,7 +38,7 @@ export async function ensureContainerRunning({
hostPort,
containerPort,
});
logger.stderr(
stderr(
`[StartContainer] Container '${containerName}' started. Monitoring HealthCheck for readiness.`,
);
await waitForHealthCheck({
Expand All @@ -44,9 +47,7 @@ export async function ensureContainerRunning({
interval,
maxAttempts,
});
logger.stderr(
`[ContainerReady] Container '${containerName}' is up and healthy.`,
);
stderr(`[ContainerReady] Container '${containerName}' is up and healthy.`);
}

/**
Expand All @@ -57,8 +58,7 @@ export async function ensureContainerRunning({
*/
async function pullImage(imageName) {
const docker = container.resolve("docker");
const logger = container.resolve("logger"); // Dependency injection for logger
logger.stderr(`[PullImage] Pulling image '${imageName}'...\n`);
stderr(`[PullImage] Pulling image '${imageName}'...`);

try {
const stream = await docker.pull(imageName);
Expand All @@ -70,12 +70,12 @@ async function pullImage(imageName) {
docker.modem.followProgress(
stream,
(err, output) => {
writePullProgress(layers, numLines);
writePullProgress(layers, numLines, imageName);
if (err) {
reject(err);
} else {
// Move to the reserved space for completion message
logger.stderr(`[PullImage] Image '${imageName}' pulled.`);
stderr(`[PullImage] Image '${imageName}' pulled.`);
resolve(output);
}
},
Expand All @@ -86,7 +86,7 @@ async function pullImage(imageName) {
`${event.id}: ${event.status} ${event.progress || ""}`;
}
if (Date.now() - lastUpdate > 100) {
numLines = writePullProgress(layers, numLines);
numLines = writePullProgress(layers, numLines, imageName);
lastUpdate = Date.now();
}
},
Expand All @@ -106,19 +106,21 @@ async function pullImage(imageName) {
* so that the progress is displayed in the same place with no "flicker".
* @param {Object} layers The layers of the image
* @param {number} numLines The number of lines to clear and update
* @param {string} imageName The image name
* @returns {number} The number of lines written. Pass this value back into
* the next call to writePullProgress so that it can update the lines in place.
*/
function writePullProgress(layers, numLines) {
const logger = container.resolve("logger");
function writePullProgress(layers, numLines, imageName) {
const stderrStream = container.resolve("stderrStream");
// Clear only the necessary lines and update them in place
stderrStream.write(`\x1B[${numLines}A`);
numLines = 0;
// clear the screen
stderrStream.write("\x1B[0J");
stderr(`[PullImage] Pulling image '${imageName}'...`);
numLines++;
Object.values(layers).forEach((line) => {
logger.stderr(line);
stderr(line);
numLines++;
});
return numLines;
Expand All @@ -137,8 +139,7 @@ function writePullProgress(layers, numLines) {
*/
async function findContainer({ containerName, hostPort }) {
const docker = container.resolve("docker");
const logger = container.resolve("logger"); // Dependency injection for logger
logger.stderr(`[FindContainer] Looking for container '${containerName}'...`);
stderr(`[FindContainer] Looking for container '${containerName}'...`);
const filters = JSON.stringify({ name: [containerName] });
const containers = await docker.listContainers({ all: true, filters });
if (containers.length === 0) {
Expand Down Expand Up @@ -253,14 +254,13 @@ async function startContainer({
containerPort,
}) {
const docker = container.resolve("docker");
const logger = container.resolve("logger");
const existingContainer = await findContainer({ containerName, hostPort });
let logStream = undefined;
if (existingContainer) {
const dockerContainer = docker.getContainer(existingContainer.Id);
const state = existingContainer.State;
if (state === "paused") {
logger.stderr(
stderr(
`[StartContainer] Container '${containerName}' exists but is paused. Unpausing it...`,
);
await dockerContainer.unpause();
Expand All @@ -269,7 +269,7 @@ async function startContainer({
containerName,
});
} else if (state === "created" || state === "exited") {
logger.stderr(
stderr(
`[StartContainer] Container '${containerName}' exists in state '${existingContainer.State}'. Starting it...`,
);
await dockerContainer.start();
Expand All @@ -278,7 +278,7 @@ async function startContainer({
containerName,
});
} else if (state === "running") {
logger.stderr(
stderr(
`[StartContainer] Container '${containerName}' is already running.`,
);
} else {
Expand All @@ -287,7 +287,7 @@ async function startContainer({
);
}
} else {
logger.stderr(`[StartContainer] Starting container '${containerName}'...`);
stderr(`[StartContainer] Starting container '${containerName}'...`);
const dockerContainer = await createContainer({
imageName,
containerName,
Expand All @@ -312,7 +312,6 @@ async function startContainer({
* @returns {Promise<Object>} The log stream
*/
async function createLogStream({ dockerContainer, containerName }) {
const logger = container.resolve("logger");
let logStream = await dockerContainer.logs({
stdout: true,
stderr: true,
Expand All @@ -322,21 +321,19 @@ async function createLogStream({ dockerContainer, containerName }) {

// Pipe the logs to your logger
logStream.on("data", (chunk) => {
logger.stderr(`[StartContainer][${containerName}] ${chunk.toString()}`);
stderr(`[StartContainer][${containerName}] ${chunk.toString()}`);
});

logStream.on("end", async () => {
logger.stderr(
`[StartContainer] Container '${containerName}' logs have finished.`,
);
stderr(`[StartContainer] Container '${containerName}' logs have finished.`);
logStream = await createLogStream({
dockerContainer,
containerName,
});
});

logStream.on("error", (error) => {
logger.stderr(
stderr(
`[StartContainer] Error tailing logs for container '${containerName}': ${error.message}`,
);
});
Expand All @@ -360,9 +357,8 @@ async function waitForHealthCheck({
interval = 10000,
logStream,
}) {
const logger = container.resolve("logger");
const fetch = container.resolve("fetch");
logger.stderr(`[HealthCheck] Waiting for Fauna to be ready at ${url}...`);
stderr(`[HealthCheck] Waiting for Fauna to be ready at ${url}...`);

let attemptCounter = 0;
let errorMessage = "";
Expand All @@ -374,15 +370,15 @@ async function waitForHealthCheck({
timeout: 1000,
});
if (response.ok) {
logger.stderr(`[HealthCheck] Fauna is ready at ${url}`);
stderr(`[HealthCheck] Fauna is ready at ${url}`);
logStream?.destroy();
return;
}
errorMessage = `with HTTP status: '${response.status}'`;
} catch (e) {
errorMessage = `with error: ${e.message}`;
}
logger.stderr(
stderr(
`[HealthCheck] Fauna is not yet ready. Attempt ${attemptCounter + 1}/${maxAttempts} failed ${errorMessage}. Retrying in ${interval / 1000} seconds...`,
);
attemptCounter++;
Expand All @@ -392,10 +388,19 @@ async function waitForHealthCheck({
});
}

logger.stderr(
stderr(
`[HealthCheck] Max attempts reached. Service at ${url} did not respond.`,
);
throw new CommandError(
`[HealthCheck] Fauna at ${url} is not ready after ${maxAttempts} attempts. Consider increasing --interval or --maxAttempts.`,
);
}

/**
* Outputs to stderr.
* @param {string} log The log
*/
function stderr(log) {
const logger = container.resolve("logger");
logger.stderr(colorize(log, { format: Format.LOG, color }));
}
5 changes: 3 additions & 2 deletions src/lib/formatting/codeToAnsi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import chalk from "chalk";
import { createHighlighterCoreSync } from "shiki/core";
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
import json from "shiki/langs/json.mjs";
import log from "shiki/langs/log.mjs";
import githubDarkHighContrast from "shiki/themes/github-dark-high-contrast.mjs";

import { isTTY } from "../misc.mjs";
Expand All @@ -12,7 +13,7 @@ const THEME = "github-dark-high-contrast";
export const createHighlighter = () => {
const highlighter = createHighlighterCoreSync({
themes: [githubDarkHighContrast],
langs: [json, fql],
langs: [fql, log, json],
engine: createJavaScriptRegexEngine(),
});

Expand Down Expand Up @@ -64,7 +65,7 @@ const { codeToTokensBase, getTheme } = createHighlighter();
* Returns a string with ANSI codes applied to the code. This is a JS port of the
* TypeScript codeToAnsi function from the Shiki library.
* @param {*} code - The code to format.
* @param {"json" | "fql"} language - The language of the code.
* @param {"fql" | "log" | "json"} language - The language of the code.
* @returns {string} - The formatted code with ANSI codes applied.
*/
export function codeToAnsi(code, language) {
Expand Down
12 changes: 12 additions & 0 deletions src/lib/formatting/colorize.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import stripAnsi from "strip-ansi";

import { container } from "../../cli.mjs";
import { codeToAnsi } from "./codeToAnsi.mjs";

export const Format = {
FQL: "fql",
LOG: "log",
JSON: "json",
TEXT: "text",
};
Expand Down Expand Up @@ -42,6 +44,14 @@ const jsonToAnsi = (obj) => {
return res.trim();
};

const logToAnsi = (obj) => {
if (typeof obj !== "string") {
throw new Error("Unable to format LOG unless it is already a string.");
}
const res = codeToAnsi(obj, "log");
return res.trim();
};

/**
* Formats an object for display with ANSI color codes.
* @param {any} obj - The object to format
Expand All @@ -55,6 +65,8 @@ export const toAnsi = (obj, { format = Format.TEXT } = {}) => {
return fqlToAnsi(obj);
case Format.JSON:
return jsonToAnsi(obj);
case Format.LOG:
return logToAnsi(obj);
default:
return textToAnsi(obj);
}
Expand Down
Loading

0 comments on commit 2f32803

Please sign in to comment.