Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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": "[email protected]",
"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
Loading