@witchcraft/nuxt-logger 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # @witchcraft/nuxt-logger
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![License][license-src]][license-href]
6
+ [![Nuxt][nuxt-src]][nuxt-href]
7
+
8
+
9
+ Nuxt isomorphic-ish logging module (it can be used on the client/server/electron) and automatically adds a transport on the server and electron main to save logs to a file.
10
+
11
+ It's simplified wrapper around pino that exposes only the default logger methods.
12
+
13
+
14
+ - [🏀 Online playground](https://stackblitz.com/github/witchcraftjs/nuxt-logger?file=playground%2Fapp.vue)
15
+
16
+ ## Features
17
+
18
+ - Creates a nuxt plugin client side to log environment info (`info`).
19
+ - Creates a server middleware for logging requests (`trace`) with customizable redaction.
20
+ - `useLogger` composable for creating a client logger.
21
+ - `useServerLogger` composable for creating a server logger.
22
+ - `useElectronLogger` for electron + utilities for setting things up.
23
+
24
+ ## Install
25
+ ```bash
26
+ pnpx nuxi module add @witchcraft/nuxt-logger
27
+ ```
28
+
29
+ ## Usage
30
+
31
+ Just start using it with the `useLogger` or `useServerLogger` composable, they should be auto imported if you have auto imports on.
32
+
33
+ ```ts
34
+ const logger = useLogger()
35
+ logger.info({ns:"namespace", msg:"hello world"})
36
+ ```
37
+
38
+ ### Redaction
39
+
40
+ By default redacts some known sensitive keys for the middleware logging requests.
41
+
42
+ Also redacts the `redact` key, but only in production, so you can put anything semi-sensitive in there that you need to inspect in development.
43
+
44
+ ### Electron
45
+
46
+ The electron logger requires a bit of setup since electron's main cannot be passed the runtime config normally.
47
+
48
+ This assumes the usage of
49
+ [@witchcraft/nuxt-electron](todo) since it can handle this among other issues. Note this plugin needs to be before it in the plugins list.
50
+
51
+ There is a full example in the [@witchcraft/nuxt-electron playground](TODO).
52
+
53
+ ```ts [main.ts]
54
+ import { STATIC, useDevDataDir } from "@witchcraft/nuxt-electron/electron"
55
+ import { ELECTRON } from "@witchcraft/nuxt-logger/electron"
56
+
57
+ // assuming you stick your active window instances here
58
+ const windows = []
59
+ const userDataDir = useDevDataDir() ?? parsed(args).userDataDir ?? app.getPath("appData")
60
+
61
+ // this only needs to be called like this the first time
62
+ const logger = useElectronLogger(
63
+ {
64
+ ...STATIC.ELECTRON_RUNTIME_CONFIG.logger,
65
+ // override the log path since with electron we only know it at runtime
66
+ // this module sets it to undefined for electron anyways
67
+ logPath: path.join(userDataDir, "log.txt"),
68
+ },
69
+ // give it a way to access windows to be able to communicate with them
70
+ () => windows,
71
+ {
72
+ // true by default, allows recieving logs from the renderer
73
+ recieveFromRenderer: true
74
+ }
75
+ )
76
+ // later/elsewhere
77
+ // NOTE that the namespaces MUST begin with `main:`
78
+ useElectronLogger().debug({ns:"main:hello"}) // will work
79
+
80
+ ```
81
+ We can setup the apis for two way communication in the preload script.
82
+
83
+ ```ts
84
+ // preload.ts
85
+ import { contextBridge, ipcRenderer } from "electron"
86
+ import { createElectronLoggerApi, type ElectronLoggerApi } from "@witchcraft/nuxt-logger/electron"
87
+
88
+ declare global {
89
+ interface Window {
90
+ electron: {
91
+ api: ElectronLoggerApi {
92
+ //...
93
+ }
94
+ }
95
+ }
96
+ }
97
+ // creates window.electron.api.log/_logs
98
+ // note the naming/structure must be like this, it's currently hardcoded
99
+ contextBridge.exposeInMainWorld("electron", {
100
+ api: {
101
+ ...createElectronLoggerApi(ipcRenderer)
102
+ },
103
+ meta: {
104
+ env {
105
+ // the regular useLogger in the renderer will look for this variable automatically
106
+ LOG_LEVEL: process.env.LOG_LEVEL,
107
+ WRITE_LEVEL: process.env.LOG_WRITE_LEVEL,
108
+ }
109
+ }
110
+ })
111
+ ```
112
+
113
+ And receive `main`'s logs on the client. This must be called before other calls to `useLogger`.
114
+
115
+ ```vue
116
+ // app.vue
117
+
118
+ <script lang="ts" setup>
119
+ // setup the regular client side logger
120
+ useLogger({
121
+ // this is the default
122
+ redirectTo: "electron",
123
+ // prefixes all messages with `renderer:` so that they will be forwarded, can be a function
124
+ redirectAll: true,
125
+ })
126
+ setupElectronMainToRendererLogging()
127
+
128
+ useLogger().debug({ns: "renderer:hello"}) // will get forwarder
129
+ useLogger().debug({ns: "hello"}) // will also get forwarded if redirectAll is true
130
+ </script>
131
+ ```
132
+
133
+
134
+ <!-- Badges -->
135
+ [npm-version-src]: https://img.shields.io/npm/v/@witchcraft/nuxt-logger/latest.svg?style=flat&colorA=020420&colorB=00DC82
136
+ [npm-version-href]: https://npmjs.com/package/@witchcraft/nuxt-logger
137
+
138
+ [npm-downloads-src]: https://img.shields.io/npm/dm/@witchcraft/nuxt-logger.svg?style=flat&colorA=020420&colorB=00DC82
139
+ [npm-downloads-href]: https://npmjs.com/package/@witchcraft/nuxt-logger
140
+
141
+ [license-src]: https://img.shields.io/npm/l/@witchcraft/nuxt-logger.svg?style=flat&colorA=020420&colorB=00DC82
142
+ [license-href]: https://npmjs.com/package/@witchcraft/nuxt-logger
143
+
144
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
145
+ [nuxt-href]: https://nuxt.com
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@witchcraft/nuxt-logger",
3
+ "version": "0.0.1",
4
+ "description": "Nuxt module for logging.",
5
+ "repository": "witchcraftjs/nuxt-logger",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types.d.mts",
12
+ "import": "./dist/module.mjs"
13
+ },
14
+ "./*": {
15
+ "types": "./dist/runtime/*.d.ts",
16
+ "import": "./dist/runtime/*.js"
17
+ },
18
+ "./electron": {
19
+ "types": "./dist/runtime/electron/index.d.ts",
20
+ "import": "./dist/runtime/electron/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "src"
26
+ ],
27
+ "scripts": {
28
+ "prepare": "husky && cd playground && pnpm i",
29
+ "build": "nuxt-module-build prepare && nuxt-module-build build && nuxi build playground",
30
+ "build:only": "nuxt-module-build build",
31
+ "dev": "nuxi dev playground",
32
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
33
+ "lint": "eslint \"{src,test,playground/app}/**/*.{ts,vue}\" \"*.{js,cjs,mjs,ts}\"",
34
+ "lint:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest watch"
37
+ },
38
+ "//peerDependencies": "Because we need the real paths to the workers, they must both be in the app's node_modules",
39
+ "peerDependencies": {
40
+ "pino": "^9.8.0",
41
+ "pino-pretty": "^11.2.2"
42
+ },
43
+ "dependencies": {
44
+ "@alanscodelog/tsconfigs": "^6.1.0",
45
+ "@alanscodelog/utils": "^6.0.1",
46
+ "@nuxt/kit": "^4.0.3",
47
+ "defu": "latest",
48
+ "pino": "^9.9.0",
49
+ "pino-pretty": "^11.3.0"
50
+ },
51
+ "devDependencies": {
52
+ "@alanscodelog/eslint-config": "^6.3.0",
53
+ "@alanscodelog/semantic-release-config": "^5.0.4",
54
+ "@nuxt/eslint-config": "^1.9.0",
55
+ "@nuxt/module-builder": "^1.0.2",
56
+ "@nuxt/schema": "^4.0.3",
57
+ "@nuxt/test-utils": "^3.19.2",
58
+ "@types/node": "latest",
59
+ "changelogen": "^0.6.2",
60
+ "electron": "^34.5.8",
61
+ "eslint": "^9.34.0",
62
+ "husky": "^9.1.7",
63
+ "nuxt": "^4.0.3",
64
+ "semantic-release": "^24.2.8",
65
+ "typescript": "^5.9.2",
66
+ "vitest": "^3.2.4",
67
+ "vue-tsc": "^2.2.12"
68
+ },
69
+ "release": {
70
+ "extends": [
71
+ "@alanscodelog/semantic-release-config"
72
+ ]
73
+ },
74
+ "commitlint": {
75
+ "extends": [
76
+ "@alanscodelog"
77
+ ]
78
+ },
79
+ "publishConfig": {
80
+ "access": "public"
81
+ }
82
+ }
package/src/module.ts ADDED
@@ -0,0 +1,173 @@
1
+ import {
2
+ addImportsDir,
3
+ addPlugin,
4
+ addServerHandler,
5
+ addServerImportsDir,
6
+ createResolver,
7
+ defineNuxtModule,
8
+ resolvePath
9
+ } from "@nuxt/kit"
10
+ import { defu } from "defu"
11
+ import fs from "node:fs/promises"
12
+ import path from "node:path"
13
+ import type { TransportMultiOptions } from "pino"
14
+
15
+ declare module "@nuxt/schema" {
16
+ interface PublicRuntimeConfig {
17
+ logger: Pick<ModuleOptions, "redact" | "additionalServerTransportTargets" | "disabledServerTransportTargets"> & {
18
+ logPath: string
19
+ }
20
+ }
21
+ }
22
+
23
+ export interface ModuleOptions {
24
+ /**
25
+ * See {@link https://getpino.io/#/docs/redaction?id=redaction Pino redaction options}.
26
+ */
27
+ redact: string[]
28
+ /** This is "/var/log/testing/${appName}.log". */
29
+ serverLogPath?: string
30
+ /** This is "~~/logs/server.log" by default. */
31
+ devServerLogPath?: string
32
+ /** The module tries to get the app name from `runtimeConfig.appInfo.name`, otherwise it uses the name of the `~~` directory by default. */
33
+ appName?: string
34
+ /** @default false */
35
+ enableServerRequestLogging?: boolean
36
+ /**
37
+ * Additional transport targets to add to the server transport.
38
+ * File and console + pino-pretty are added by default. See disabledServerTransportTargets for disabling a transport.
39
+ *
40
+ * Since some transports will require options, but passing them via the nuxt config will hard code them, there is an additional `_loadFromEnv` property that can list a list of dot seperated properties to load from `process.env` and `_contexts` to set what context it will run in.
41
+ *
42
+ * For example, to add `pino-loki`:
43
+ * ```ts
44
+ * additionalServerTransportTargets: [
45
+ * {
46
+ * target: "pino-loki",
47
+ * _contexts: ["server"], // can be any of ["server", "client", "electron-client", "electron-main"] (all by default)
48
+ * _loadFromEnv: ["options.host", "options.basicAuth.username", "options.basicAuth.password"],
49
+ * options: {
50
+ * //...
51
+ * labels: {
52
+ * // set additional labels
53
+ * application: "...",
54
+ * },
55
+ * propsToLabels: ["level", ...don't forget to set your labels ],
56
+ * replaceTimestamp: true, // loki requires timestamps in a specific format
57
+ * host: "PINO_LOKI_HOST",
58
+ * basicAuth: {
59
+ * // don't forget the quotes!
60
+ * username: "PINO_LOKI_BASIC_AUTH_USERNAME",
61
+ * password: "PINO_LOKI_BASIC_AUTH_PASSWORD",
62
+ * },
63
+ * },
64
+ * },
65
+ * ]
66
+ * ```
67
+ * The location for the auth part on grafana cloud is confusing as fuck, leaving this here for posterity.
68
+ *
69
+ * Go to https://grafana.com/orgs/YOURORGHERE/stacks => Details => Loki => Send Logs
70
+ *
71
+ * There you can get the user and create a token with write access and it sets the right stuff. It will then appear in https://YOURORGHERE.grafana.net/a/grafana-auth-app
72
+ *
73
+ * If nothing is erroring, you should be able to see grafana receiving logs in Drilldown => Logs => Logs (https://YOURORGHERE.grafana.net/drilldown).
74
+ *
75
+ * You can then create a dashboard based on your labels.
76
+ */
77
+ additionalServerTransportTargets?: (TransportMultiOptions["targets"][number] & {
78
+ _contexts?: ("server" | "client" | "electron-client" | "electron-main")[]
79
+ _loadFromEnv: string[]
80
+ })[]
81
+ /** Disable a transport target by it's target name. */
82
+ disabledServerTransportTargets?: string[]
83
+ }
84
+
85
+ export default defineNuxtModule<ModuleOptions>({
86
+ meta: {
87
+ name: "logger",
88
+ configKey: "logger"
89
+ },
90
+ defaults: {
91
+ redact: [
92
+ ...(process.env.NODE_ENV === "development" ? [] : ["redact"]),
93
+ `req.headers["sec-ch-ua"]`,
94
+ `req.headers.cookie`,
95
+ `req.headers["user-agent"]`,
96
+ `req.headers.authorization`,
97
+ `req.headers.referer`,
98
+ `req.headers["x-forwarded-for"]`,
99
+ `req.headers["set-cookie"]`,
100
+ // i don't think this is actually sensitive, but just in case
101
+ `req["sec-websocket-key"]`
102
+ ] as string[],
103
+ enableServerRequestLogging: false,
104
+ additionalServerTransportTargets: [],
105
+ disabledServerTransportTargets: []
106
+ },
107
+ async setup(options, nuxt) {
108
+ const { resolve } = createResolver(import.meta.url)
109
+ options.devServerLogPath ??= await resolvePath("~~/logs/server.log", { alias: nuxt.options.alias })
110
+ const maybeAppName = (nuxt.options.runtimeConfig.public as any)?.appInfo?.name
111
+ const appName = options.appName ?? maybeAppName ?? path.basename(await resolvePath("~~", { alias: nuxt.options.alias }))
112
+ options.serverLogPath ??= `/var/log/testing/${appName}.log`
113
+ if (process.env.NODE_ENV === "development") {
114
+ if (!await fs.stat(options.devServerLogPath).then(() => true).catch(() => false)) {
115
+ await fs.mkdir(path.dirname(options.devServerLogPath), {
116
+ recursive: true
117
+ })
118
+ await fs.writeFile(options.devServerLogPath, "")
119
+ }
120
+ } else {
121
+ if (!await fs.stat(options.serverLogPath).then(() => true).catch(() => false)) {
122
+ await fs.mkdir(path.dirname(options.serverLogPath), { recursive: true })
123
+ await fs.writeFile(options.serverLogPath, "")
124
+ }
125
+ }
126
+
127
+ nuxt.options.runtimeConfig.public.logger = defu(
128
+ nuxt.options.runtimeConfig.public.logger as any,
129
+ {
130
+ redact: options.redact,
131
+ logPath: nuxt.options.dev ? options.devServerLogPath : options.serverLogPath,
132
+ additionalServerTransportTargets: options.additionalServerTransportTargets,
133
+ disabledServerTransportTargets: options.disabledServerTransportTargets
134
+ }
135
+ )
136
+ if (nuxt.options.modules.includes("@witchcraft/nuxt-electron")) {
137
+ const opts = nuxt.options as any
138
+ opts.electron ??= {}
139
+ opts.electron.electronOnlyRuntimeConfig ??= {}
140
+ opts.electron.electronOnlyRuntimeConfig.logger ??= {}
141
+ opts.electron.electronOnlyRuntimeConfig.logger.logPath = undefined
142
+ }
143
+ // the server files require transpilation to use #imports and #logger
144
+ // they are needed, otherwise I can't seem to get the imports to resolve correctly
145
+ // see also runtime/shared/createUseLogger.ts
146
+ for (const file of [
147
+ "./runtime",
148
+ "./runtime/server/middleware/log",
149
+ "./runtime/server/utils/useServerLogger"
150
+ ]) {
151
+ nuxt.options.build.transpile.push(resolve(file))
152
+ }
153
+
154
+ addImportsDir(resolve("./runtime/composables"))
155
+ addServerImportsDir(resolve("./runtime/server/utils"))
156
+
157
+ if (options.enableServerRequestLogging) {
158
+ addServerHandler({
159
+ middleware: true,
160
+ handler: resolve("./runtime/server/middleware/log")
161
+ })
162
+ }
163
+ addPlugin(resolve("./runtime/plugins/init"))
164
+
165
+ nuxt.options.alias["#logger"] = resolve("./runtime")
166
+ nuxt.hook("vite:extendConfig", config => {
167
+ // fixes https://github.com/nuxt/nuxt/issues/13247 partially (also transpiling it breaks it)
168
+ config.optimizeDeps ??= {}
169
+ config.optimizeDeps.include ??= []
170
+ config.optimizeDeps.include.push("pino")
171
+ })
172
+ }
173
+ })
@@ -0,0 +1,12 @@
1
+ import { useLogger } from "./useLogger.js"
2
+
3
+ // todo see about importing the composable from nuxt-electron
4
+ const isElectronClient = (typeof window !== "undefined" && "electron" in window && window.electron)
5
+
6
+ export function setupElectronMainToRendererLogging(): void {
7
+ if (!isElectronClient) return
8
+ const logger = useLogger()
9
+ ;(window as any).electron?.api._logs.on((level: string, data: any) => {
10
+ (logger as any)[level](data)
11
+ })
12
+ }
@@ -0,0 +1,5 @@
1
+ import { useRuntimeConfig } from "#app"
2
+
3
+ import { createUseLogger } from "../shared/createUseLogger.js"
4
+
5
+ export const useLogger = createUseLogger(useRuntimeConfig)
@@ -0,0 +1,23 @@
1
+ import type { ipcRenderer as IpcRenderer } from "electron"
2
+
3
+ import { ELECTRON_LOG_TYPE } from "./types.js"
4
+
5
+ import type { Levels } from "../types.js"
6
+
7
+ export type ElectronLoggerApi = {
8
+ log: (level: Levels, data: any) => void
9
+ _logs: { on(cb: (level: Levels, data: any) => void): void }
10
+ }
11
+
12
+ export function createElectronLoggerApi(ipcRenderer: typeof IpcRenderer): ElectronLoggerApi {
13
+ return {
14
+ log: (level: Levels, data: any) => ipcRenderer.send(ELECTRON_LOG_TYPE.TO_MAIN, { level, data }),
15
+ _logs: {
16
+ on(cb: (level: Levels, data: any) => void) {
17
+ ipcRenderer.on(ELECTRON_LOG_TYPE.FROM_MAIN_ON_RENDERER, (_event, level, data) => {
18
+ cb(level, data)
19
+ })
20
+ }
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,6 @@
1
+ // note adding file endings breaks build types
2
+ export { createElectronLoggerApi, type ElectronLoggerApi } from "./createElectronLoggerApi"
3
+
4
+ export { useElectronLogger } from "./useElectronLogger"
5
+ export { wrapLoggerForElectron } from "./wrapLoggerForElectron"
6
+ export * from "./types"
@@ -0,0 +1,12 @@
1
+ import { ipcMain } from "electron"
2
+
3
+ import { ELECTRON_LOG_TYPE } from "./types.js"
4
+ import type { useElectronLogger } from "./useElectronLogger.js"
5
+
6
+ export function receiveFromRendererOnMain(logger: ReturnType<typeof useElectronLogger>): void {
7
+ ipcMain.on(ELECTRON_LOG_TYPE.TO_MAIN, (_event, message) => {
8
+ if (typeof message.data === "object" && typeof message.level === "string") {
9
+ logger[message.level as keyof typeof logger](message.data)
10
+ }
11
+ })
12
+ }
@@ -0,0 +1,9 @@
1
+ import type { EnumLike } from "@alanscodelog/utils"
2
+ import { enumFromArray } from "@alanscodelog/utils/enumFromArray"
3
+
4
+ export const ELECTRON_LOG_TYPE = enumFromArray([
5
+ "FROM_MAIN_ON_RENDERER",
6
+ "TO_MAIN"
7
+ ], "ELECTRON_LOG_TYPE.")
8
+
9
+ export type ElectronLogType = EnumLike<typeof ELECTRON_LOG_TYPE>
@@ -0,0 +1,98 @@
1
+ import { keys } from "@alanscodelog/utils/keys"
2
+ import type { PublicRuntimeConfig } from "@nuxt/schema"
3
+ import { app, type BrowserWindow } from "electron"
4
+ import path from "node:path"
5
+ import { type Logger, pino } from "pino"
6
+
7
+ import { receiveFromRendererOnMain } from "./receiveFromRendererOnMain.js"
8
+ import { ELECTRON_LOG_TYPE } from "./types.js"
9
+
10
+ import { getBaseOptions } from "../helpers/getBaseOptions.js"
11
+
12
+ declare const globalThis: any
13
+ globalThis.__bundlerPathsOverrides = {
14
+ "pino-worker": `./node_modules/pino/lib/worker.js`,
15
+ "pino-worker-pipeline": `./node_modules/pino/lib/worker-pipeline.js`,
16
+ "thread-stream-worker": `./node_modules/thread-stream/lib/worker.js`,
17
+ "pino/file": `./node_modules/pino/file.js`,
18
+ "pino-pretty": `./node_modules/pino-pretty/index.js`
19
+ }
20
+ const bundlerOverrides = globalThis.__bundlerPathsOverrides
21
+
22
+ const appPath = app.getAppPath()
23
+ for (const key of keys(bundlerOverrides)) {
24
+ bundlerOverrides[key] = path.resolve(appPath, bundlerOverrides[key])
25
+ }
26
+
27
+ let initiated = false
28
+ const levels: (keyof Logger)[] = ["error", "warn", "debug", "trace", "info", "fatal"] as const
29
+ const logger: Record<typeof levels[number], ((data: any) => void)> = {} as any
30
+
31
+ /**
32
+ * A simplified wrapper around pino for use in electron main. Note that it does not support child logger and is actually a simple wrapper around just the basic levels.
33
+ *
34
+ * This is in part because pino might get swapped out (see @{link usePino}, but also to allow us to forward messages to the renderer easily because otherwise to do so we would have to use a custom pino transport that then communicates back again with the main process to send messages to the correct window which is a ridiculous round trip for no benefit.
35
+ *
36
+ * This is seperate since it requires special handling because of the following:
37
+ * - We can't use useRuntimeConfig so it's injected by using vite define instead ({@link STATIC}).
38
+ * - We need to pass the logPath which is defined at runtime.
39
+ * - We need to use some electron only imports which causes problems for the regular server.
40
+ * - Pino needs to be told were to find it's workers electron side in prod.
41
+ * {@link https://github.com/pinojs/pino/blob/master/docs/bundling.md}
42
+ * Using vite's ?worker&url imports almost works (the paths are correct) but something about the transpiling breaks things I think.
43
+ */
44
+
45
+ export function useElectronLogger(
46
+ pinoConfig?: PublicRuntimeConfig["logger"],
47
+ windows?: () => BrowserWindow[], {
48
+ receiveFromRenderer = true
49
+ }: { receiveFromRenderer?: boolean } = {}
50
+ ) {
51
+ if (initiated && pinoConfig) {
52
+ throw new Error("Config can only be passed the first time when initiating.")
53
+ }
54
+ // doing initiated check because we need to be able to pass config from electron
55
+ if (!initiated) {
56
+ if (!pinoConfig) {
57
+ throw new Error("Config must be passed the first time using useElectronLogger.")
58
+ }
59
+ if (!windows) {
60
+ throw new Error("windows getter must be passed the first time using useElectronLogger.")
61
+ }
62
+ initiated = true
63
+
64
+ const { opts, transports, debug } = getBaseOptions(pinoConfig)
65
+
66
+ const pinoLogger = pino(
67
+ opts,
68
+ pino.transport(transports)
69
+ )
70
+
71
+ for (const level of levels) {
72
+ // the references are safe
73
+
74
+ logger[level] = (data: any) => {
75
+ if (typeof data === "object") {
76
+ if (data.ns.startsWith("renderer:")) {
77
+ (pinoLogger as any)[level](data)
78
+ return
79
+ } else if (!data.ns.startsWith("main:")) {
80
+ throw new Error(`Namespace on electron's main side must start with "main:", got ${data.ns} instead in log: ${JSON.stringify(data)}.`)
81
+ }
82
+ }
83
+ (pinoLogger as any)[level](data)
84
+ const wins = windows()
85
+ for (const win of wins) {
86
+ win.webContents.send(ELECTRON_LOG_TYPE.FROM_MAIN_ON_RENDERER,
87
+ level, data)
88
+ }
89
+ }
90
+ }
91
+ if (receiveFromRenderer) {
92
+ receiveFromRendererOnMain(logger)
93
+ }
94
+
95
+ logger.debug({ ns: "main:log", ...debug })
96
+ }
97
+ return logger
98
+ }
@@ -0,0 +1,16 @@
1
+ import type { useElectronLogger } from "./useElectronLogger.js"
2
+ /**
3
+ * Wraps a logger (prefixed the first argument's namespace key with "main:") for use in electron.
4
+ */
5
+ export function wrapLoggerForElectron<T extends ReturnType<typeof useElectronLogger>>(
6
+ logger: T,
7
+ prefix: string = "main:"
8
+ ): T {
9
+ const res = {}
10
+ for (const key of Object.keys(logger)) {
11
+ res[key] = (...args: any[]) => {
12
+ logger[key]({ ...args[0], ns: prefix + args[0].ns }, ...args.slice(1))
13
+ }
14
+ }
15
+ return res
16
+ }
@@ -0,0 +1,156 @@
1
+ import { get } from "@alanscodelog/utils/get"
2
+ import { keys } from "@alanscodelog/utils/keys"
3
+ import { readable } from "@alanscodelog/utils/readable"
4
+ import { set } from "@alanscodelog/utils/set"
5
+ import { walk } from "@alanscodelog/utils/walk"
6
+ import type { PublicRuntimeConfig } from "@nuxt/schema"
7
+ import type { LoggerOptions, TransportMultiOptions } from "pino"
8
+ import { pino } from "pino"
9
+
10
+ // normally we would access logger.levels, but because we create
11
+ // the shared config here we can't
12
+ const levels = {
13
+ values: {
14
+ fatal: 60,
15
+ error: 50,
16
+ warn: 40,
17
+ info: 30,
18
+ debug: 20,
19
+ trace: 10
20
+ },
21
+ labels: {
22
+ 10: "trace",
23
+ 20: "debug",
24
+ 30: "info",
25
+ 40: "warn",
26
+ 50: "error",
27
+ 60: "fatal"
28
+ }
29
+ }
30
+
31
+ const isElectronClient = (typeof window !== "undefined" && "electron" in window && window.electron)
32
+
33
+ export function getBaseOptions(
34
+ config: PublicRuntimeConfig["logger"]
35
+ ): {
36
+ opts: LoggerOptions
37
+ browserOpts: LoggerOptions
38
+ transports: TransportMultiOptions
39
+ debug: {
40
+ logPath: string
41
+ writeLevel: string
42
+ logLevel: string
43
+ }
44
+ } {
45
+ // @ts-expect-error the path is wrong on electron's client side
46
+ // but it's not used anyways
47
+ if (isElectronClient) { delete config.logPath }
48
+ const {
49
+ redact,
50
+ logPath,
51
+ additionalServerTransportTargets,
52
+ disabledServerTransportTargets
53
+ } = config
54
+ const validLevels = keys(levels.values)
55
+ if (process.env.LOG_LEVEL && !validLevels.includes(process.env.LOG_LEVEL as any)) {
56
+ throw new Error(`logLevel is not a valid level: ${readable(validLevels)}`)
57
+ }
58
+ const maybeElectronLogLevel = import.meta.client
59
+ ? (window as any).electron?.meta?.env?.LOG_LEVEL
60
+ : undefined
61
+ const maybeElectronLogWriteLevel = import.meta.client
62
+ ? (window as any).electron?.meta?.env?.LOG_WRITE_LEVEL
63
+ : undefined
64
+ const writeLevel: (typeof validLevels)[number]
65
+ = maybeElectronLogWriteLevel
66
+ ?? maybeElectronLogLevel
67
+ ?? process.env.LOG_WRITE_LEVEL as any
68
+ ?? process.env.LOG_LEVEL as any
69
+ ?? (import.meta.dev ? "debug" : undefined)
70
+ ?? "warn"
71
+ const logLevel: (typeof validLevels)[number]
72
+ = maybeElectronLogWriteLevel
73
+ ?? process.env.LOG_LEVEL as any
74
+ ?? (import.meta.dev ? "debug" : undefined)
75
+ ?? "debug"
76
+
77
+ const processedAdditionalServerTransportTargets = []
78
+ const thisContexts = [
79
+ ...(isElectronClient ? ["electron-client"] : []),
80
+ // might not be available, careful
81
+ ...((import.meta as any)?.electron ? ["electron-main"] : []),
82
+ ...(import.meta.client ? ["client"] : []),
83
+ ...(import.meta.server ? ["server"] : [])
84
+ ]
85
+ for (const target of additionalServerTransportTargets ?? []) {
86
+ const contexts = (target as any)._contexts as string[]
87
+ const contextMatches = contexts === undefined || contexts.find(c => thisContexts.includes(c))
88
+ if (!contextMatches) continue
89
+
90
+ const propPaths = (target as any)._loadFromEnv as string[]
91
+ // we have to clone because we can't write to runtimeConfig
92
+ const clone = walk(target, undefined, { save: true })
93
+ processedAdditionalServerTransportTargets.push(clone)
94
+ for (const propPath of propPaths) {
95
+ const splitPath = propPath.split(".")
96
+ if (propPath) {
97
+ set(clone, splitPath, process.env[get(clone, splitPath)])
98
+ }
99
+ }
100
+ delete (clone as any)._loadFromEnv
101
+ }
102
+
103
+ return {
104
+ debug: {
105
+ logPath,
106
+ writeLevel,
107
+ logLevel
108
+ },
109
+ opts: {
110
+ redact,
111
+ // do not under any circumstances change how level looks, it breaks the logging ??? :/
112
+ level: logLevel,
113
+ formatters: {
114
+ bindings: () => ({})
115
+ },
116
+ timestamp: pino.stdTimeFunctions.isoTime
117
+ },
118
+ browserOpts: {
119
+ browser: {
120
+ asObject: false,
121
+ transmit: isElectronClient
122
+ ? {
123
+ level: logLevel,
124
+ send: (level, logEvent) => {
125
+ if (!logEvent.messages[0]?.ns?.startsWith("main") && levels.values[level] < levels.values[writeLevel]) {
126
+ logEvent.messages[0].ns &&= `renderer:${logEvent.messages[0].ns}`
127
+ ;(window as any).electron.api.log(logEvent)
128
+ }
129
+ }
130
+ }
131
+ : undefined
132
+ }
133
+ },
134
+ transports: {
135
+ targets: [
136
+ {
137
+ level: writeLevel,
138
+ target: "pino/file",
139
+ options: {
140
+ destination: logPath,
141
+ mkdir: true
142
+ }
143
+ },
144
+ {
145
+ level: logLevel,
146
+ target: "pino-pretty",
147
+ options: {
148
+ colorize: true
149
+ }
150
+ },
151
+ ...(processedAdditionalServerTransportTargets ?? [])
152
+ // not sure when target doesn't exist, #future investigate
153
+ ].filter(t => !disabledServerTransportTargets?.includes((t as any).target))
154
+ }
155
+ }
156
+ }
@@ -0,0 +1,25 @@
1
+ import { defineNuxtPlugin, useRuntimeConfig } from "#imports"
2
+
3
+ import { useLogger } from "../composables/useLogger"
4
+
5
+ export default defineNuxtPlugin({
6
+ name: "client-init-log",
7
+ setup(nuxtApp) {
8
+ if (import.meta.client) {
9
+ nuxtApp.hook("app:created", () => {
10
+ const config = useRuntimeConfig().public
11
+ const initialLog: any = { appInfo: config.appInfo }
12
+ if (window.navigator !== undefined) {
13
+ initialLog.environmentInfo = {
14
+ userAgent: navigator.userAgent
15
+ }
16
+ }
17
+ useLogger().info({ ns: "client:init", ...initialLog })
18
+ })
19
+ }
20
+ },
21
+ env: {
22
+ // don't run when rendering server-only or island components.
23
+ islands: false
24
+ }
25
+ })
@@ -0,0 +1,13 @@
1
+ import { defineEventHandler, useServerLogger } from "#imports"
2
+
3
+ export default defineEventHandler(event => {
4
+ const logger = useServerLogger()
5
+ logger.trace({
6
+ ns: "server:req:all",
7
+ req: {
8
+ headers: event.node.req.headers,
9
+ method: event.node.req.method,
10
+ url: event.node.req.url
11
+ }
12
+ })
13
+ })
@@ -0,0 +1,23 @@
1
+ import os from "node:os"
2
+
3
+ import { defineNitroPlugin, useRuntimeConfig, useServerLogger } from "#imports"
4
+
5
+ export default defineNitroPlugin(_nitroApp => {
6
+ const cpus = os.cpus()
7
+ const environmentInfo = {
8
+ os: os.platform(),
9
+ osType: os.type(),
10
+ osVersion: os.release(),
11
+ arch: os.arch(),
12
+ nodeVersion: process.version,
13
+ cpu: cpus?.[0]?.model,
14
+ cpus: cpus.length,
15
+ totalMem: os.totalmem()
16
+ }
17
+ useServerLogger().info({
18
+ ns: "server:init",
19
+ appInfo: useRuntimeConfig().public,
20
+ environmentInfo,
21
+ isUsingTestingDb: process.env.LOCALSERVERDB
22
+ })
23
+ })
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json",
3
+ }
@@ -0,0 +1,5 @@
1
+ import { useRuntimeConfig } from "#imports"
2
+
3
+ import { createUseLogger } from "../../shared/createUseLogger.js"
4
+
5
+ export const useServerLogger = createUseLogger(useRuntimeConfig)
@@ -0,0 +1,74 @@
1
+ import type { RuntimeConfig } from "nuxt/schema"
2
+ import { type Logger, pino } from "pino"
3
+
4
+ import { getBaseOptions } from "../helpers/getBaseOptions.js"
5
+ import type { Levels } from "../types.js"
6
+
7
+ export type BaseLogger = Record<Levels, ((data: any) => void)> & {
8
+ flush: Logger["flush"]
9
+ }
10
+ const logger: BaseLogger = {} as any
11
+
12
+ /**
13
+ * This exists to allow creating useLogger both on the client and the server since for the module to work on the server we must import from `#imports` and have the module transpile the files, but this is not neccesary from the composable.
14
+ *
15
+ * So they both create useLogger by passing in useRuntimeConfig how they can.
16
+ */
17
+ export function createUseLogger(
18
+ rc: () => RuntimeConfig
19
+ ) {
20
+ let initiated = false
21
+ return function useLogger({
22
+ redirectTo,
23
+ redirectAll = true
24
+ }: {
25
+ redirectTo?: "electron"
26
+ /**
27
+ * Prefixes all messages not coming from main with `renderer:` so that they will be forwarded.
28
+ *
29
+ * Can also be a function to filter what gets forwarded.
30
+ */
31
+ redirectAll?: boolean | ((level: keyof Logger, data: any) => boolean)
32
+ } = {}) {
33
+ const levels: (Levels)[] = ["error", "warn", "debug", "trace", "info", "fatal"] as const
34
+ // there's a bit of a problem calling useRuntimeConfig/useNuxtApp outside the function
35
+ // so doing initiated check instead
36
+ if (!initiated) {
37
+ initiated = true
38
+
39
+ const config = rc().public
40
+ const { opts, transports, browserOpts, debug } = getBaseOptions(config.logger)
41
+ let transport: any
42
+ if (import.meta.server) {
43
+ transport = pino.transport(transports)
44
+ }
45
+ const pinoLogger = pino(
46
+ {
47
+ ...opts,
48
+ ...(import.meta.client
49
+ ? browserOpts
50
+ : {})
51
+ },
52
+ transport
53
+ )
54
+ for (const level of levels) {
55
+ logger[level] = (data: any) => {
56
+ if (redirectTo === "electron") {
57
+ if (typeof data === "object" && typeof data.ns === "string" && !data.ns.startsWith("main:")) {
58
+ if (data.ns.startsWith("renderer:")) {
59
+ (window as any).electron?.api.log(level, data)
60
+ } else if (typeof redirectAll === "function" ? redirectAll(level, data) : redirectAll) {
61
+ data.ns = `renderer:${data.ns}`
62
+ ;(window as any).electron?.api.log(level, data)
63
+ }
64
+ }
65
+ }
66
+ (pinoLogger as any)[level](data)
67
+ }
68
+ }
69
+ logger.debug({ ns: "log", ...debug, enabledTransportTargets: transports.targets.map(t => (t as any)?.target) })
70
+ return logger
71
+ }
72
+ return logger
73
+ }
74
+ }
@@ -0,0 +1 @@
1
+ export type Levels = "error" | "warn" | "debug" | "trace" | "info" | "fatal"