Skip to content

Commit

Permalink
Winston Logger Adapter (#622)
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan authored Feb 13, 2025
1 parent a12edad commit 16f9bd9
Show file tree
Hide file tree
Showing 9 changed files with 541 additions and 18 deletions.
30 changes: 30 additions & 0 deletions .changeset/wild-balloons-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
'@graphql-hive/logger-winston': major
---

**Winston Adapter**

Now you can integrate [Winston](https://github.com/winstonjs/winston) into Hive Gateway on Node.js

```ts
import { defineConfig } from '@graphql-hive/gateway'
import { createLogger, format, transports } from 'winston'
import { createLoggerFromWinston } from '@graphql-hive/winston'

// Create a Winston logger
const winstonLogger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.json()
),
transports: [
new transports.Console()
]
})

export const gatewayConfig = defineConfig({
// Create an adapter for Winston
logging: createLoggerFromWinston(winstonLogger)
})
```
2 changes: 1 addition & 1 deletion packages/fusion-runtime/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ export function wrapExecutorWithHooks({
}
loggerForExecutionRequest.set(baseExecutionRequest, execReqLogger);
}
execReqLogger = execReqLogger?.child?.(subgraphName);
execReqLogger = execReqLogger?.child?.({ subgraph: subgraphName });
if (onSubgraphExecuteHooks.length === 0) {
return baseExecutor(baseExecutionRequest);
}
Expand Down
62 changes: 62 additions & 0 deletions packages/logger-winston/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"name": "@graphql-hive/logger-winston",
"version": "0.0.0",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/graphql-hive/gateway.git",
"directory": "packages/logger-winston"
},
"homepage": "https://the-guild.dev/graphql/hive/docs/gateway",
"author": {
"email": "contact@the-guild.dev",
"name": "The Guild",
"url": "https://the-guild.dev"
},
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"main": "./dist/index.js",
"exports": {
".": {
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
},
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./package.json": "./package.json"
},
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "pkgroll --clean-dist",
"prepack": "yarn build"
},
"peerDependencies": {
"graphql": "^15.9.0 || ^16.9.0",
"winston": "^3.17.0"
},
"peerDependenciesMeta": {
"@parcel/watcher": {
"optional": true
}
},
"dependencies": {
"@graphql-mesh/types": "^0.103.6",
"@whatwg-node/disposablestack": "^0.0.5",
"tslib": "^2.8.1"
},
"devDependencies": {
"graphql": "16.10.0",
"pkgroll": "2.8.2",
"winston": "^3.17.0"
},
"sideEffects": false
}
102 changes: 102 additions & 0 deletions packages/logger-winston/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type {
LazyLoggerMessage,
Logger as MeshLogger,
} from '@graphql-mesh/types';
import { DisposableSymbols } from '@whatwg-node/disposablestack';
import type { Logger as WinstonLogger } from 'winston';

function prepareArgs(messageArgs: LazyLoggerMessage[]) {
const flattenedMessageArgs = messageArgs
.flat(Infinity)
.flatMap((messageArg) => {
if (typeof messageArg === 'function') {
messageArg = messageArg();
}
if (messageArg?.toJSON) {
messageArg = messageArg.toJSON();
}
if (messageArg instanceof AggregateError) {
return messageArg.errors;
}
return messageArg;
});
let message: string = '';
const extras: any[] = [];
for (let messageArg of flattenedMessageArgs) {
if (messageArg == null) {
continue;
}
const typeofMessageArg = typeof messageArg;
if (
typeofMessageArg === 'string' ||
typeofMessageArg === 'number' ||
typeofMessageArg === 'boolean'
) {
message = message ? message + ', ' + messageArg : messageArg;
} else if (typeofMessageArg === 'object') {
extras.push(messageArg);
}
}
return [message, ...extras] as const;
}

class WinstonLoggerAdapter implements MeshLogger, Disposable {
public name?: string;
constructor(
private winstonLogger: WinstonLogger,
private meta: Record<string, any> = {},
) {
if (meta['name']) {
this.name = meta['name'];
}
}
log(...args: any[]) {
if (this.winstonLogger.isInfoEnabled()) {
this.winstonLogger.info(...prepareArgs(args));
}
}
info(...args: any[]) {
if (this.winstonLogger.isInfoEnabled()) {
this.winstonLogger.info(...prepareArgs(args));
}
}
warn(...args: any[]) {
if (this.winstonLogger.isWarnEnabled()) {
this.winstonLogger.warn(...prepareArgs(args));
}
}
error(...args: any[]) {
if (this.winstonLogger.isErrorEnabled()) {
this.winstonLogger.error(...prepareArgs(args));
}
}
debug(...lazyArgs: LazyLoggerMessage[]) {
if (this.winstonLogger.isDebugEnabled()) {
this.winstonLogger.debug(...prepareArgs(lazyArgs));
}
}
child(nameOrMeta: string | Record<string, string | number>) {
if (typeof nameOrMeta === 'string') {
nameOrMeta = {
name: this.name
? this.name.includes(nameOrMeta)
? this.name
: `${this.name}, ${nameOrMeta}`
: nameOrMeta,
};
}
return new WinstonLoggerAdapter(this.winstonLogger.child(nameOrMeta), {
...this.meta,
...nameOrMeta,
});
}
[DisposableSymbols.dispose]() {
return this.winstonLogger.close();
}
}

export function createLoggerFromWinston(
winstonLogger: WinstonLogger,
): WinstonLoggerAdapter {
return new WinstonLoggerAdapter(winstonLogger);
}
147 changes: 147 additions & 0 deletions packages/logger-winston/tests/winston.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { Writable } from 'node:stream';
import { describe, expect, it } from 'vitest';
import * as winston from 'winston';
import { createLoggerFromWinston } from '../src';

describe('Winston', () => {
let log = '';
let lastCallback = () => {};
const stream = new Writable({
write(chunk, _encoding, callback) {
log = chunk.toString('utf-8');
lastCallback = callback;
},
});
const logLevels = ['error', 'warn', 'info', 'debug'] as const;
for (const level of logLevels) {
describe(`Level: ${level}`, () => {
it('basic', () => {
const logger = winston.createLogger({
level,
format: winston.format.json(),
transports: [
new winston.transports.Stream({
stream,
}),
],
});
using loggerAdapter = createLoggerFromWinston(logger);
const testData = [
'Hello',
['World'],
{ foo: 'bar' },
42,
true,
null,
undefined,
() => 'Expensive',
];
loggerAdapter[level](...testData);
lastCallback();
const logJson = JSON.parse(log);
expect(logJson).toEqual({
level,
foo: 'bar',
message: 'Hello, World, 42, true, Expensive',
});
});
it('child', () => {
const logger = winston.createLogger({
level,
format: winston.format.json(),
transports: [
new winston.transports.Stream({
stream,
}),
],
});
const loggerAdapter = createLoggerFromWinston(logger);
const testData = [
'Hello',
['World'],
{ foo: 'bar' },
42,
true,
null,
undefined,
() => 'Expensive',
];
const childLogger = loggerAdapter.child('child');
childLogger[level](...testData);
lastCallback();
const logJson = JSON.parse(log);
expect(logJson).toEqual({
level,
foo: 'bar',
message: 'Hello, World, 42, true, Expensive',
name: 'child',
});
});
it('deduplicate names', () => {
const logger = winston.createLogger({
level,
format: winston.format.json(),
transports: [
new winston.transports.Stream({
stream,
}),
],
});
const loggerAdapter = createLoggerFromWinston(logger);
const testData = [
'Hello',
['World'],
{ foo: 'bar' },
42,
true,
null,
undefined,
() => 'Expensive',
];
const childLogger = loggerAdapter.child('child').child('child');
childLogger[level](...testData);
lastCallback();
const logJson = JSON.parse(log);
expect(logJson).toEqual({
level,
foo: 'bar',
message: 'Hello, World, 42, true, Expensive',
name: 'child',
});
});
it('nested', () => {
const logger = winston.createLogger({
level,
format: winston.format.json(),
transports: [
new winston.transports.Stream({
stream,
}),
],
});
const loggerAdapter = createLoggerFromWinston(logger);
const testData = [
'Hello',
['World'],
{ foo: 'bar' },
42,
true,
null,
undefined,
() => 'Expensive',
];
const childLogger = loggerAdapter.child('child');
const nestedLogger = childLogger.child('nested');
nestedLogger[level](...testData);
lastCallback();
const logJson = JSON.parse(log);
expect(logJson).toEqual({
level,
foo: 'bar',
message: 'Hello, World, 42, true, Expensive',
name: 'child, nested',
});
});
});
}
});
10 changes: 3 additions & 7 deletions packages/runtime/src/plugins/useFetchDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,11 @@ export function useFetchDebug<TContext extends Record<string, any>>(opts: {
onYogaInit({ yoga }) {
fetchAPI = yoga.fetchAPI;
},
onFetch({ url, options, logger = opts.logger, requestId }) {
onFetch({ url, options, logger = opts.logger }) {
const fetchId = fetchAPI.crypto.randomUUID();
const loggerMeta: Record<string, string> = {
const fetchLogger = logger.child({
fetchId,
};
if (requestId) {
loggerMeta['requestId'] = requestId;
}
const fetchLogger = logger.child(loggerMeta);
});
const httpFetchRequestLogger = fetchLogger.child('http-fetch-request');
httpFetchRequestLogger.debug(() => ({
url,
Expand Down
10 changes: 3 additions & 7 deletions packages/runtime/src/plugins/useSubgraphExecuteDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,11 @@ export function useSubgraphExecuteDebug<
onYogaInit({ yoga }) {
fetchAPI = yoga.fetchAPI;
},
onSubgraphExecute({ executionRequest, logger = opts.logger, requestId }) {
onSubgraphExecute({ executionRequest, logger = opts.logger }) {
const subgraphExecuteId = fetchAPI.crypto.randomUUID();
const loggerMeta: Record<string, string> = {
const subgraphExecuteHookLogger = logger.child({
subgraphExecuteId,
};
if (requestId) {
loggerMeta['requestId'] = requestId;
}
const subgraphExecuteHookLogger = logger.child(loggerMeta);
});
if (executionRequest) {
const subgraphExecuteStartLogger = subgraphExecuteHookLogger.child(
'subgraph-execute-start',
Expand Down
Loading

0 comments on commit 16f9bd9

Please sign in to comment.