diff --git a/.changeset/hip-years-bake.md b/.changeset/hip-years-bake.md new file mode 100644 index 00000000..a39f08b6 --- /dev/null +++ b/.changeset/hip-years-bake.md @@ -0,0 +1,16 @@ +--- +"zemble-plugin-auth-otp": patch +"zemble-plugin-auth": patch +"zemble-plugin-cms-users": patch +"zemble-plugin-cms": patch +"@zemble/core": patch +"create-zemble-app": patch +"create-zemble-plugin": patch +"@zemble/graphql": patch +"zemble-plugin-kv": patch +"zemble-plugin-logger-graphql": patch +"minimal": patch +"supplement-stack": patch +--- + +Streamline logging diff --git a/.eslintrc b/.eslintrc index 13a92046..0fa4a8d8 100644 --- a/.eslintrc +++ b/.eslintrc @@ -3,7 +3,7 @@ "eslint-config-kingstinct/react-native" ], "rules": { - "no-console": 1, + "no-console": 2, "@typescript-eslint/sort-type-union-intersection-members": 0, "@typescript-eslint/no-unused-vars": 1, "import/no-unresolved": 0, diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 459532b5..aaa2e0c1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,17 +13,14 @@ on: bun-version: description: 'Bun Version' required: false - default: 'latest' type: string os: description: 'Operating System' required: false - default: 'ubuntu-latest' type: string mongo-version: description: 'MongoDB Version' required: false - default: '7.0.4' type: string workflow_dispatch: inputs: @@ -53,16 +50,18 @@ on: - "**" # matches every branch - "!main" # excludes the master branch +env: + BUN_VERSION: ${{ inputs.bun-version || 'latest' }} + MONGOMS_PREFER_GLOBAL_PATH: true + MONGOMS_VERSION: ${{ inputs.mongo-version || '7.0.4' }} + RUNS_ON: ${{ inputs.os || 'ubuntu-latest' }} + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: test: # The type of runner that the job will run on runs-on: ${{ inputs.os || 'ubuntu-latest' }} timeout-minutes: 10 - env: - MONGOMS_PREFER_GLOBAL_PATH: true - MONGOMS_VERSION: ${{ inputs.mongo-version || '7.0.4' }} - BUN_VERSION: ${{ inputs.bun-version || 'latest' }} # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -73,7 +72,7 @@ jobs: id: cache-mongodb-binaries name: Cache MongoDB binaries with: - key: ${{ runner.os }}-mongodb-${{ inputs.bun-version }}-${{ env.MONGOMS_VERSION }} + key: ${{ runner.os }}-mongodb-${{ env.BUN_VERSION }}-${{ env.MONGOMS_VERSION }} path: ~/.cache/mongodb-binaries - uses: oven-sh/setup-bun@v1 @@ -94,9 +93,6 @@ jobs: env: SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }} PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }} - MONGOMS_PREFER_GLOBAL_PATH: true - MONGOMS_VERSION: ${{ inputs.mongo-version || '7.0.4' }} - BUN_VERSION: ${{ inputs.bun-version || 'latest' }} timeout-minutes: 10 # disable for now - since it for some reason fails on CI if: false @@ -110,7 +106,7 @@ jobs: id: cache-mongodb-binaries name: Cache MongoDB binaries with: - key: ${{ runner.os }}-mongodb-${{ inputs.bun-version }}-${{ env.MONGOMS_VERSION }} + key: ${{ runner.os }}-mongodb-${{ env.BUN_VERSION }}-${{ env.MONGOMS_VERSION }} path: ~/.cache/mongodb-binaries - uses: oven-sh/setup-bun@v1 @@ -125,7 +121,7 @@ jobs: working-directory: ./packages/supabase/supabase-app - name: Test - run: bun run codegen && bun test + run: bun test working-directory: ./packages/supabase typecheck: @@ -133,7 +129,6 @@ jobs: timeout-minutes: 10 env: MONGOMS_DISABLE_POSTINSTALL: true - BUN_VERSION: ${{ inputs.bun-version || 'latest' }} # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -156,7 +151,6 @@ jobs: timeout-minutes: 10 env: MONGOMS_DISABLE_POSTINSTALL: true - BUN_VERSION: ${{ inputs.bun-version || 'latest' }} # Steps represent a sequence of tasks that will be executed as part of the job steps: @@ -179,7 +173,6 @@ jobs: timeout-minutes: 10 env: MONGOMS_DISABLE_POSTINSTALL: true - BUN_VERSION: ${{ inputs.bun-version || 'latest' }} # Steps represent a sequence of tasks that will be executed as part of the job steps: diff --git a/apps/minimal/app.ts b/apps/minimal/app.ts index 2526da55..2598fad1 100644 --- a/apps/minimal/app.ts +++ b/apps/minimal/app.ts @@ -2,13 +2,17 @@ import { createApp } from '@zemble/core' import GraphQL from '@zemble/graphql' import Migrations from '@zemble/migrations' import dryrunAdapter from '@zemble/migrations/adapters/dryrun' +import Logger from '@zemble/pino' import Routes from '@zemble/routes' +import GraphQLLogger from 'zemble-plugin-logger-graphql' import MyRoutes from './plugins/files/plugin' export default createApp({ plugins: [ Routes.configure(), + Logger.configure(), + GraphQLLogger, GraphQL.configure({ }), MyRoutes.configure(), Migrations.configure({ diff --git a/apps/minimal/graphql/Subscription/randomNumber.ts b/apps/minimal/graphql/Subscription/randomNumber.ts index 2959ba0b..ee69c39e 100644 --- a/apps/minimal/graphql/Subscription/randomNumber.ts +++ b/apps/minimal/graphql/Subscription/randomNumber.ts @@ -2,14 +2,11 @@ import type { SubscriptionResolvers } from '../schema.generated' const randomNumber: SubscriptionResolvers['randomNumber'] = { // subscribe to the randomNumber event - subscribe: (_, __, { pubsub }) => { - console.log('subscribing to randomNumber') + subscribe: (_, __, { pubsub, logger }) => { + logger.info('subscribing to randomNumber') return pubsub.subscribe('randomNumber') }, - resolve: (payload: number) => { - console.log('resolving randomNumber', payload) - return payload - }, + resolve: (payload: number) => payload, } export default randomNumber diff --git a/apps/minimal/graphql/Subscription/tick.ts b/apps/minimal/graphql/Subscription/tick.ts index ce7e549d..c1bed659 100644 --- a/apps/minimal/graphql/Subscription/tick.ts +++ b/apps/minimal/graphql/Subscription/tick.ts @@ -11,9 +11,9 @@ const initializeOnce = (pubsub: Zemble.PubSubType) => { const tick: SubscriptionResolvers['tick'] = { // subscribe to the tick event - subscribe: (_, __, { pubsub }) => { + subscribe: (_, __, { pubsub, logger }) => { initializeOnce(pubsub) - console.log('subscribing to tick') + logger.info('subscribing to tick') return pubsub.subscribe('tick') }, resolve: (payload: number) => payload, diff --git a/apps/minimal/package.json b/apps/minimal/package.json index 4975008e..d2ff531d 100644 --- a/apps/minimal/package.json +++ b/apps/minimal/package.json @@ -41,7 +41,9 @@ "@zemble/migrations": "workspace:*", "@zemble/bun": "workspace:*", "zemble-plugin-auth": "workspace:*", - "@zemble/routes": "workspace:*" + "@zemble/routes": "workspace:*", + "@zemble/pino": "workspace:*", + "zemble-plugin-logger-graphql": "workspace:*" }, "devDependencies": { "@tsconfig/node20": "^20.1.2", diff --git a/apps/supplement-stack/app.ts b/apps/supplement-stack/app.ts index 2a30ce54..d0f2facb 100644 --- a/apps/supplement-stack/app.ts +++ b/apps/supplement-stack/app.ts @@ -1,4 +1,5 @@ import bunRunner from '@zemble/bun' +import zembleContext from '@zemble/core/zembleContext' import YogaGraphQL from '@zemble/graphql' import AuthOTP from 'zemble-plugin-auth-otp' @@ -49,4 +50,4 @@ void bunRunner({ ], }) -void connect() +void connect({ logger: zembleContext.logger }) diff --git a/apps/supplement-stack/clients/papr.ts b/apps/supplement-stack/clients/papr.ts index a3da44fe..f4a03d86 100644 --- a/apps/supplement-stack/clients/papr.ts +++ b/apps/supplement-stack/clients/papr.ts @@ -1,30 +1,31 @@ -import zembleContext from '@zemble/core/zembleContext' import { MongoClient } from 'mongodb' import Papr from 'papr' +import type { IStandardLogger } from '@zemble/core' + // eslint-disable-next-line import/no-mutable-exports export let client: MongoClient | undefined const papr = new Papr() -export async function connect() { +export async function connect({ logger }: {readonly logger: IStandardLogger}) { const mongoUrl = process.env.MONGO_URL if (!mongoUrl) throw new Error('MONGO_URL not set') - zembleContext.logger.log('Connecting to MongoDB...', mongoUrl) + logger.info('Connecting to MongoDB...', mongoUrl) client = await MongoClient.connect(mongoUrl) - zembleContext.logger.log('Connected to MongoDB!') + logger.info('Connected to MongoDB!') const db = client.db() papr.initialize(db) - zembleContext.logger.log(`Registering ${papr.models.size} models...`) + logger.info(`Registering ${papr.models.size} models...`) papr.models.forEach((model) => { - zembleContext.logger.log(`Registering model: ${model.collection.collectionName}`) + logger.info(`Registering model: ${model.collection.collectionName}`) }) await papr.updateSchemas() diff --git a/apps/supplement-stack/graphql/Mutation/addIngredient.ts b/apps/supplement-stack/graphql/Mutation/addIngredient.ts index bad875de..8cfc2367 100644 --- a/apps/supplement-stack/graphql/Mutation/addIngredient.ts +++ b/apps/supplement-stack/graphql/Mutation/addIngredient.ts @@ -10,7 +10,7 @@ import type { WithoutId } from 'mongodb' const addIngredient: MutationResolvers['addIngredient'] = async (parent, { title, imageUrls, nutrientsPer100g, servingSizes, ingredientId, -}) => { +}, { logger }) => { const _id = ingredientId ? new ObjectId(ingredientId) : new ObjectId() const ingredient: WithoutId = { __typename: 'Ingredient', @@ -23,7 +23,7 @@ const addIngredient: MutationResolvers['addIngredient'] = async (parent, { })) ?? [], } - console.log(ingredient) + logger.info(ingredient) const eatable = await Eatables.findOneAndUpdate({ _id }, { $set: ingredient, diff --git a/apps/todo-app-with-auth-backend/queues/hello-world.ts b/apps/todo-app-with-auth-backend/queues/hello-world.ts index 485c009f..033a8610 100644 --- a/apps/todo-app-with-auth-backend/queues/hello-world.ts +++ b/apps/todo-app-with-auth-backend/queues/hello-world.ts @@ -1,5 +1,5 @@ import { ZembleQueue } from 'zemble-plugin-bull' -export default new ZembleQueue((job) => { - console.log(job.data) +export default new ZembleQueue((job, { logger }) => { + logger.info(job.data) }) diff --git a/build.ts b/build.ts index e17de910..523169cc 100644 --- a/build.ts +++ b/build.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { readdirSync, statSync } from 'node:fs' import { join } from 'node:path' diff --git a/bun.lockb b/bun.lockb index e0ebe4ad..9ba3a9f5 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 33c8eab4..4c1017cd 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "postinstall": "cp manual-patches/sofa-api/package.json node_modules/sofa-api/", "pre-push": "bunx turbo run lint test typecheck --continue", "generate-cms-json-schema": "typescript-json-schema ./packages/cms/tsconfig.json CmsConfigFile --include ./packages/cms/types.ts --out ./packages/cms/entities-json-schema.json --required --strictNullChecks --rejectDateType", - "fix-workspace-dependencies": "find . -type f -name 'package.json' -not -path './package.json' -not -path '*/node_modules/*' -exec perl -pi -e 's/\"workspace:[^\"]*\"/\"*\"/g' {} \\;", + "fix-workspace-dependencies": "find . -type f -name 'package.json' -not -path './package.json' -not -path '*/node_modules/*' -exec perl -pi -e 's/\"workspace:.*\"/\"*\"/g' {} \\;", "changeset-release": "bun run fix-workspace-dependencies && bunx changeset publish", "create-changeset": "bunx changeset && bunx changeset version", "reinstall": "rm -rf node_modules/ && find . -name 'node_modules' -type d -prune -exec rm -rf '{}' + && bun install --force", @@ -76,6 +76,7 @@ "eslint-plugin-yml": "^1.11.0", "husky": "^8.0.3", "lint-staged": "^15.2.0", + "pino-pretty": "^10.3.1", "turbo": "^1.11.2", "typescript-json-schema": "^0.62.0" }, diff --git a/packages/auth-otp/plugin.ts b/packages/auth-otp/plugin.ts index f1aee3e0..317cc2f2 100644 --- a/packages/auth-otp/plugin.ts +++ b/packages/auth-otp/plugin.ts @@ -79,7 +79,7 @@ const defaultConfig = { to, }) } else { - logger.log(`handleAuthRequest for ${to.email}`, twoFactorCode) + logger.info(`handleAuthRequest for ${to.email}`, twoFactorCode) } }, } satisfies Partial @@ -95,7 +95,7 @@ const plugin = new Plugin(import.meta.dir, ], defaultConfig, devConfig: { - handleAuthRequest: ({ email }, code, { logger }) => { logger.log(`handleAuthRequest for ${email}`, code) }, + handleAuthRequest: ({ email }, code, { logger }) => { logger.info(`handleAuthRequest for ${email}`, code) }, generateTokenContents, from: { email: 'noreply@zemble.com' }, }, diff --git a/packages/auth/bin/generate-keys.ts b/packages/auth/bin/generate-keys.ts index 9cded399..1abcf893 100755 --- a/packages/auth/bin/generate-keys.ts +++ b/packages/auth/bin/generate-keys.ts @@ -1,4 +1,5 @@ #!/usr/bin/env bun +import zembleContext from '@zemble/core/zembleContext' import * as fs from 'node:fs/promises' import * as path from 'node:path' @@ -15,8 +16,8 @@ await generateKeys().then(async ({ publicKey, privateKey }) => { if (!data.includes('PUBLIC_KEY') && !data.includes('PRIVATE_KEY')) { await fs.appendFile(envPath, `\nPUBLIC_KEY='${publicKey.trim()}'\nPRIVATE_KEY='${privateKey.trim()}'`) - console.log('PUBLIC_KEY and PRIVATE_KEY was appended to your local .env file!') + zembleContext.logger.info('PUBLIC_KEY and PRIVATE_KEY was appended to your local .env file!') } else { - console.log('The "PUBLIC_KEY" and/or "PRIVATE_KEY" already exists in .env file, will not overwrite!') + zembleContext.logger.info('The "PUBLIC_KEY" and/or "PRIVATE_KEY" already exists in .env file, will not overwrite!') } }) diff --git a/packages/bull/ZembleQueueBull.ts b/packages/bull/ZembleQueueBull.ts index f799ac56..e4c49690 100644 --- a/packages/bull/ZembleQueueBull.ts +++ b/packages/bull/ZembleQueueBull.ts @@ -1,5 +1,6 @@ import { Queue, Worker } from 'bullmq' +import type { IStandardLogger } from '@zemble/core' import type { Job, JobsOptions, QueueOptions, RedisOptions, RepeatOptions, } from 'bullmq' @@ -20,13 +21,19 @@ export type ZembleQueueConfig = { readonly defaultJobOptions?: JobsOptions } +export type ZembleJobOpts = { + readonly logger: IStandardLogger +} + +export type ZembleWorker = (job: Job, opts: ZembleJobOpts) => Promise | void + export class ZembleQueueBull { - readonly #worker: (job: Job) => Promise | void + readonly #worker: ZembleWorker readonly #config?: ZembleQueueConfig constructor( - readonly worker: (job: Job) => Promise | void, + readonly worker: ZembleWorker, config?: ZembleQueueConfig, ) { this.#worker = worker diff --git a/packages/bull/ZembleQueueMock.ts b/packages/bull/ZembleQueueMock.ts index bccb7e6a..18873a65 100644 --- a/packages/bull/ZembleQueueMock.ts +++ b/packages/bull/ZembleQueueMock.ts @@ -1,7 +1,9 @@ /* eslint-disable functional/immutable-data */ /* eslint-disable functional/prefer-readonly-type */ -import type { ZembleQueueBull, ZembleQueueConfig } from './ZembleQueueBull' +import zembleContext from '@zemble/core/zembleContext' + +import type { ZembleQueueBull, ZembleQueueConfig, ZembleWorker } from './ZembleQueueBull' import type { Job, JobsOptions, Queue, } from 'bullmq' @@ -13,14 +15,14 @@ interface IZembleQueue { class ZembleQueueMock implements IZembleQueue { constructor( - readonly worker: (job: Job) => Promise | void, + readonly worker: ZembleWorker, config?: ZembleQueueConfig, ) { this.#worker = worker this.#config = config } - readonly #worker: (job: Job) => Promise | void + readonly #worker: ZembleWorker readonly #config?: ZembleQueueConfig @@ -39,7 +41,7 @@ class ZembleQueueMock implements IZemb await prev await new Promise((resolve) => { setTimeout(() => { - void this.#worker(job) + void this.#worker(job, { logger: zembleContext.logger }) resolve(undefined) }, 0) }) @@ -71,7 +73,7 @@ class ZembleQueueMock implements IZemb async add(name: string, data: DataType, opts?: JobsOptions | undefined): Promise> { const job = this.#createMockJob(name, data, opts) - setTimeout(async () => this.#worker(job), 0) + setTimeout(async () => this.#worker(job, { logger: zembleContext.logger }), 0) return job } } diff --git a/packages/bull/clients/redis.ts b/packages/bull/clients/redis.ts index 92ff17aa..1592f312 100644 --- a/packages/bull/clients/redis.ts +++ b/packages/bull/clients/redis.ts @@ -1,30 +1,33 @@ import Redis from 'ioredis' +import type { IStandardLogger } from '@zemble/core' import type { RedisOptions } from 'ioredis' const NODE_ENV = 'development' as string -export const createClient = (redisUrl: string, options?: RedisOptions): Redis => { +export const createClient = (redisUrl: string, options: { readonly redis?: RedisOptions, readonly logger: IStandardLogger }): Redis => { if (NODE_ENV === 'test') { // this is currently just to avoid connection to the real Redis cluster return {} as Redis // throw new Error('Redis client is not available in test environment'); } - console.info(`[@zemble/bull] Connecting to Redis at ${redisUrl}`) + const { logger } = options + + logger.info(`Connecting to Redis at ${redisUrl}`) const redis = new Redis(redisUrl, { maxRetriesPerRequest: null, enableReadyCheck: false, - ...options, + ...options.redis, }) redis.setMaxListeners(30) redis.on('error', (error) => { - console.error(error, 'Redis error') + logger.error(error, 'Redis error') }) redis.on('connect', () => { - console.info('[@zemble/bull] Connected to Redis') + logger.info('Connected to Redis') }) return redis diff --git a/packages/bull/plugin.ts b/packages/bull/plugin.ts index 04aaca31..8681375f 100644 --- a/packages/bull/plugin.ts +++ b/packages/bull/plugin.ts @@ -60,18 +60,18 @@ export { ZembleQueue } export default new Plugin(import.meta.dir, { defaultConfig: defaults, middleware: async ({ - plugins, context: { pubsub }, config, app, + plugins, context: { pubsub }, config, app, logger, }) => { const appPath = process.cwd() const allQueues = [ ...(await Promise.all(plugins.map(async ({ pluginPath, config }) => { if (!config.middleware?.['zemble-plugin-bull']?.disable) { - return setupQueues(pluginPath, pubsub, config) + return setupQueues(pluginPath, pubsub, config, logger) } return [] }))).flat(), - ...await setupQueues(appPath, pubsub, config), + ...await setupQueues(appPath, pubsub, config, logger), ] if (config.bullboard !== false && process.env.NODE_ENV !== 'test') { diff --git a/packages/bull/queues/hello-world.ts b/packages/bull/queues/hello-world.ts index db520c7b..4c3b7968 100644 --- a/packages/bull/queues/hello-world.ts +++ b/packages/bull/queues/hello-world.ts @@ -1,7 +1,7 @@ import { ZembleQueue } from '../ZembleQueue' -export default new ZembleQueue((job) => { - console.log(job.data) +export default new ZembleQueue((job, { logger }) => { + logger.info(job.data) }, { repeat: { // every 5 seconds diff --git a/packages/bull/utils/setupQueues.ts b/packages/bull/utils/setupQueues.ts index 41b0b63b..ffb44143 100644 --- a/packages/bull/utils/setupQueues.ts +++ b/packages/bull/utils/setupQueues.ts @@ -9,6 +9,7 @@ import createClient from '../clients/redis' import { type BullPluginConfig } from '../plugin' import ZembleQueueBull from '../ZembleQueueBull' +import type { IStandardLogger } from '@zemble/core' import type { Queue, Job, @@ -22,6 +23,7 @@ const setupQueues = async ( pluginPath: string, pubSub: Zemble.PubSubType, config: BullPluginConfig | undefined, + logger: IStandardLogger, ): Promise => { const queuePath = path.join(pluginPath, '/queues') @@ -50,7 +52,7 @@ const setupQueues = async ( if (hasQueues) { if (process.env.NODE_ENV !== 'test' || process.env.DEBUG) { - console.log('[bull-plugin] Initializing queues from ', queuePath) + logger.info('[bull-plugin] Initializing queues from ', queuePath) } const redisUrl = config?.redisUrl ?? process.env.REDIS_URL @@ -66,7 +68,7 @@ const setupQueues = async ( fileNameWithoutExtension, createClient( redisUrl, - config?.redisOptions, + { redis: config?.redisOptions, logger }, ), ) @@ -94,7 +96,7 @@ const setupQueues = async ( } })) } else { - console.error('[bull-plugin] Failed to initialize. No redisUrl provided for bull plugin, you can specify it directly or with REDIS_URL') + logger.error('[bull-plugin] Failed to initialize. No redisUrl provided for bull plugin, you can specify it directly or with REDIS_URL') } } return queues diff --git a/packages/bun/serve.ts b/packages/bun/serve.ts index 554f19c2..8c69ebc4 100644 --- a/packages/bun/serve.ts +++ b/packages/bun/serve.ts @@ -12,7 +12,7 @@ export const serve = async (config: Configure | Promise | Zemble.App // mostly for clickability in the terminal :) const linkPrefix = bunServer.hostname === 'localhost' ? 'http://' : '' - console.log(`[@zemble/bun] Serving on ${linkPrefix}${bunServer.hostname}:${bunServer.port}`) + app.providers.logger.info(`[@zemble/bun] Serving on ${linkPrefix}${bunServer.hostname}:${bunServer.port}`) return app } diff --git a/packages/cms-users/clients/papr.ts b/packages/cms-users/clients/papr.ts index 763f7550..941cf97c 100644 --- a/packages/cms-users/clients/papr.ts +++ b/packages/cms-users/clients/papr.ts @@ -1,8 +1,8 @@ -import zembleContext from '@zemble/core/zembleContext' import Papr, { schema, types } from 'papr' import plugin from '../plugin' +import type { IStandardLogger } from '@zemble/core' import type { MongoClient } from 'mongodb' // eslint-disable-next-line import/no-mutable-exports @@ -10,16 +10,16 @@ export let client: MongoClient | undefined const papr = new Papr() -export async function connect() { +export async function connect({ logger }: {readonly logger: IStandardLogger}) { const db = plugin.providers.mongodb?.db if (db === undefined) throw new Error('MongoDB client not provided or initialized') papr.initialize(db) - zembleContext.logger.log(`Registering ${papr.models.size} models...`) + logger.info(`Registering ${papr.models.size} models...`) papr.models.forEach((model) => { - zembleContext.logger.log(`Registering model: ${model.collection.collectionName}`) + logger.info(`Registering model: ${model.collection.collectionName}`) }) await papr.updateSchemas() diff --git a/packages/cms-users/plugin.ts b/packages/cms-users/plugin.ts index a6d5aadf..c93c98b3 100644 --- a/packages/cms-users/plugin.ts +++ b/packages/cms-users/plugin.ts @@ -44,13 +44,11 @@ const isFirstUser = async (): Promise => { return isFirstUserInternal } -const middleware = async () => { - await Promise.all([connect(), papr.connect()]) -} - const plugin = new Plugin(import.meta.dir, { - middleware: () => middleware, + middleware: async ({ logger }) => { + await Promise.all([connect({ logger }), papr.connect({ logger })]) + }, dependencies: () => { const deps: DependenciesResolver = [ { diff --git a/packages/cms-users/test-setup.ts b/packages/cms-users/test-setup.ts index c41526ce..8caa37bc 100644 --- a/packages/cms-users/test-setup.ts +++ b/packages/cms-users/test-setup.ts @@ -1,5 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ import { createTestApp } from '@zemble/core/test-utils' +import zembleContext from '@zemble/core/zembleContext' import { startInMemoryInstanceAndConfigurePlugin, closeAndStopInMemoryInstance, emptyAllCollections } from '@zemble/mongodb/test-utils' import { setupEnvOverride, resetEnv } from 'zemble-plugin-auth/test-utils' import cmsPapr from 'zemble-plugin-cms/clients/papr' @@ -14,8 +15,8 @@ export const setupBeforeAll = async () => { await createTestApp(plugin) - await connect() - await cmsPapr.connect() + await connect({ logger: zembleContext.logger }) + await cmsPapr.connect({ logger: zembleContext.logger }) } export const teardownAfterAll = async () => { diff --git a/packages/cms/clients/papr.ts b/packages/cms/clients/papr.ts index 72cd0d71..63653ef1 100644 --- a/packages/cms/clients/papr.ts +++ b/packages/cms/clients/papr.ts @@ -1,11 +1,11 @@ /* eslint-disable functional/immutable-data, functional/prefer-readonly-type, no-multi-assign */ -import zembleContext from '@zemble/core/zembleContext' import Papr, { VALIDATION_LEVEL, schema, types } from 'papr' import plugin from '../plugin' import { readEntities } from '../utils/fs' -import type { MongoClient, Db } from 'mongodb' +import type { IStandardLogger } from '@zemble/core' +import type { Db } from 'mongodb' import type { Model } from 'papr' export const EntityEntrySchema = schema({ @@ -42,7 +42,7 @@ class PaprWrapper { #initializing = Promise.resolve() - async initialize() { + async initialize({ logger }: {readonly logger: IStandardLogger}) { const papr = new Papr() await plugin.providers.mongodb?.client.connect() @@ -51,7 +51,7 @@ class PaprWrapper { if (db === undefined) throw new Error('MongoDB client not provided or initialized') - zembleContext.logger.log(`Registering ${papr.models.size} models...`) + logger.info(`Registering ${papr.models.size} models...`) papr.initialize(db) @@ -67,8 +67,8 @@ class PaprWrapper { this.papr = papr } - async connect() { - this.#initializing = this.initialize() + async connect({ logger }: {readonly logger: IStandardLogger}) { + this.#initializing = this.initialize({ logger }) return this.#initializing } diff --git a/packages/cms/dynamicSchema/createDynamicSchema.ts b/packages/cms/dynamicSchema/createDynamicSchema.ts index b1896a65..ee94068f 100644 --- a/packages/cms/dynamicSchema/createDynamicSchema.ts +++ b/packages/cms/dynamicSchema/createDynamicSchema.ts @@ -27,6 +27,7 @@ import { readEntities } from '../utils/fs' import type { EntityEntryType } from '../clients/papr' import type { AnyField } from '../types' +import type { IStandardLogger } from '@zemble/core' import type { GraphQLFieldConfig, GraphQLEnumType, @@ -62,9 +63,9 @@ const fieldResolver = (parent: EntityEntryType, field: AnyField, displayNameFiel return null } -export default async () => { +export default async ({ logger }: {readonly logger: IStandardLogger}) => { if (process.env.NODE_ENV !== 'test') { - await papr.connect() + await papr.connect({ logger }) } const entities = await readEntities() @@ -199,7 +200,7 @@ export default async () => { if (process.env.DEBUG) { const schemaStr = printSchemaWithDirectives(schema) - console.log(`\n\n------- ▼ UPDATED SCHEMA ▼ -------\n\n${schemaStr}\n\n------- ⏶ UPDATED SCHEMA ⏶ -------\n\n`) + logger.debug(`\n\n------- ▼ UPDATED SCHEMA ▼ -------\n\n${schemaStr}\n\n------- ⏶ UPDATED SCHEMA ⏶ -------\n\n`) } if (process.env.NODE_ENV !== 'test') { diff --git a/packages/cms/plugin.ts b/packages/cms/plugin.ts index b8081f6c..f12b16f9 100644 --- a/packages/cms/plugin.ts +++ b/packages/cms/plugin.ts @@ -18,21 +18,19 @@ const defaultConfig = { } satisfies CmsConfig -const middleware = async () => { - await papr.connect() -} - const plugin = new Plugin(import.meta.dir, { - middleware: () => middleware, - dependencies: () => { + middleware: async ({ logger }) => { + await papr.connect({ logger }) + }, + dependencies: ({ providers }) => { const deps: DependenciesResolver = [ { plugin: MongoDB, }, { plugin: graphqlYoga.configure({ - extendSchema: async () => Promise.all([createDynamicSchema()]), + extendSchema: async () => Promise.all([createDynamicSchema({ logger: providers.logger })]), yoga: { plugins: [ useExtendedValidation({ diff --git a/packages/cms/test-setup.ts b/packages/cms/test-setup.ts index 844a14e1..9f79c8c8 100644 --- a/packages/cms/test-setup.ts +++ b/packages/cms/test-setup.ts @@ -1,6 +1,7 @@ /* eslint-disable functional/immutable-data, import/no-extraneous-dependencies */ import { setupEnvOverride, resetEnv, createTestApp } from '@zemble/core/test-utils' +import zembleContext from '@zemble/core/zembleContext' import { startInMemoryInstanceAndConfigurePlugin, closeAndStopInMemoryInstance, emptyAllCollections } from '@zemble/mongodb/test-utils' import generateKeys from 'zemble-plugin-auth/generate-keys' @@ -17,7 +18,7 @@ export const setupBeforeAll = async () => { await createTestApp(plugin) - await papr.connect() + await papr.connect({ logger: zembleContext.logger }) await mockAndReset() } diff --git a/packages/core/cli-runner.ts b/packages/core/cli-runner.ts index 877a3ae1..4d51b2a2 100644 --- a/packages/core/cli-runner.ts +++ b/packages/core/cli-runner.ts @@ -1,16 +1,18 @@ import path from 'node:path' +import zembleContext from './zembleContext' + import type { Configure, Plugin, ZembleApp } from '@zemble/core' const pluginPaths = process.argv.slice(2) if (pluginPaths[0] === '--help' || pluginPaths[0] === '-h' || pluginPaths[0] === 'help') { - console.log('Usage: zemble-dev [morePlugins..]') + zembleContext.logger.info('Usage: zemble-dev [morePlugins..]') process.exit(0) } if (pluginPaths.length === 0) { - console.warn('No plugins specified so this is just an empty app, see `zemble-dev --help` for usage') + zembleContext.logger.warn('No plugins specified so this is just an empty app, see `zemble-dev --help` for usage') } type Runner = (config: Configure | ZembleApp) => void diff --git a/packages/core/createApp.ts b/packages/core/createApp.ts index d5d8c165..83967971 100644 --- a/packages/core/createApp.ts +++ b/packages/core/createApp.ts @@ -32,7 +32,9 @@ export const createApp = async ({ plugins: pluginsBeforeResolvingDeps }: Configu // maybe this should be later - how about middleware that overrides logger? if (process.env.NODE_ENV !== 'test') { - hono.use('*', logger(context.logger.log)) + hono.use('*', logger((...args) => { + context.logger.info(...args) + })) } hono.use('*', async (ctx, next) => { @@ -61,7 +63,7 @@ export const createApp = async ({ plugins: pluginsBeforeResolvingDeps }: Configu ? existingPlugin.configure(plugin.config) : plugin.configure(existingPlugin.config) - context.logger.warn(`[@zemble] Found multiple instances of ${plugin.pluginName}, attempting to merge config, using version ${plugin.pluginVersion}`) + context.logger.warn(`[@zemble/core] Found multiple instances of ${plugin.pluginName}, attempting to merge config, using version ${plugin.pluginVersion}`) return prev.map((p) => (p.pluginName !== pluginToUse.pluginName ? p : pluginToUse)) } @@ -70,39 +72,51 @@ export const createApp = async ({ plugins: pluginsBeforeResolvingDeps }: Configu return [...prev, plugin] }, [] as readonly Plugin[]) - const middleware = plugins.filter( + const pluginsWithMiddleware = plugins.filter( (plugin) => 'initializeMiddleware' in plugin, ) - context.logger.log(`[@zemble] Initializing ${packageJson.name} with ${plugins.length} plugins:\n${plugins.map((p) => `- ${p.pluginName}@${p.pluginVersion}${'initializeMiddleware' in p ? ' (middleware)' : ''}`).join('\n')}`) - - if (process.env.DEBUG) { - plugins.forEach((plugin) => { - context.logger.log(`[@zemble] Loading ${plugin.pluginName} with config: ${JSON.stringify(filterConfig(plugin.config), null, 2)}`) - }) - } + context.logger.info(`[@zemble/core] Initializing ${packageJson.name} with ${plugins.length} plugins:\n${plugins.map( + (p) => `- ${p.pluginName}@${p.pluginVersion}${'initializeMiddleware' in p + ? ' (middleware)' + : ''}`, + ).join('\n')}`) + + const defaultProviders = { + logger: context.logger, + } as Zemble.Providers + + plugins.forEach((plugin) => { + // eslint-disable-next-line functional/immutable-data, no-param-reassign + plugin.providers = { ...defaultProviders } + if (process.env.DEBUG) { + context.logger.info(`[@zemble/core] Loading ${plugin.pluginName} with config: ${JSON.stringify(filterConfig(plugin.config), null, 2)}`) + } + }) const appDir = process.cwd() const preInitApp = { hono, appDir, - providers: {} as Zemble.Providers, + providers: defaultProviders, } - const runBeforeServe = await middleware?.reduce(async ( + const runBeforeServe = await pluginsWithMiddleware?.reduce(async ( prev, - middleware, + pluginWithMiddleware, ) => { const p = await prev - const ret = await middleware.initializeMiddleware?.({ + const ret = await pluginWithMiddleware.initializeMiddleware?.({ plugins, app: preInitApp, // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore context, - config: middleware.config, + config: pluginWithMiddleware.config, + self: pluginWithMiddleware, + logger: pluginWithMiddleware.providers.logger.child({ middlewarePluginName: pluginWithMiddleware.pluginName, middlewarePluginVersion: pluginWithMiddleware.pluginVersion }), }) if (typeof ret === 'function') { @@ -135,7 +149,7 @@ export const createApp = async ({ plugins: pluginsBeforeResolvingDeps }: Configu if (process.env.DEBUG) { const routes = hono.routes.map((route) => ` - [${route.method}] ${route.path}`).join('\n') - console.log(`[@zemble] Routes:\n${routes}`) + context.logger.info(`[@zemble/core] Routes:\n${routes}`) } return zembleApp diff --git a/packages/core/createLogger.ts b/packages/core/createLogger.ts new file mode 100644 index 00000000..270038ed --- /dev/null +++ b/packages/core/createLogger.ts @@ -0,0 +1,65 @@ +import { + type LevelWithSilentOrString, +} from 'pino' + +interface LogFn { + (obj: unknown, msg?: string, ...args: readonly unknown[]): void; + (msg: string, ...args: readonly unknown[]): void; +} + +const severitiesInOrder: readonly LevelWithSilentOrString[] = [ + 'fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent', +] + +const defaultDebugLevel = 'info' + +const logFn = (levelToLog: LevelWithSilentOrString, minLevelToLog: LevelWithSilentOrString, extraData?: object): LogFn => (...args) => { + if (minLevelToLog === 'silent') { + return + } + + const shouldLog = severitiesInOrder.indexOf(levelToLog) <= severitiesInOrder.indexOf(minLevelToLog) + if (shouldLog) { + const callFn = (Object.keys(console).includes(levelToLog) ? levelToLog : (levelToLog === 'fatal' ? 'error' : 'log')) + + if (extraData) { + if (typeof args[0] === 'string') { + // @ts-expect-error fix later + // eslint-disable-next-line no-console + console[callFn](`${args[0]} ${JSON.stringify(extraData)}`, ...args.slice(1)) + } else if (typeof args[0] === 'object') { + // @ts-expect-error fix later + // eslint-disable-next-line no-console + console[callFn]({ ...extraData, ...args[0] }, ...args.slice(1)) + } else { + // @ts-expect-error fix later + // eslint-disable-next-line no-console + console[callFn](JSON.stringify(extraData), ...args) + } + } else { + // @ts-expect-error fix later + // eslint-disable-next-line no-console + console[callFn](...args) + } + } +} + +const createLogger = (extraData?: object) => { + let obj = { + level: defaultDebugLevel, + child: (moreData?: object) => createLogger(moreData + ? { ...extraData, ...moreData } + : extraData), + } + + severitiesInOrder.forEach((level) => { + obj = { + ...obj, + [level]: logFn(level, obj.level, extraData), + } + }) + + return obj as unknown as Zemble.GlobalContext['logger'] +} + +export default createLogger diff --git a/packages/core/package.json b/packages/core/package.json index c4c53c74..5f0b95a0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -33,7 +33,9 @@ "license": "ISC", "dependencies": { "dotenv": "^16.3.1", - "hono": "^3.11.10" + "hono": "^3.11.10", + "pino": "^8.17.2", + "pino-debug": "^2.0.0" }, "devDependencies": { "type-fest": "^4.8.3" diff --git a/packages/core/types.ts b/packages/core/types.ts index a0ca5835..14c429a1 100644 --- a/packages/core/types.ts +++ b/packages/core/types.ts @@ -5,6 +5,7 @@ import type { PubSub } from 'graphql-yoga' import type { Hono, Context as HonoContext, } from 'hono' +import type pino from 'pino' export interface IEmail { readonly email: string @@ -39,8 +40,8 @@ export abstract class IStandardKeyValueService { abstract entries(): Promise | readonly (readonly [string, T])[] } -interface IStandardLogger extends Pick { - +export interface IStandardLogger extends pino.BaseLogger { + readonly child: (bindings: Record) => IStandardLogger } declare global { @@ -66,6 +67,9 @@ declare global { sendEmail?: IStandardSendEmailService // eslint-disable-next-line functional/prefer-readonly-type kv: (prefix: K) => IStandardKeyValueService + + // eslint-disable-next-line functional/prefer-readonly-type + logger: IStandardLogger } interface Providers extends DefaultProviders { @@ -104,9 +108,7 @@ declare global { // optional standard services here, so we can override them interface BaseStandardContext { - // eslint-disable-next-line functional/prefer-readonly-type - // eslint-disable-next-line functional/prefer-readonly-type } interface GlobalContext extends BaseStandardContext { @@ -151,7 +153,8 @@ export type Dependency = { export type DependenciesResolver = readonly Dependency[] | ((self: TSelf) => readonly Dependency[]) export type PluginOpts< - TSelf, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TSelf extends Plugin, TConfig extends Zemble.GlobalConfig = Zemble.GlobalConfig, TDefaultConfig extends Partial = TConfig, TResolvedConfig extends TConfig & TDefaultConfig = TConfig & TDefaultConfig, @@ -173,6 +176,8 @@ export type PluginOpts< readonly version?: string, readonly middleware?: Middleware + + // readonly provides: TSelf['providers'] } export type RunBeforeServeFn = (() => Promise) | (() => void) @@ -185,6 +190,8 @@ export type Middleware, readonly context: Zemble.GlobalContext - readonly config: TMiddlewareConfig + readonly config: TMiddlewareConfig, + readonly self: PluginType, + readonly logger: IStandardLogger, } ) => MiddlewareReturn diff --git a/packages/core/zembleContext.ts b/packages/core/zembleContext.ts index 1ff6668f..eced3d05 100644 --- a/packages/core/zembleContext.ts +++ b/packages/core/zembleContext.ts @@ -1,18 +1,12 @@ +import createLogger from './createLogger' + import type { IStandardKeyValueService } from '.' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore class ContextInstance implements Zemble.GlobalContext { // eslint-disable-next-line functional/prefer-readonly-type - logger = process.env.NODE_ENV === 'test' ? { - debug: () => {}, - info: () => {}, - time: () => {}, - timeEnd: () => {}, - warn: () => {}, - error: console.error, - log: () => {}, - } : console + logger = createLogger() // eslint-disable-next-line class-methods-use-this kv< diff --git a/packages/create-zemble-app/bin/create-zemble-app.js b/packages/create-zemble-app/bin/create-zemble-app.js index 68f9af4c..37261b14 100755 --- a/packages/create-zemble-app/bin/create-zemble-app.js +++ b/packages/create-zemble-app/bin/create-zemble-app.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun import { copy } from 'fs-extra' import { spawn } from 'node:child_process' diff --git a/packages/create-zemble-app/cli.test.ts b/packages/create-zemble-app/cli.test.ts index 6b0f120d..b37f43f8 100644 --- a/packages/create-zemble-app/cli.test.ts +++ b/packages/create-zemble-app/cli.test.ts @@ -1,3 +1,4 @@ +import zembleContext from '@zemble/core/zembleContext' import { afterAll, beforeAll, expect, test, @@ -23,19 +24,19 @@ const testTemplate = (template: string) => { const createRes = spawnSync('bun', [binPath, name, template], { cwd: testDirectory }) if (createRes.error) { - console.error(createRes.error.message) + zembleContext.logger.error(createRes.error.message) } if (createRes.stdout && process.env.DEBUG) { - console.log(createRes.stdout.toString('utf-8')) + zembleContext.logger.info(createRes.stdout.toString('utf-8')) } expect(createRes.status).toBe(0) const testRes = spawnSync('bun', ['run', 'test'], { cwd: join(testDirectory, name) }) if (testRes.error) { - console.error(testRes.error.message) + zembleContext.logger.error(testRes.error.message) } if (testRes.stdout && process.env.DEBUG) { - console.log(testRes.stdout.toString('utf-8')) + zembleContext.logger.info(testRes.stdout.toString('utf-8')) } expect(testRes.status).toBe(0) diff --git a/packages/create-zemble-app/templates/graphql/graphql/Subscription/randomNumber.ts b/packages/create-zemble-app/templates/graphql/graphql/Subscription/randomNumber.ts index 2959ba0b..ee69c39e 100644 --- a/packages/create-zemble-app/templates/graphql/graphql/Subscription/randomNumber.ts +++ b/packages/create-zemble-app/templates/graphql/graphql/Subscription/randomNumber.ts @@ -2,14 +2,11 @@ import type { SubscriptionResolvers } from '../schema.generated' const randomNumber: SubscriptionResolvers['randomNumber'] = { // subscribe to the randomNumber event - subscribe: (_, __, { pubsub }) => { - console.log('subscribing to randomNumber') + subscribe: (_, __, { pubsub, logger }) => { + logger.info('subscribing to randomNumber') return pubsub.subscribe('randomNumber') }, - resolve: (payload: number) => { - console.log('resolving randomNumber', payload) - return payload - }, + resolve: (payload: number) => payload, } export default randomNumber diff --git a/packages/create-zemble-app/templates/graphql/package.json b/packages/create-zemble-app/templates/graphql/package.json index 57b35b81..795180cb 100644 --- a/packages/create-zemble-app/templates/graphql/package.json +++ b/packages/create-zemble-app/templates/graphql/package.json @@ -11,7 +11,7 @@ }, "scripts": { "start": "bun serve.ts", - "test": "bun run codegen && bun test", + "test": "bun test", "dev": "bun --hot serve.ts", "typecheck": "tsc --noEmit", "codegen": "graphql-codegen" diff --git a/packages/create-zemble-plugin/bin/create-zemble-plugin.js b/packages/create-zemble-plugin/bin/create-zemble-plugin.js index ffadbbed..b877f618 100755 --- a/packages/create-zemble-plugin/bin/create-zemble-plugin.js +++ b/packages/create-zemble-plugin/bin/create-zemble-plugin.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node +#!/usr/bin/env bun import { copy } from 'fs-extra' import { spawn } from 'node:child_process' diff --git a/packages/create-zemble-plugin/templates/graphql/package.json b/packages/create-zemble-plugin/templates/graphql/package.json index 3db0dc38..a3ce42b9 100644 --- a/packages/create-zemble-plugin/templates/graphql/package.json +++ b/packages/create-zemble-plugin/templates/graphql/package.json @@ -15,7 +15,7 @@ "@zemble/graphql": "latest" }, "scripts": { - "test": "bun run codegen && bun test", + "test": "bun test", "dev": "zemble-dev plugin.ts", "typecheck": "tsc --noEmit", "codegen": "graphql-codegen" diff --git a/packages/email-sendgrid/plugin.ts b/packages/email-sendgrid/plugin.ts index 69752fed..103163fc 100644 --- a/packages/email-sendgrid/plugin.ts +++ b/packages/email-sendgrid/plugin.ts @@ -42,14 +42,16 @@ const defaultConfig = { // eslint-disable-next-line unicorn/consistent-function-scoping const plugin = new Plugin(import.meta.dir, { - middleware: async ({ plugins, config, app }) => { + middleware: async ({ + plugins, config, app, logger, + }) => { if (!config.disable) { const initializeProvider = (): IStandardSendEmailService => async ({ from, to, html, text, subject, // eslint-disable-next-line unicorn/consistent-function-scoping }) => { if (!plugin.config.SENDGRID_API_KEY) { - console.warn('SENDGRID_API_KEY must be set to send email, skipping') + logger.warn('SENDGRID_API_KEY must be set to send email, skipping') return false } diff --git a/packages/graphql/clients/redis.ts b/packages/graphql/clients/redis.ts index f69d24b2..b936bbd7 100644 --- a/packages/graphql/clients/redis.ts +++ b/packages/graphql/clients/redis.ts @@ -1,31 +1,34 @@ // eslint-disable-next-line import/no-extraneous-dependencies import Redis from 'ioredis' +import type { IStandardLogger } from '@zemble/core' import type { RedisOptions } from 'ioredis' const NODE_ENV = 'development' as string -export const createClient = (redisUrl: string, options?: RedisOptions): Redis => { +export const createClient = (redisUrl: string, options: { readonly redis?: RedisOptions, readonly logger: IStandardLogger }): Redis => { if (NODE_ENV === 'test') { // this is currently just to avoid connection to the real Redis cluster return {} as Redis // throw new Error('Redis client is not available in test environment'); } - console.info(`[@zemble/graphql] Connecting to Redis at ${redisUrl}`) + const { logger } = options + + logger.info(`Connecting to Redis at ${redisUrl}`) const redis = new Redis(redisUrl, { maxRetriesPerRequest: null, enableReadyCheck: false, - ...options, + ...options.redis, }) redis.setMaxListeners(30) redis.on('error', (error) => { - console.error(error, 'Redis error') + logger.error(error, 'Redis error') }) redis.on('connect', () => { - console.info('[@zemble/graphql] Connected to Redis') + logger.info('Connected to Redis') }) return redis diff --git a/packages/graphql/createPubSub.ts b/packages/graphql/createPubSub.ts index 0511c15b..03b6b485 100644 --- a/packages/graphql/createPubSub.ts +++ b/packages/graphql/createPubSub.ts @@ -1,15 +1,18 @@ +import zembleContext from '@zemble/core/zembleContext' import { createPubSub as createYogaPubSub } from 'graphql-yoga' +import type { IStandardLogger } from '@zemble/core' import type { RedisOptions } from 'ioredis' -const createPubSub = async (redisUrl?: string, redisConfig?: RedisOptions) => { +const createPubSub = async (redisUrl?: string, options?: { readonly redis?: RedisOptions, readonly logger?: IStandardLogger }) => { if (redisUrl) { + const optionsWithDefaults = { logger: zembleContext.logger, ...options } try { // eslint-disable-next-line import/no-extraneous-dependencies const { createRedisEventTarget } = await import('@graphql-yoga/redis-event-target') const { createClient } = await import('./clients/redis') - const publishClient = createClient(redisUrl, redisConfig) - const subscribeClient = createClient(redisUrl, redisConfig) + const publishClient = createClient(redisUrl, optionsWithDefaults) + const subscribeClient = createClient(redisUrl, optionsWithDefaults) const eventTarget = createRedisEventTarget({ publishClient, @@ -18,7 +21,7 @@ const createPubSub = async (redisUrl?: string, redisConfig?: RedisOptions) => { return createYogaPubSub({ eventTarget }) } catch (error) { - console.error('Error initializing pubsub, maybe you need to install ioredis or @graphql-yoga/redis-event-target?', error) + optionsWithDefaults.logger.error('Error initializing pubsub, maybe you need to install ioredis or @graphql-yoga/redis-event-target?', error) } } diff --git a/packages/graphql/graphql/Subscription/randomNumber.ts b/packages/graphql/graphql/Subscription/randomNumber.ts index 2959ba0b..ee69c39e 100644 --- a/packages/graphql/graphql/Subscription/randomNumber.ts +++ b/packages/graphql/graphql/Subscription/randomNumber.ts @@ -2,14 +2,11 @@ import type { SubscriptionResolvers } from '../schema.generated' const randomNumber: SubscriptionResolvers['randomNumber'] = { // subscribe to the randomNumber event - subscribe: (_, __, { pubsub }) => { - console.log('subscribing to randomNumber') + subscribe: (_, __, { pubsub, logger }) => { + logger.info('subscribing to randomNumber') return pubsub.subscribe('randomNumber') }, - resolve: (payload: number) => { - console.log('resolving randomNumber', payload) - return payload - }, + resolve: (payload: number) => payload, } export default randomNumber diff --git a/packages/graphql/graphql/Subscription/tick.ts b/packages/graphql/graphql/Subscription/tick.ts index ce7e549d..c1bed659 100644 --- a/packages/graphql/graphql/Subscription/tick.ts +++ b/packages/graphql/graphql/Subscription/tick.ts @@ -11,9 +11,9 @@ const initializeOnce = (pubsub: Zemble.PubSubType) => { const tick: SubscriptionResolvers['tick'] = { // subscribe to the tick event - subscribe: (_, __, { pubsub }) => { + subscribe: (_, __, { pubsub, logger }) => { initializeOnce(pubsub) - console.log('subscribing to tick') + logger.info('subscribing to tick') return pubsub.subscribe('tick') }, resolve: (payload: number) => payload, diff --git a/packages/graphql/middleware.ts b/packages/graphql/middleware.ts index 520c6f02..6a2d71f6 100644 --- a/packages/graphql/middleware.ts +++ b/packages/graphql/middleware.ts @@ -17,12 +17,15 @@ import type { Middleware } from '@zemble/core/types' export const middleware: Middleware = async ( { - config, app, context, plugins, + config, app, context, plugins, logger, }, ) => { const pubsub = await createPubSub( config.redisUrl, - config.redisOptions, + { + logger, + redis: config.redisOptions, + }, ) const { hono } = app @@ -33,14 +36,14 @@ export const middleware: Middleware = async ( }) if (process.env.NODE_ENV === 'test') { - // @ts-expect-error sdfgsdfg + // @ts-expect-error fix later app.gqlRequest = async (query, vars, opts) => { const response = await gqlRequest(app, query, vars, opts) return response } - // @ts-expect-error sdfgsdfg + // @ts-expect-error fix later app.gqlRequestUntyped = async (untypedQuery: string, vars, opts) => { const response = await gqlRequestUntyped(app, untypedQuery, vars, opts) return response @@ -78,7 +81,7 @@ export const middleware: Middleware = async ( return mergedSchema }, pubsub, - context.logger, + logger, { ...config.yoga, graphiql: async (req, context) => { diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 12e797a8..9eaf57f4 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -7,7 +7,7 @@ "dev": "zemble-dev plugin.ts", "lint": "eslint .", "graphql-codegen": "graphql-codegen", - "typecheck": "graphql-codegen && tsc --noEmit" + "typecheck": "tsc --noEmit" }, "keywords": [ "zemble", diff --git a/packages/graphql/plugin.ts b/packages/graphql/plugin.ts index 35216652..fb959b2f 100644 --- a/packages/graphql/plugin.ts +++ b/packages/graphql/plugin.ts @@ -1,5 +1,6 @@ -import { useEngine } from '@envelop/core' -import { Plugin } from '@zemble/core' +import { useEngine, useLogger } from '@envelop/core' +import { Plugin, type IStandardLogger } from '@zemble/core' +import zembleContext from '@zemble/core/zembleContext' import * as GraphQLJS from 'graphql' import middleware from './middleware' @@ -53,6 +54,7 @@ declare global { readonly token: string | undefined readonly decodedToken: Zemble.TokenRegistry[keyof Zemble.TokenRegistry] | undefined readonly honoContext: RouteContext + readonly logger: IStandardLogger } interface HonoBindings extends Record { @@ -92,11 +94,21 @@ export interface GraphQLMiddlewareConfig extends Zemble.GlobalConfig { readonly outputMergedSchemaPath?: string | false } +const logFn = (eventName: string, ...args: readonly unknown[]) => { + plugin.providers.logger.debug(eventName, ...args) +} + const defaultConfig = { yoga: { graphqlEndpoint: '/graphql', - // eslint-disable-next-line react-hooks/rules-of-hooks - plugins: [useEngine(GraphQLJS)], + plugins: [ + // eslint-disable-next-line react-hooks/rules-of-hooks + useEngine(GraphQLJS), + // eslint-disable-next-line react-hooks/rules-of-hooks + useLogger({ + logFn, + }), + ], maskedErrors: { isDev: process.env.NODE_ENV === 'development', }, @@ -111,7 +123,9 @@ const defaultConfig = { outputMergedSchemaPath: './app.generated.graphql', } satisfies GraphQLMiddlewareConfig -export default new Plugin( +const plugin = new Plugin( import.meta.dir, { defaultConfig, middleware }, ) + +export default plugin diff --git a/packages/graphql/utils/buildMergedSchema.ts b/packages/graphql/utils/buildMergedSchema.ts index cd6e0448..5d1a0773 100644 --- a/packages/graphql/utils/buildMergedSchema.ts +++ b/packages/graphql/utils/buildMergedSchema.ts @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign */ /* eslint-disable functional/immutable-data */ import { mergeSchemas } from '@graphql-tools/schema' +import zembleContext from '@zemble/core/zembleContext' import { type GraphQLScalarType } from 'graphql' import * as fs from 'node:fs' import * as path from 'node:path' @@ -9,7 +10,7 @@ import createPluginSchema from './createPluginSchema' import type { GraphQLMiddlewareConfig } from '../plugin' import type { Subschema } from '@graphql-tools/delegate' -import type { Plugin } from '@zemble/core' +import type { IStandardLogger, Plugin } from '@zemble/core' import type { GraphQLSchemaWithContext, } from 'graphql-yoga' @@ -18,8 +19,10 @@ const processPluginSchema = async (pluginPath: string, { transforms, scalars, skipGraphQLValidation, -}: { readonly transforms: Subschema['transforms'], readonly scalars: Record, readonly skipGraphQLValidation?: boolean }) => { + logger, +}: { readonly transforms: Subschema['transforms'], readonly scalars: Record, readonly skipGraphQLValidation?: boolean, readonly logger: IStandardLogger }) => { const graphqlDir = path.join(pluginPath, '/graphql') + const hasGraphQL = fs.existsSync(graphqlDir) if (hasGraphQL) { return [ @@ -28,6 +31,7 @@ const processPluginSchema = async (pluginPath: string, { transforms, scalars, skipGraphQLValidation: !!skipGraphQLValidation, + logger, }), ] } @@ -42,7 +46,9 @@ export const buildMergedSchema = async ( const selfSchemas: readonly GraphQLSchemaWithContext[] = [ // don't load if we're already a plugin ...!isPlugin - ? await processPluginSchema(process.cwd(), { transforms: [], scalars: config.scalars || {}, skipGraphQLValidation: false }) + ? await processPluginSchema(process.cwd(), { + transforms: [], scalars: config.scalars || {}, skipGraphQLValidation: false, logger: zembleContext.logger, + }) : [], // eslint-disable-next-line no-nested-ternary ...(config.extendSchema @@ -60,7 +66,7 @@ export const buildMergedSchema = async ( // eslint-disable-next-line @typescript-eslint/await-thenable const graphQLSchemas = await pluginsToAdd.reduce(async ( prev, - { pluginPath, config: traversedPluginConfig }, + { pluginPath, config: traversedPluginConfig, providers }, ) => { const graphqlSchemaTransforms = traversedPluginConfig.middleware?.['@zemble/graphql']?.graphqlSchemaTransforms // eslint-disable-next-line functional/prefer-readonly-type @@ -69,6 +75,7 @@ export const buildMergedSchema = async ( ...await processPluginSchema(pluginPath, { transforms: graphqlSchemaTransforms ?? [], scalars: config.scalars || {}, + logger: providers.logger, // skipGraphQLValidation: true, // skip validation so we don't need to provide root queries for plugins where it doesn't make sense }), ] diff --git a/packages/graphql/utils/createPluginSchema.ts b/packages/graphql/utils/createPluginSchema.ts index 8f6b8519..645420a9 100644 --- a/packages/graphql/utils/createPluginSchema.ts +++ b/packages/graphql/utils/createPluginSchema.ts @@ -9,15 +9,17 @@ import { join } from 'node:path' import readResolvers from './readResolvers' import type { Subschema } from '@graphql-tools/delegate' +import type { IStandardLogger } from '@zemble/core' import type { GraphQLScalarType } from 'graphql' export const createPluginSchema = async ({ - graphqlDir, transforms, scalars, skipGraphQLValidation, + graphqlDir, transforms, scalars, skipGraphQLValidation, logger, }: { readonly graphqlDir: string; readonly transforms: Subschema['transforms'], readonly scalars: Record, readonly skipGraphQLValidation: boolean, + readonly logger: IStandardLogger, }) => { const [ Query, @@ -26,11 +28,11 @@ export const createPluginSchema = async ({ Type, Scalars, ] = await Promise.all([ - readResolvers(join(graphqlDir, '/Query')), - readResolvers(join(graphqlDir, '/Mutation')), - readResolvers(join(graphqlDir, '/Subscription')), - readResolvers(join(graphqlDir, '/Type')), - readResolvers(join(graphqlDir, '/Scalar')), + readResolvers(join(graphqlDir, '/Query'), logger), + readResolvers(join(graphqlDir, '/Mutation'), logger), + readResolvers(join(graphqlDir, '/Subscription'), logger), + readResolvers(join(graphqlDir, '/Type'), logger), + readResolvers(join(graphqlDir, '/Scalar'), logger), ]) const graphqlGlob = join(graphqlDir, './**/*.graphql') diff --git a/packages/graphql/utils/gqlRequest.ts b/packages/graphql/utils/gqlRequest.ts index 71240521..7efa661f 100644 --- a/packages/graphql/utils/gqlRequest.ts +++ b/packages/graphql/utils/gqlRequest.ts @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ /* eslint-disable functional/immutable-data */ +import zembleContext from '@zemble/core/zembleContext' import { type GraphQLFormattedError } from 'graphql' import { print } from 'graphql/language/printer' @@ -31,7 +32,7 @@ export async function gqlRequest( } if (errors && !options?.silenceErrors) { - console.error(errors) + zembleContext.logger.error(errors) } return { errors, data, response } diff --git a/packages/graphql/utils/gqlRequestUntyped.ts b/packages/graphql/utils/gqlRequestUntyped.ts index d94f1d81..f43d2e5e 100644 --- a/packages/graphql/utils/gqlRequestUntyped.ts +++ b/packages/graphql/utils/gqlRequestUntyped.ts @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ /* eslint-disable functional/immutable-data */ +import zembleContext from '@zemble/core/zembleContext' import { type GraphQLFormattedError } from 'graphql' export async function gqlRequestUntyped( @@ -26,7 +27,7 @@ export async function gqlRequestUntyped( } if (errors && !options?.silenceErrors) { - console.error(errors) + zembleContext.logger.error(errors) } return { data, errors, response } diff --git a/packages/graphql/utils/readResolvers.ts b/packages/graphql/utils/readResolvers.ts index 8c9e4739..3042d46d 100644 --- a/packages/graphql/utils/readResolvers.ts +++ b/packages/graphql/utils/readResolvers.ts @@ -1,7 +1,9 @@ import { readdirSync } from 'node:fs' import { join } from 'node:path' -export const readResolvers = async (path: string) => { +import type { IStandardLogger } from '@zemble/core' + +export const readResolvers = async (path: string, logger: IStandardLogger) => { try { const resolvedPaths = new Set() const erroredPaths: Record = {} @@ -33,7 +35,7 @@ export const readResolvers = async (path: string) => { Object.keys(erroredPaths).forEach((route) => { if (!resolvedPaths.has(route)) { const error = erroredPaths[route] - console.error(error) + logger.error(error) } }) diff --git a/packages/kv/clients/RedisKeyValue.ts b/packages/kv/clients/RedisKeyValue.ts index d521c4cc..db42d112 100644 --- a/packages/kv/clients/RedisKeyValue.ts +++ b/packages/kv/clients/RedisKeyValue.ts @@ -1,4 +1,4 @@ -import { IStandardKeyValueService } from '@zemble/core' +import { IStandardKeyValueService, type IStandardLogger } from '@zemble/core' import createClient from './redis' @@ -28,12 +28,12 @@ class RedisKeyValue extends IStandardKeyValueService { private readonly prefix: string - constructor(prefix: string, redisUrl: string, redisOptions?: RedisOptions) { + constructor(prefix: string, redisUrl: string, logger: IStandardLogger, redisOptions?: RedisOptions) { super() this.client = createClient( redisUrl, - redisOptions, + { redis: redisOptions, logger }, ) this.prefix = `zemble-plugin-kv:${prefix}` diff --git a/packages/kv/clients/redis.ts b/packages/kv/clients/redis.ts index 2ce3b26b..b936bbd7 100644 --- a/packages/kv/clients/redis.ts +++ b/packages/kv/clients/redis.ts @@ -1,30 +1,34 @@ +// eslint-disable-next-line import/no-extraneous-dependencies import Redis from 'ioredis' +import type { IStandardLogger } from '@zemble/core' import type { RedisOptions } from 'ioredis' const NODE_ENV = 'development' as string -export const createClient = (redisUrl: string, options?: RedisOptions): Redis => { +export const createClient = (redisUrl: string, options: { readonly redis?: RedisOptions, readonly logger: IStandardLogger }): Redis => { if (NODE_ENV === 'test') { // this is currently just to avoid connection to the real Redis cluster return {} as Redis // throw new Error('Redis client is not available in test environment'); } - console.info(`[@zemble/kv] Connecting to Redis at ${redisUrl}`) + const { logger } = options + + logger.info(`Connecting to Redis at ${redisUrl}`) const redis = new Redis(redisUrl, { maxRetriesPerRequest: null, enableReadyCheck: false, - ...options, + ...options.redis, }) redis.setMaxListeners(30) redis.on('error', (error) => { - console.error(error, 'Redis error') + logger.error(error, 'Redis error') }) redis.on('connect', () => { - console.info('[@zemble/kv] Connected to Redis') + logger.info('Connected to Redis') }) return redis diff --git a/packages/kv/plugin.ts b/packages/kv/plugin.ts index a7dd8693..0730374c 100644 --- a/packages/kv/plugin.ts +++ b/packages/kv/plugin.ts @@ -35,7 +35,7 @@ const plugin = new Plugin { await setupProvider({ app, @@ -51,20 +51,21 @@ const plugin = new Plugin(initWithConfig.cloudflareNamespace!, prefix as string) } - context.logger.warn('cloudflareNamespace is required for cloudflare implementation') + logger.warn('cloudflareNamespace is required for cloudflare implementation') } else if (initWithConfig.implementation === 'redis') { if (initWithConfig.redisUrl) { return new RedisKeyValue( prefix as string, initWithConfig.redisUrl!, + logger, initWithConfig.redisOptions, ) } - context.logger.warn('redisUrl is required for redis implementation') + logger.warn('redisUrl is required for redis implementation') } if (process.env.NODE_ENV === 'production') { - context.logger.warn('Using in-memory key-value store in production is not recommended, since you can\'t share data between multiple instances of your app') + logger.warn('Using in-memory key-value store in production is not recommended, since you can\'t share data between multiple instances of your app') } return new KeyValue(prefix as string) as IStandardKeyValueService diff --git a/packages/logger-graphql/graphql/Mutation/setLogLevel.ts b/packages/logger-graphql/graphql/Mutation/setLogLevel.ts new file mode 100644 index 00000000..ea3cd187 --- /dev/null +++ b/packages/logger-graphql/graphql/Mutation/setLogLevel.ts @@ -0,0 +1,10 @@ +import type { MutationResolvers } from '../schema.generated' + +const logLevel: MutationResolvers['setLogLevel'] = (_, { level }, { logger }) => { + // eslint-disable-next-line functional/immutable-data, no-param-reassign + logger.level = level + + return level +} + +export default logLevel diff --git a/packages/logger-graphql/graphql/Query/logLevel.ts b/packages/logger-graphql/graphql/Query/logLevel.ts new file mode 100644 index 00000000..58c86ec9 --- /dev/null +++ b/packages/logger-graphql/graphql/Query/logLevel.ts @@ -0,0 +1,5 @@ +import type { QueryResolvers } from '../schema.generated' + +const logLevel: QueryResolvers['logLevel'] = (_, __, { logger }) => logger.level + +export default logLevel diff --git a/packages/logger-graphql/graphql/Query/logger.ts b/packages/logger-graphql/graphql/Query/logs.ts similarity index 100% rename from packages/logger-graphql/graphql/Query/logger.ts rename to packages/logger-graphql/graphql/Query/logs.ts diff --git a/packages/logger-graphql/graphql/Subscription/logs.ts b/packages/logger-graphql/graphql/Subscription/logs.ts new file mode 100644 index 00000000..4b304f4d --- /dev/null +++ b/packages/logger-graphql/graphql/Subscription/logs.ts @@ -0,0 +1,21 @@ +import type { SubscriptionResolvers } from '../schema.generated' + +const loggerStreamer: SubscriptionResolvers['logs'] = { + subscribe: (_, __, { pubsub }) => { + setTimeout(() => { + pubsub.publish(`logger`, { severity: 'info', args: ['logger streamer started'] }) + }, 0) + return pubsub.subscribe(`logger`) + }, + resolve: (payload: { readonly severity: string, readonly args: readonly [unknown?, ...readonly unknown[]]}) => { + const { severity, args } = payload + + return { + severity, + message: args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a, null, ' ').replaceAll('\n', ' '))).join(', '), + timestamp: new Date().toISOString(), + } + }, +} + +export default loggerStreamer diff --git a/packages/logger-graphql/graphql/schema.graphql b/packages/logger-graphql/graphql/schema.graphql index 0edc5e2f..72530669 100644 --- a/packages/logger-graphql/graphql/schema.graphql +++ b/packages/logger-graphql/graphql/schema.graphql @@ -1,7 +1,41 @@ +scalar JSONObject + directive @stream on FIELD_DEFINITION | FIELD +input AuthOr { + includes: JSONObject + match: JSONObject +} + +directive @auth( + match: JSONObject, + skip: Boolean + includes: JSONObject + or: [AuthOr!] +) on FIELD_DEFINITION + +enum LogLevel { + fatal + error + warn + info + debug + trace + silent +} + type Query { - logger: [LogOutput!]! @stream + # not getting the log streaming to work currently, not quite sure why + logs: [LogOutput!]! @auth(match: { isSuperAdmin: true }) @stream + logLevel: LogLevel! @auth(match: { isSuperAdmin: true }) +} + +type Subscription { + logs(minLevel: LogLevel): LogOutput! @auth(match: { isSuperAdmin: true }) +} + +type Mutation { + setLogLevel(level: LogLevel!): LogLevel! @auth(match: { isSuperAdmin: true }) } type LogOutput { diff --git a/packages/logger-graphql/package.json b/packages/logger-graphql/package.json index 4a60f2d8..9245805e 100644 --- a/packages/logger-graphql/package.json +++ b/packages/logger-graphql/package.json @@ -37,8 +37,10 @@ "@graphql-yoga/plugin-defer-stream": "^3.1.0", "@zemble/core": "workspace:*", "@zemble/graphql": "workspace:*", + "@zemble/pino": "workspace:*", "graphql-yoga": "^5.1.0", - "hono": "^3.11.10" + "hono": "^3.11.10", + "pino": "^8.17.2" }, "devDependencies": { "@graphql-codegen/cli": "^5.0.0", diff --git a/packages/logger-graphql/plugin.ts b/packages/logger-graphql/plugin.ts index 854b7784..f94b3ea6 100644 --- a/packages/logger-graphql/plugin.ts +++ b/packages/logger-graphql/plugin.ts @@ -1,6 +1,9 @@ import { useDeferStream } from '@graphql-yoga/plugin-defer-stream' import { Plugin } from '@zemble/core' +import zembleContext from '@zemble/core/zembleContext' import YogaPlugin from '@zemble/graphql' +import Logger from '@zemble/pino' +import pino from 'pino' export interface GraphQLMiddlewareConfig extends Zemble.GlobalConfig { @@ -13,48 +16,29 @@ const defaultConfig = { export default new Plugin( import.meta.dir, { - middleware: ({ context }) => { - // eslint-disable-next-line functional/immutable-data - context.logger = { - time: (...args) => { - context.pubsub.publish('logger', { severity: 'time', args }) - // eslint-disable-next-line no-console - console.time(...args) - }, - timeEnd: (...args) => { - context.pubsub.publish('logger', { severity: 'timeEnd', args }) - // eslint-disable-next-line no-console - console.timeEnd(...args) - }, - debug: (...args) => { - context.pubsub.publish('logger', { severity: 'debug', args }) - // eslint-disable-next-line no-console - console.debug(...args) - }, - error: (...args) => { - context.pubsub.publish('logger', { severity: 'error', args }) - // eslint-disable-next-line no-console - console.error(...args) - }, - info: (...args) => { - context.pubsub.publish('logger', { severity: 'info', args }) - // eslint-disable-next-line no-console - console.info(...args) - }, - log: (...args) => { - context.pubsub.publish('logger', { severity: 'log', args }) - // eslint-disable-next-line no-console - console.log(...args) - }, - warn: (...args) => { - context.pubsub.publish('logger', { severity: 'warn', args }) - // eslint-disable-next-line no-console - console.warn(...args) - }, - } + middleware: ({ logger, context }) => { + // @ts-expect-error sdf + logger.on('level-change', (level) => { + context.pubsub.publish('logger', { severity: 'info', args: ['log level change', level] }) + }) }, defaultConfig, - dependencies: [ + dependencies: () => [ + { + plugin: Logger.configure({ + logger: { + hooks: { + logMethod(inputArgs, method, level) { + const levelLabel = pino.levels.labels[level] + // todo [>1]: fix so that zembleContext type is consistent + const context = zembleContext as unknown as Zemble.GlobalContext + context.pubsub.publish('logger', { severity: levelLabel, args: inputArgs }) + method.apply(this, inputArgs) + }, + }, + }, + }), + }, { plugin: YogaPlugin.configure({ yoga: { diff --git a/packages/migrations/bin/migrate-down.ts b/packages/migrations/bin/migrate-down.ts index ba2e176e..50a7862f 100755 --- a/packages/migrations/bin/migrate-down.ts +++ b/packages/migrations/bin/migrate-down.ts @@ -12,7 +12,7 @@ const appModule = await import(join(process.cwd(), appFile)) const appOrServe = await appModule.default as Zemble.App | undefined if (appOrServe && 'runBeforeServe' in appOrServe) { - await migrateDown() + await migrateDown({ logger: appOrServe.providers.logger }) } else { console.warn(`Usage: migrate-down [app-file] Will default to "." i.e. the main file in package.json.`) diff --git a/packages/migrations/bin/migrate-up.ts b/packages/migrations/bin/migrate-up.ts index 50971f9e..e8731f91 100755 --- a/packages/migrations/bin/migrate-up.ts +++ b/packages/migrations/bin/migrate-up.ts @@ -12,7 +12,7 @@ const appModule = await import(join(process.cwd(), appFile)) const appOrServe = await appModule.default as Zemble.App | undefined if (appOrServe && 'runBeforeServe' in appOrServe) { - await migrateUp() + await migrateUp({ logger: appOrServe.providers.logger }) } else { console.warn(`Usage: migrate-up [app-file] Will default to "." i.e. the main file in package.json.`) diff --git a/packages/migrations/plugin.ts b/packages/migrations/plugin.ts index 29b4a0d3..f2b23d29 100644 --- a/packages/migrations/plugin.ts +++ b/packages/migrations/plugin.ts @@ -2,6 +2,7 @@ import { Plugin } from '@zemble/core' import { readdir } from 'node:fs/promises' import { join } from 'node:path' +import type { IStandardLogger } from '@zemble/core' import type { JsonValue } from 'type-fest' export type MigrationStatus = { @@ -93,8 +94,9 @@ const getMigrations = async (migrationsDir: string, adapter: MigrationAdapter | let upMigrationsRemaining = [] as readonly MigrationToProcess[] let downMigrationsRemaining = [] as readonly MigrationToProcess[] -export const migrateDown = async (migrateDownCount = 1) => { - console.log(`[@zemble/migrations] migrateDown: ${upMigrationsRemaining.length} migrations to process`) +export const migrateDown = async ({ migrateDownCount = 1, logger }: { readonly migrateDownCount?: number, readonly logger: IStandardLogger }) => { + logger.info(`migrateDown: ${upMigrationsRemaining.length} migrations to process`) + await downMigrationsRemaining.reduce(async (prev, { migrationName, fullPath, adapter, }, index) => { @@ -103,20 +105,20 @@ export const migrateDown = async (migrateDownCount = 1) => { return } - console.log(`[@zemble/migrations] Migrate down: ${migrationName}`) + logger.info(`Migrate down: ${migrationName}`) const { down } = await import(fullPath) as Migration if (down) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await adapter?.down(migrationName, async (context) => down(context ?? {})) } else { - console.warn(`[@zemble/migrations] Migration ${migrationName} did not have a down function.`) + logger.warn(`Migration ${migrationName} did not have a down function.`) } }, Promise.resolve()) } -export const migrateUp = async (migrateUpCount = Infinity) => { - console.log(`[@zemble/migrations] migrateUp: ${upMigrationsRemaining.length} migrations to process`) +export const migrateUp = async ({ logger, migrateUpCount = Infinity }: { readonly logger: IStandardLogger, readonly migrateUpCount?: number }) => { + logger.info(`migrateUp: ${upMigrationsRemaining.length} migrations to process`) await upMigrationsRemaining.reduce(async (prev, { migrationName, fullPath, progress, adapter, }, index) => { @@ -126,7 +128,7 @@ export const migrateUp = async (migrateUpCount = Infinity) => { return } - console.log(`[@zemble/migrations] Migrate up: ${migrationName}`) + logger.info(`Migrate up: ${migrationName}`) const { up } = await import(fullPath) as unknown as { readonly up: Up, readonly down?: Down } if (up) { await adapter?.up(migrationName, async (context) => up({ @@ -150,11 +152,11 @@ const defaultConfig = { waitForMigrationsToComplete: true, } satisfies Omit -export default new Plugin( +const plugin = new Plugin( import.meta.dir, { middleware: (async ({ - plugins, app, config, + plugins, app, config, logger, }) => { const migrationsPathOfApp = join(app.appDir, config.migrationsDir ?? 'migrations') @@ -174,7 +176,7 @@ export default new Plugin( return async () => { if (config.runMigrationsOnStart) { - const completer = migrateUp() + const completer = migrateUp({ logger }) if (config.waitForMigrationsToComplete) { await completer @@ -186,3 +188,5 @@ export default new Plugin( defaultConfig, }, ) + +export default plugin diff --git a/packages/mongodb/plugin.ts b/packages/mongodb/plugin.ts index 1cd590af..69536d70 100644 --- a/packages/mongodb/plugin.ts +++ b/packages/mongodb/plugin.ts @@ -46,10 +46,10 @@ export default new Plugin( import.meta.dir, { middleware: async ({ - app, config, plugins, context, + app, config, plugins, logger, }) => { if (process.env.DEBUG) { - context.logger.log('Connecting to MongoDB', config.url.replace(regexToHidePassword, '***')) + logger.info('Connecting to MongoDB', config.url.replace(regexToHidePassword, '***')) } // we create a global mongodb client for the app, which is also used for all plugins that don't have a custom @@ -57,10 +57,10 @@ export default new Plugin( const defaultClient = new MongoClient(config.url, config.options) defaultClient.on('error', (error) => { - context.logger.error('MongoDB error', error) + logger.error('MongoDB error', error) }) - context.logger.log('Connected to MongoDB') + logger.info('Connected to MongoDB') await defaultClient.connect() @@ -73,7 +73,7 @@ export default new Plugin( const customClient = new MongoClient(config.url, config.options) customClient.on('error', (error) => { - context.logger.error('MongoDB error', error) + logger.error('MongoDB error', error) }) await customClient.connect() diff --git a/packages/node/serve.ts b/packages/node/serve.ts index fe352595..7f8b5661 100644 --- a/packages/node/serve.ts +++ b/packages/node/serve.ts @@ -9,7 +9,7 @@ export const serve = async (config: Configure | Promise | Zemble.App const server = nodeServer({ fetch: app.hono.fetch }) server.addListener('listening', () => { - console.log(`[@zemble/node] Serving on ${JSON.stringify(server.address())}`) + app.providers.logger.info(`[@zemble/node] Serving on ${JSON.stringify(server.address())}`) }) return app diff --git a/packages/pino/.gitignore b/packages/pino/.gitignore new file mode 100644 index 00000000..cb55653e --- /dev/null +++ b/packages/pino/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +node_modules +*.generated.ts +*.generated +.env +*.log \ No newline at end of file diff --git a/packages/pino/.vscode/extensions.json b/packages/pino/.vscode/extensions.json new file mode 100644 index 00000000..a357795c --- /dev/null +++ b/packages/pino/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "oven.bun-vscode" + ] +} \ No newline at end of file diff --git a/packages/pino/README.md b/packages/pino/README.md new file mode 100644 index 00000000..91e9abdc --- /dev/null +++ b/packages/pino/README.md @@ -0,0 +1,13 @@ +# logger + +## Develop + +```bash +bun dev +``` + +## Test + +```bash +bun test +``` \ No newline at end of file diff --git a/packages/pino/codegen.ts b/packages/pino/codegen.ts new file mode 100644 index 00000000..1e6cc7ed --- /dev/null +++ b/packages/pino/codegen.ts @@ -0,0 +1,9 @@ +import defaultConfig from '@zemble/graphql/codegen' + +import type { CodegenConfig } from '@graphql-codegen/cli' + +const config: CodegenConfig = { + ...defaultConfig, +} + +export default config diff --git a/packages/pino/package.json b/packages/pino/package.json new file mode 100644 index 00000000..8d84b804 --- /dev/null +++ b/packages/pino/package.json @@ -0,0 +1,32 @@ +{ + "name": "@zemble/pino", + "version": "0.0.1", + "description": "", + "type": "module", + "keywords": [ + "zemble", + "zemble-plugin", + "@zemble" + ], + "dependencies": { + "@zemble/bun": "workspace:*", + "@zemble/core": "workspace:*", + "pino": "^8.17.2", + "pino-debug": "^2.0.0" + }, + "scripts": { + "test": "bun test", + "dev": "zemble-dev plugin.ts", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@tsconfig/bun": "^1.0.1", + "@types/bun": "latest", + "pino-pretty": "^10.3.1" + }, + "peerDependencies": { + "typescript": "^5.2.2" + }, + "module": "plugin.ts", + "main": "plugin.ts" +} diff --git a/packages/pino/plugin.ts b/packages/pino/plugin.ts new file mode 100644 index 00000000..33ca9575 --- /dev/null +++ b/packages/pino/plugin.ts @@ -0,0 +1,95 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable functional/immutable-data */ +import { Plugin } from '@zemble/core' +import pino from 'pino' +// @ts-expect-error no types available +// eslint-disable-next-line import/no-extraneous-dependencies +import pinoDebug from 'pino-debug' + +import type pinopretty from 'pino-pretty' + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Zemble { + /* interface MiddlewareConfig { + readonly ['zemble-plugin-email-sendgrid']?: Zemble.DefaultMiddlewareConfig + } */ + + interface Providers { + // eslint-disable-next-line functional/prefer-readonly-type + logger: pino.Logger + } + } +} + +interface LoggerConfig extends Zemble.GlobalConfig { + readonly logger?: pino.LoggerOptions +} + +export const defaultProdConfig = { + level: process.env.LOG_LEVEL ?? 'info', +} satisfies pino.LoggerOptions + +export const defaultDevConfig = { + hooks: { + logMethod(inputArgs, method) { + method.apply(this, inputArgs) + }, + }, + transport: { + targets: [ + { + target: 'pino-pretty', + level: process.env.LOG_LEVEL ?? 'debug', + options: { + translateTime: 'HH:MM:ss', + messageFormat: '{if pluginName}[{pluginName}@{pluginVersion}]{end} {if middlewarePluginName}({middlewarePluginName}@{middlewarePluginVersion}){end} {msg}', + ignore: 'pid,req.hostname,req.remoteAddress,req.remotePort,hostname,pluginName,pluginVersion,middlewarePluginName,middlewarePluginVersion', + } satisfies pinopretty.PrettyOptions, + }, + ], + }, +} satisfies pino.LoggerOptions + +export const defaultTestConfig = { + ...defaultDevConfig, + transport: { + ...defaultDevConfig.transport, + targets: defaultDevConfig.transport.targets.map((target) => ({ + ...target, + level: process.env.LOG_LEVEL ?? 'warn', + options: { + ...target.options, + colorize: false, + }, + })), + }, +}satisfies pino.LoggerOptions + +export default new Plugin( + import.meta.dir, + { + middleware: ({ + app, plugins, config, context, + }) => { + const defaultConfig = process.env.NODE_ENV === 'production' ? defaultProdConfig + : (process.env.NODE_ENV === 'test' ? defaultTestConfig + : defaultDevConfig) + + const logger = pino(config.logger ?? defaultConfig) + + app.providers.logger = logger + + context.logger = logger + + pinoDebug(logger) + + plugins.forEach((plugin) => { + plugin.providers.logger = logger.child({ + pluginName: plugin.pluginName, + pluginVersion: plugin.pluginVersion, + }) + }) + }, + }, +) diff --git a/packages/pino/tsconfig.json b/packages/pino/tsconfig.json new file mode 100644 index 00000000..6ea4cd3c --- /dev/null +++ b/packages/pino/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@tsconfig/bun/tsconfig.json", + "exclude": [ "**/*.generated/*.ts" ] +} diff --git a/packages/routes/middleware.ts b/packages/routes/middleware.ts index 22d8a224..c095b4f1 100644 --- a/packages/routes/middleware.ts +++ b/packages/routes/middleware.ts @@ -6,7 +6,7 @@ import initializePlugin from './utils/initializePlugin' import type { RoutesConfig } from './plugin' import type { Middleware } from '@zemble/core/types' -const middleware: Middleware = async ({ app, plugins }) => { +const middleware: Middleware = async ({ app, plugins, logger }) => { await plugins.reduce(async ( prev, { pluginPath, config }, @@ -17,13 +17,16 @@ const middleware: Middleware = async ({ app, plugins }) => { pluginPath, app, config: config.middleware?.['@zemble/routes'] ?? {}, + logger, }) } return undefined }, Promise.resolve(undefined)) - await initializePlugin({ pluginPath: process.cwd(), app, config: {} }) + await initializePlugin({ + pluginPath: process.cwd(), app, config: {}, logger, + }) } export default middleware diff --git a/packages/routes/utils/initializePlugin.ts b/packages/routes/utils/initializePlugin.ts index 174f9037..df3b61c9 100644 --- a/packages/routes/utils/initializePlugin.ts +++ b/packages/routes/utils/initializePlugin.ts @@ -4,6 +4,7 @@ import * as path from 'node:path' import readRoutes from './readRoutes' import type { RoutesGlobalConfig } from '../plugin' +import type { IStandardLogger } from '@zemble/core' import type { MiddlewareHandler } from 'hono' const httpVerbs = [ @@ -91,12 +92,13 @@ const initializeRoutes = async ( routePath: string, app: Pick, config: Omit, + logger: IStandardLogger, ) => { const hasRoutes = fs.existsSync(routePath) const { hono } = app if (hasRoutes) { - const routesAndFilenames = await readRoutes(routePath) + const routesAndFilenames = await readRoutes({ rootDir: routePath, logger }) const routePromises = Object.keys(routesAndFilenames).map(async (route) => { const val = routesAndFilenames[route]! @@ -182,14 +184,16 @@ export async function initializePlugin( pluginPath, app, config, + logger, }: { readonly pluginPath: string; readonly app: Pick readonly config: Omit; + readonly logger: IStandardLogger; }, ) { const routePath = path.join(pluginPath, config.rootPath ?? 'routes') - await initializeRoutes(routePath, app, config) + await initializeRoutes(routePath, app, config, logger) } export default initializePlugin diff --git a/packages/routes/utils/readRoutes.ts b/packages/routes/utils/readRoutes.ts index e9b4bbfe..a8567b00 100644 --- a/packages/routes/utils/readRoutes.ts +++ b/packages/routes/utils/readRoutes.ts @@ -1,15 +1,17 @@ import * as fs from 'node:fs' import * as path from 'node:path' +import type { IStandardLogger } from '@zemble/core' + export type PathsWithMetadata = Record -export const readRoutes = async (rootDir: string, prefix = ''): Promise => fs.readdirSync(path.join(rootDir, prefix)).reduce(async (prev, filename) => { +export const readRoutes = async ({ rootDir, logger }: {readonly rootDir: string, readonly logger: IStandardLogger}, prefix = ''): Promise => fs.readdirSync(path.join(rootDir, prefix)).reduce(async (prev, filename) => { const route = path.join(rootDir, prefix, filename) const tat = fs.statSync(route) if (tat.isDirectory()) { - const newRoutes = await readRoutes(rootDir, path.join(prefix, filename)) + const newRoutes = await readRoutes({ rootDir, logger }, path.join(prefix, filename)) return { ...await prev, ...newRoutes } } @@ -18,7 +20,7 @@ export const readRoutes = async (rootDir: string, prefix = ''): Promise { - console.log('subscribing to randomNumber') + subscribe: (_, __, { pubsub, logger }) => { + logger.info('subscribing to randomNumber') return pubsub.subscribe('randomNumber') }, - resolve: (payload: number) => { - console.log('resolving randomNumber', payload) - return payload - }, + resolve: (payload: number) => payload, } export default randomNumber diff --git a/packages/supabase/graphql/Subscription/tick.ts b/packages/supabase/graphql/Subscription/tick.ts index ce7e549d..c1bed659 100644 --- a/packages/supabase/graphql/Subscription/tick.ts +++ b/packages/supabase/graphql/Subscription/tick.ts @@ -11,9 +11,9 @@ const initializeOnce = (pubsub: Zemble.PubSubType) => { const tick: SubscriptionResolvers['tick'] = { // subscribe to the tick event - subscribe: (_, __, { pubsub }) => { + subscribe: (_, __, { pubsub, logger }) => { initializeOnce(pubsub) - console.log('subscribing to tick') + logger.info('subscribing to tick') return pubsub.subscribe('tick') }, resolve: (payload: number) => payload, diff --git a/packages/supabase/package.json b/packages/supabase/package.json index 4f930dd5..d5263085 100644 --- a/packages/supabase/package.json +++ b/packages/supabase/package.json @@ -21,8 +21,8 @@ "scripts": { "dev": "zemble-dev plugin.ts", "lint": "eslint .", - "typecheck": "bun run codegen && tsc --noEmit", - "codegen": "graphql-codegen", + "typecheck": "tsc --noEmit", + "graphql-codegen": "graphql-codegen", "supabase-codegen": "supabase gen types typescript --project-id \"nptqmmaxmynahsgfuvhn\" --schema public > types/supabase.generated.ts" }, "devDependencies": { diff --git a/packages/todo/graphql/Subscription/todoCreated.ts b/packages/todo/graphql/Subscription/todoCreated.ts index fbfb75d1..465ced46 100644 --- a/packages/todo/graphql/Subscription/todoCreated.ts +++ b/packages/todo/graphql/Subscription/todoCreated.ts @@ -12,7 +12,7 @@ declare global { const todoCreated: SubscriptionResolvers['todoCreated'] = { // subscribe to the todoCreated event subscribe: (_, __, { pubsub, logger }) => { - logger.log('subscribing to todoCreated') + logger.info('subscribing to todoCreated') return pubsub.subscribe('todoCreated') }, resolve: (payload: Todo) => payload, diff --git a/packages/todo/graphql/Subscription/todoUpdated.ts b/packages/todo/graphql/Subscription/todoUpdated.ts index bbc340ec..d9e427c2 100644 --- a/packages/todo/graphql/Subscription/todoUpdated.ts +++ b/packages/todo/graphql/Subscription/todoUpdated.ts @@ -12,7 +12,7 @@ declare global { const todoUpdated: SubscriptionResolvers['todoUpdated'] = { // subscribe to the todoUpdated event subscribe: (_, __, { pubsub, logger }) => { - logger.log('subscribing to todoUpdated') + logger.info('subscribing to todoUpdated') return pubsub.subscribe('todoUpdated') }, resolve: (payload: Todo) => payload,