Compare commits
2 commits
d2e4b76610
...
50f95da95a
| Author | SHA1 | Date | |
|---|---|---|---|
| 50f95da95a | |||
| 3e297fb89f |
4 changed files with 57 additions and 0 deletions
15
README.md
15
README.md
|
|
@ -7,6 +7,7 @@ Express 5 + TypeScript backend generated from `nholtware/service-template-expres
|
||||||
- `GET /healthz` liveness, always 200 once listening
|
- `GET /healthz` liveness, always 200 once listening
|
||||||
- `GET /readyz` readiness: Postgres `SELECT 1` and Redis `PING` for whichever of `DATABASE_URL` / `REDIS_URL` are set; 503 + JSON reason otherwise
|
- `GET /readyz` readiness: Postgres `SELECT 1` and Redis `PING` for whichever of `DATABASE_URL` / `REDIS_URL` are set; 503 + JSON reason otherwise
|
||||||
- `GET /` service name, version (git sha), pod, a `service_info` row, Redis hit counter
|
- `GET /` service name, version (git sha), pod, a `service_info` row, Redis hit counter
|
||||||
|
- `GET /log-demo?msg=...` logs `{ demo: true, text }` with message `log-demo` (logging-pipeline demo; delete when you don't need it)
|
||||||
|
|
||||||
## Local dev
|
## Local dev
|
||||||
|
|
||||||
|
|
@ -20,6 +21,20 @@ Env is validated at boot (`src/lib/config.ts`): `PORT`, `SERVICE_NAME` (required
|
||||||
Migrations: SQL files in `migrations/`, applied with `npm run migrate`.
|
Migrations: SQL files in `migrations/`, applied with `npm run migrate`.
|
||||||
Logs: one JSON object per line on stdout (`level,time,msg,service,version`, plus `req.*`/`res.statusCode`/`responseTime` per request).
|
Logs: one JSON object per line on stdout (`level,time,msg,service,version`, plus `req.*`/`res.statusCode`/`responseTime` per request).
|
||||||
|
|
||||||
|
## Seeing your logs
|
||||||
|
|
||||||
|
Every container's stdout is shipped to Elasticsearch by Fluent Bit; nothing to configure per service.
|
||||||
|
|
||||||
|
1. Open Kibana: http://kibana.192.168.1.83.nip.io, user `elastic`, password from
|
||||||
|
`kubectl get secret -n logging logging-es-elastic-user -o jsonpath='{.data.elastic}' | base64 -d`.
|
||||||
|
2. **Discover**, data view `kube-*` (the default).
|
||||||
|
3. Query with KQL, e.g. `service:"${REPO_NAME}" and demo:true`, or `service:"${REPO_NAME}" and level:"error"`.
|
||||||
|
|
||||||
|
Any field you pass to the logger becomes a queryable field:
|
||||||
|
`req.log.info({ orderId }, "order placed")` → KQL `orderId:123`. The message string always
|
||||||
|
overrides a `msg` key in the object, so don't name a field `msg`. Lines show up within ~10 s;
|
||||||
|
retention is 14 days.
|
||||||
|
|
||||||
## Using this template
|
## Using this template
|
||||||
|
|
||||||
1. **Generate** a repo from this template in Forgejo (public). The repo name is substituted into `package.json`, this README, `.env.example`, and the first migration (CI derives the app name from the repo name itself).
|
1. **Generate** a repo from this template in Forgejo (public). The repo name is substituted into `package.json`, this README, `.env.example`, and the first migration (CI derives the app name from the repo name itself).
|
||||||
|
|
|
||||||
|
|
@ -67,3 +67,26 @@ describe("routes", () => {
|
||||||
expect(b.headers["x-request-id"]).toMatch(/^[0-9a-f-]{36}$/);
|
expect(b.headers["x-request-id"]).toMatch(/^[0-9a-f-]{36}$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("log-demo", () => {
|
||||||
|
it("logs one JSON line with demo fields and the request id", async () => {
|
||||||
|
const lines: Record<string, unknown>[] = [];
|
||||||
|
const captured = pino(
|
||||||
|
{ base: { service: "test-svc" } },
|
||||||
|
{ write: (l: string) => lines.push(JSON.parse(l)) },
|
||||||
|
);
|
||||||
|
const app = createApp({ config: parseConfig(base), logger: captured });
|
||||||
|
const res = await request(app)
|
||||||
|
.get("/log-demo?msg=hello")
|
||||||
|
.set("X-Request-Id", "rid-demo");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body).toMatchObject({ logged: true, text: "hello" });
|
||||||
|
const line = lines.find((l) => l.msg === "log-demo");
|
||||||
|
expect(line).toMatchObject({
|
||||||
|
service: "test-svc",
|
||||||
|
demo: true,
|
||||||
|
text: "hello",
|
||||||
|
req: { id: "rid-demo" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type { Config } from "./lib/config.js";
|
||||||
import type { Db } from "./lib/db.js";
|
import type { Db } from "./lib/db.js";
|
||||||
import type { Cache } from "./lib/redis.js";
|
import type { Cache } from "./lib/redis.js";
|
||||||
import { healthRouter } from "./routes/health.js";
|
import { healthRouter } from "./routes/health.js";
|
||||||
|
import { logDemoRouter } from "./routes/log-demo.js";
|
||||||
import { rootRouter } from "./routes/root.js";
|
import { rootRouter } from "./routes/root.js";
|
||||||
|
|
||||||
export interface Deps {
|
export interface Deps {
|
||||||
|
|
@ -43,6 +44,7 @@ export function createApp({ config, logger, db, cache }: Deps) {
|
||||||
|
|
||||||
app.use(healthRouter({ db, cache }));
|
app.use(healthRouter({ db, cache }));
|
||||||
app.use(rootRouter({ config, db, cache }));
|
app.use(rootRouter({ config, db, cache }));
|
||||||
|
app.use(logDemoRouter());
|
||||||
|
|
||||||
// Express identifies error handlers by arity; keep all four args.
|
// Express identifies error handlers by arity; keep all four args.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
|
|
||||||
17
src/routes/log-demo.ts
Normal file
17
src/routes/log-demo.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Router } from "express";
|
||||||
|
|
||||||
|
// Developer-story demo for the logging pipeline: every field in the first
|
||||||
|
// argument to req.log.* becomes a queryable field in Kibana (data view kube-*).
|
||||||
|
// The message string always wins over a `msg` key in the object, so the
|
||||||
|
// caller's text goes in `text`.
|
||||||
|
export function logDemoRouter(): Router {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get("/log-demo", (req, res) => {
|
||||||
|
const text = typeof req.query.msg === "string" ? req.query.msg : "";
|
||||||
|
req.log.info({ demo: true, text }, "log-demo");
|
||||||
|
res.json({ logged: true, text, requestId: req.id });
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue