Skip to content

Commit

Permalink
Initial setup of log tracing tools
Browse files Browse the repository at this point in the history
  • Loading branch information
MattiasOlla committed Oct 29, 2024
0 parents commit 7d0697a
Show file tree
Hide file tree
Showing 24 changed files with 5,449 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .c8rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"reporter": ["text", "html"],
"all": true,
"statements": "100",
"branches": "100",
"functions": "100",
"lines": "100",
"exclude": ["coverage", "dist", "test", "index.ts", "eslint.config.js"]
}
11 changes: 11 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# EditorConfig is awesome: http://EditorConfig.org

# top-most EditorConfig file
root = true

[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 2
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
on:
push:
branches:
- main
pull_request:

name: Create Release

jobs:
test:
name: Run tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm

- name: Install dependencies
run: npm ci

- name: Lint
run: npm run lint

- name: Check formatting
run: |
npm run format
[[ $(git status --porcelain) ]] && exit 1
- name: Check types
run: npm run typecheck

- name: Run tests
run: npm run coverage

build:
name: Create Release
runs-on: ubuntu-latest
needs: test
if: github.event_name == 'push' && github.ref_name == github.event.repository.default_branch
steps:
- uses: actions/checkout@v4

- name: Check if package version changed
id: check_version
run: |
version="v$(cat package.json | jq -r '.version')"
if [ $(git tag -l "$version") ]; then
echo "Tag $version already exists."
else
echo "version_tag=$version" >> "$GITHUB_OUTPUT"
fi
- uses: actions/setup-node@v4
if: steps.check_version.outputs.version_tag
with:
node-version-file: .nvmrc
cache: npm

# TODO: enable once we are ready
# - name: Create GitHub release
# if: steps.check_version.outputs.version_tag
# run: |
# gh release create ${{ steps.check_version.outputs.version_tag }} --generate-notes
# env:
# GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# - name: Publish to NPM
# if: steps.check_version.outputs.version_tag
# run: |
# npm ci
# npm publish --access public
# env:
# NODE_AUTH_TOKEN: ...
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
coverage
dist
node_modules
10 changes: 10 additions & 0 deletions .mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"timeout": 1000,
"reporter": "spec",
"recursive": true,
"ui": "mocha-cakes-2",
"require": ["./test/setup.ts"],
"exit": true,
"extension": ["ts"],
"node-option": ["import=tsx/esm"]
}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"bracketSpacing": true,
"printWidth": 120,
"arrowParens": "always",
"trailingComma": "es5",
"semi": true
}
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# @bonniernews/logger

## Usage

```js
import {
fetchGcpProjectId,
getHttpTraceHeader,
getLoggingTraceData,
middleware,
} from "@bonniernews/bn-log-tracer";
import express from "express";
import pino from "pino";

const logger = pino({ mixin: getLoggingTraceData });
const app = express();

app.use(middleware);

// Fetches the project ID from the GCP metadata server in the background on startup.
// This is only necessary if you don't set the `GCP_PROJECT` environment variable.
fetchGcpProjectId();

app.get("/", async (req, res) => {
logger.info("Hello, world!");

const response = await fetch("https://some.service.bn.nr/some/endpoint", {
headers: { ...getHttpTraceHeader() },
});

...
});
```
8 changes: 8 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// @ts-check

import js from "@eslint/js";
import ts from "typescript-eslint";

export default ts.config(js.configs.recommended, ...ts.configs.recommended, ...ts.configs.strict, {
ignores: ["coverage", "dist"],
});
4 changes: 4 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { fetchGcpProjectId } from "./lib/gcp";
export { getHttpTraceHeader } from "./lib/http";
export { getLoggingTraceData } from "./lib/logging";
export { middleware } from "./lib/middleware";
34 changes: 34 additions & 0 deletions lib/gcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
let gcpProjectId: string | undefined = undefined;

export function getGcpProjectId() {
return process.env.GCP_PROJECT || gcpProjectId;
}

/**
* Fetches the Google Cloud Platform (GCP) project ID from the GCP metadata server.
*
* You only need to call this function if you're not setting the `GCP_PROJECT` environment variable.
*/
export async function fetchGcpProjectId() {
let gcpMetaData;
/* c8 ignore start */
try {
gcpMetaData = await import("gcp-metadata");
} catch {
console.error("Failed to import gcp-metadata module");
return;
}
/* c8 ignore stop */

const isAvailable = await gcpMetaData.isAvailable();
if (!isAvailable) return;

gcpProjectId = await gcpMetaData.project("project-id");
}

/**
* Resets the GCP project ID, for testing.
*/
export async function reset() {
gcpProjectId = undefined;
}
7 changes: 7 additions & 0 deletions lib/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { getStore } from "./middleware";

export function getHttpTraceHeader() {
const { traceparent } = getStore();
if (traceparent) return { traceparent };
return {};
}
97 changes: 97 additions & 0 deletions lib/logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { getGcpProjectId } from "./gcp";
import { getStore } from "./middleware";
import { getTraceFromTraceparent } from "./traceparent";

export function getLoggingTraceData() {
const { traceparent, ...rest } = getStore();
if (!traceparent) return {};

const trace = getTraceFromTraceparent(traceparent);
if (!trace) return rest;

const logData = { traceId: trace.traceId, spanId: trace.parentId, ...rest };

const gcpProjectId = getGcpProjectId();
if (!gcpProjectId) {
console.log("GCP Project ID not found");
return logData;
}

return {
...logData,
"logging.googleapis.com/trace": `projects/${gcpProjectId}/traces/${trace.traceId}`,
"logging.googleapis.com/spanId": trace.parentId,
"logging.googleapis.com/trace_sampled": trace.isSampled,
};
}

// TODO: This is a copy of exp-logger, implement this
// export function buildLogger(opts: SomeType) {
// return pino({ opts, mixin: getLoggingTraceData });
// }

// function severity(label) {
// switch (label) {
// case "trace":
// return "DEBUG";
// case "debug":
// return "DEBUG";
// case "info":
// return "INFO";
// case "warn":
// return "WARNING";
// case "error":
// return "ERROR";
// case "fatal":
// return "CRITICAL";
// default:
// return "DEFAULT";
// }
// }

// /**
// * @typedef LoggerOptions
// * @property {string} options.logLevel="info" which level of severity to log at
// * @property {function} options.mixin mixin for additional information in the log statement
// * @property {function} [options.formatLog] function to do change the shape of the log object
// */

// /** @typedef {import("pino").Logger} Logger */

// /**
// * @param {LoggerOptions} options
// * @return {Logger} the logger.
// *
// */
// function init(options) {
// const env = process.env.NODE_ENV || "development";
// const shouldPrettyPrint = ["development", "test", "dev"].includes(env);

// const logLocation = env === "test" && "./logs/test.log";
// return pino({
// level: options?.logLevel ?? "info",
// messageKey: "message",
// base: undefined,
// formatters: {
// level(label) {
// if (shouldPrettyPrint) {
// return { level: label };
// }
// return { severity: severity(label) };
// },
// ...(options?.formatLog && { log: options?.formatLog }),
// },
// timestamp: () => `,"time": "${new Date().toISOString()}"`,
// transport: shouldPrettyPrint
// ? {
// target: "pino-pretty",
// options: {
// destination: logLocation || 1,
// colorize: !logLocation,
// messageKey: "message",
// },
// }
// : undefined,
// mixin: options?.mixin,
// });
// }
22 changes: 22 additions & 0 deletions lib/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { RequestHandler } from "express";
import { AsyncLocalStorage } from "node:async_hooks";

type Store = {
traceparent?: string;
clientServiceAccount?: string;
[key: string]: unknown;
};

const storage = new AsyncLocalStorage<Store>();

export const middleware: RequestHandler = (req, _res, next) => {
const traceparent = req.header("traceparent");

storage.run({ traceparent }, () => {
next();
});
};

export function getStore() {
return storage.getStore() || {};
}
19 changes: 19 additions & 0 deletions lib/traceparent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00
*
* base16(version) = 00
* base16(trace-id) = 4bf92f3577b34da6a3ce929d0e0e4736
* base16(parent-id) = 00f067aa0ba902b7
* base16(trace-flags) = 00 // 00 is not sampled, 01 is sampled
*/
export function getTraceFromTraceparent(traceHeader: string) {
const parts = traceHeader.split("-");

if (!parts || parts.length !== 4) return;

return {
traceId: parts[1],
parentId: parts[2],
isSampled: parts[3] !== "00",
};
}
Loading

0 comments on commit 7d0697a

Please sign in to comment.