Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logger status #33

Merged
merged 16 commits into from
Sep 20, 2023
20 changes: 18 additions & 2 deletions .github/workflows/smoke-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,30 @@ jobs:
run: |
docker exec smoke_test /bin/bash -c "source ./setup/install.sh --skip-req"

- name: Check ethereum-reader and manager are running properly both in legacy and v4 names
- name: Check ethereum-reader's status is being served using legacy name (management-service)
run: |
curl -f http://localhost/service/management-service/status

- name: Check ethereum-reader's status is being served using v4 name
run: |
curl -f http://localhost/service/ethereum-reader/status
curl -f http://localhost/service/manager/status

- name: Check manager's status is being served using legacy name (boyar)
run: |
curl -f http://localhost/service/boyar/status

- name: Check manager's status is being served using v4 name
run: |
curl -f http://localhost/service/manager/status

- name: Check logger's status is being served using legacy name (logs-service)
run: |
curl -f http://localhost/service/logs-service/status

- name: Check logger's status is being served using v4 name
run: |
curl -f http://localhost/service/logger/status

- name: Cleanup container
if: always()
run: |
Expand Down
6 changes: 4 additions & 2 deletions deployment/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ services:

logger:
container_name: logger
# TODO: move to Orbs Docker Hub repo
image: lukerogerson1/v3-logging:0.0.1
image: orbsnetworkstaging/v4-logger:v0.0.1
volumes:
- $PODMAN_SOCKET_PATH:/var/run/docker.sock
- /opt/orbs/logger:/opt/orbs/status
environment:
- STATUS_FILE_PATH=/opt/orbs/status/status.json
healthcheck:
# TODO: improve this healthcheck
test: ping -c 1 logger || exit 1
2 changes: 2 additions & 0 deletions logging/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ FROM node:18-alpine AS release
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=dependencies /app/node_modules ./node_modules
COPY .version ./.version

EXPOSE 80
CMD ["node", "./dist/main.js"]
5 changes: 5 additions & 0 deletions logging/create-version-file.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash -e

LATEST_SEMVER=$(git describe --tags --abbrev=0 --always)
SHORT_COMMIT=$(git rev-parse HEAD | cut -c1-8)
echo "$LATEST_SEMVER-$SHORT_COMMIT" > .version
66 changes: 41 additions & 25 deletions logging/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import express, { Express, NextFunction, Request, Response } from "express";
import { request, ClientRequest, RequestOptions, IncomingMessage } from "http";
import {writeStatusToDisk} from "./status";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I be annoying and ask you to run Prettier on this file? Formatting is quite different in places


const app: Express = express();
const port: number = 80;

const serviceLaunchTime = Math.round(new Date().getTime() / 1000);
const statusFilePath = process.env.STATUS_FILE_PATH || "/opt/orbs/status/status.json";

let error = '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked ChatGPT for advice about how we can avoid this "global" error variable and it suggested this:

import express, { Express, NextFunction, Request, Response } from "express";
import { request, ClientRequest, RequestOptions, IncomingMessage } from "http";
import { writeStatusToDisk } from "./status";

const app: Express = express();
const port: number = 80;

const serviceLaunchTime = Math.round(new Date().getTime() / 1000);
const statusFilePath = process.env.STATUS_FILE_PATH || "/opt/orbs/status/status.json";

setInterval(() => {
    writeStatusToDisk(statusFilePath, serviceLaunchTime);
}, 5 * 60 * 1000);

const validNameRegex = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;

app.use((_: Request, res: Response, next: NextFunction) => {
    res.setHeader("Content-Disposition", "inline");
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    next();
});

const decodeDockerLogs = (data: Buffer): string => {
    // ... [no change in this function]
};

app.get("/service/:name/log", (req: Request, res: Response, next: NextFunction) => {
    const containerName: string = req.params.name;

    if (!validNameRegex.test(containerName)) {
        return next(new Error("Invalid container name"));
    }

    const options: RequestOptions = {
        // ... [no change in this part]
    };

    const clientRequest: ClientRequest = request(options, (resp: IncomingMessage) => {
        // ... [no change in this function]
    });

    clientRequest.on("error", (e: Error) => {
        next(e); // Pass the error to the error handling middleware
    });

    clientRequest.end();
});

// Dedicated error handling middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
    console.error(err.message);
    writeStatusToDisk(statusFilePath, serviceLaunchTime, err.message); // Log the error
    res.status(500).send("An unexpected error occurred. Try again later");
});

app.listen(port, () => {
    console.log(`Logging service listening at http://localhost:${port}`);
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think? I do like that it centralises the whole error handling logic

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So how does it work? app.use is handling the errors, and next is in charge of surfacing them?


setInterval(function status() { // setInterval that also run immediately
writeStatusToDisk(statusFilePath, serviceLaunchTime, error);
error = '';
return status;
}(), 5 * 60 * 1000);

// TODO: This can happen at the nginx level
const validNameRegex = /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;

Expand All @@ -28,11 +40,12 @@ const decodeDockerLogs = (data: Buffer): string => {
return str;
};

app.get("/service/:name/logs", (req: Request, res: Response) => {
app.get("/service/:name/log", (req: Request, res: Response) => {
const containerName: string = req.params.name;

if (!validNameRegex.test(containerName)) {
return res.status(400).send("Invalid container name");
error = "Invalid container name";
return res.status(400).send(error);
}

const options: RequestOptions = {
Expand All @@ -42,33 +55,36 @@ app.get("/service/:name/logs", (req: Request, res: Response) => {
};

const clientRequest: ClientRequest = request(
options,
(resp: IncomingMessage) => {
if (resp.statusCode === 404) {
// TODO: add proper logger
console.log(
`User ${req.ip} requested logs for non-existent service ${containerName}`
);
res.status(404).send("Service not found");
} else if (resp.statusCode !== 200) {
console.error("500 error: ", resp);
res.status(500).send("An internal error occurred. Try again later");
} else {
console.log("BACK TO SQUARE THREE!!!");
let data = "";
// Log will be max 10MB due to log rotation
resp.on("data", (chunk: Buffer) => {
const logs = decodeDockerLogs(chunk);
data += logs;
});
resp.on("end", () => {
res.send(data);
});
options,
(resp: IncomingMessage) => {
if (resp.statusCode === 404) {
// TODO: add proper logger
console.log(
`User ${req.ip} requested logs for non-existent service ${containerName}`
);
error = "Service not found";
res.status(404).send(error);
} else if (resp.statusCode !== 200) {
error = String(resp.statusMessage);
console.error("500 error: ", resp);
res.status(500).send("An internal error occurred. Try again later");
} else {
console.log("BACK TO SQUARE THREE!!!");
let data = "";
// Log will be max 10MB due to log rotation
resp.on("data", (chunk: Buffer) => {
const logs = decodeDockerLogs(chunk);
data += logs;
});
resp.on("end", () => {
res.send(data);
});
}
}
}
);

clientRequest.on("error", (e: Error) => {
error = e.message;
console.error("onError: ", e);
res.status(500).send("An unexpected error occurred. Try again later");
});
Expand Down
43 changes: 43 additions & 0 deletions logging/src/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as fs from 'fs';
import {dirname} from "path";

export function generateStatusObj(serviceLaunchTime: number, err?: string) {
let status: any = {}
if (err) {
status['Status'] = 'Error';
status['Error'] = err;
}
else {
status['Status'] = 'OK';
}
status = Object.assign({
Timestamp: new Date().toISOString(),
Payload: {
Uptime: Math.round(new Date().getTime() / 1000) - serviceLaunchTime,
MemoryBytesUsed: process.memoryUsage().heapUsed,
Version: {
Semantic: getCurrentVersion(),
},
},
}, status);
return status;
}

export function writeStatusToDisk(filePath: string, serviceLaunchTime: number, err?: string) {
const status = generateStatusObj(serviceLaunchTime, err);

fs.mkdirSync(dirname(filePath), { recursive: true });
const content = JSON.stringify(status, null, 2);
fs.writeFileSync(filePath, content);

console.log(`Wrote status JSON to ${filePath} (${content.length} bytes).`);
}

export function getCurrentVersion() {
try {
return fs.readFileSync('/app/.version').toString().trim();
} catch (err: any) {
console.error(`Could not find version: ${err.message}`);
}
return '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just return something like "Version not found" for clarity?

}
36 changes: 28 additions & 8 deletions setup/scripts/health-check.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
#!/bin/bash

echo -e "${BLUE}Performing a health check...${NC}\n"
check_services() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could also look at using docker-compose -d --wait in the future - https://maciejwalkowiak.com/blog/docker-compose-waiting-containers-ready/

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, wasn't familiar.
I tried it now locally but it doesn't work. Probably b/c services are stuck on "starting" and not "healthy".
Creating a follow-up ticket

compose_file="$HOME/deployment/docker-compose.yml"
# Get the number of services defined in docker-compose file
num_services=$(docker-compose -f $compose_file config --services | wc -l)

sleep 10 # Wait for management service to start
for i in {1..5}
do
# Get the number of services that are up
num_up=$(docker-compose -f $compose_file ps | grep "Up" | wc -l)
if [ $num_up -eq $num_services ]; then
echo "All services are up and running."
return 0
else
echo "Waiting for services to start..."
sleep 5
fi
done
return 1
}

mgmt_svs_status_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/service/ethereum-reader/status)
if [ $mgmt_svs_status_code -eq 200 ]; then
echo -e "${GREEN}Installation complete! 🚀🚀🚀${NC}"
echo "------------------------------------"
echo -e "\n👉👉👉 ${YELLOW}Please register your Guardian using the following website: https://guardians.orbs.network?name=$name&website=$website&ip=$myip&node_address=$public_add ${NC} 👈👈👈\n" # TODO: only show once - during first installation
if check_services; then
mgmt_svs_status_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/service/ethereum-reader/status)
if [ $mgmt_svs_status_code -eq 200 ]; then
echo -e "${GREEN}Installation complete! 🚀🚀🚀${NC}"
echo "------------------------------------"
echo -e "\n👉👉👉 ${YELLOW}Please register your Guardian using the following website: https://guardians.orbs.network?name=$name&website=$website&ip=$myip&node_address=$public_add ${NC} 👈👈👈\n" # TODO: only show once - during first installation
else
echo -e "${RED}Installation incomplete!${NC}"
fi
else
echo -e "${RED}Installation incomplete!${NC}"
echo -e "${RED}Installation incomplete!${NC}"
fi