-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'feat/monitoring' of https://github.com/BouyguesTelecom/…
…graphql-mesh into feat/monitoring
- Loading branch information
Showing
12 changed files
with
1,010 additions
and
830 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
232 changes: 134 additions & 98 deletions
232
packages/graphql-mesh/custom-plugins/monitor-envelop.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,110 +1,146 @@ | ||
import { type Plugin } from '@envelop/core' | ||
import { Logger } from '../utils/logger' | ||
import { NoSchemaIntrospectionCustomRule } from 'graphql'; | ||
import { GraphQLError } from 'graphql'; | ||
import { NoSchemaIntrospectionCustomRule } from 'graphql' | ||
import { GraphQLError } from 'graphql' | ||
/** | ||
* monitor plugin in order to get event contextual log and add some security rules | ||
* useful to | ||
* - log the graphql Query event | ||
* - add desabled introspection validation rule | ||
* - remove suggestion message | ||
* - log the execute result summary with executes duration | ||
* - remove not allowed introspection in result | ||
* Monitor plugin to get event contextual logs and add some security rules. | ||
* Used to: | ||
* - log the GraphQL Query event | ||
* - add a disabled introspection validation rule | ||
* - remove suggestion messages | ||
* - log the execute result summary with execution duration | ||
* - remove not allowed introspection in the result | ||
*/ | ||
|
||
const formatter = (error: GraphQLError, mask: string): GraphQLError => { | ||
if (error instanceof GraphQLError) { | ||
error.message = error.message.replace(/Did you mean ".+"/g, mask); | ||
} | ||
return error as GraphQLError; | ||
}; | ||
if (error instanceof GraphQLError) { | ||
error.message = error.message.replace(/Did you mean ".+"/g, mask) | ||
} | ||
return error as GraphQLError | ||
} | ||
|
||
export default ({ options }): Plugin => { | ||
// not allow by default | ||
const allowIntrospection = options?.introspection?.allow || process.env['ENABLED_INTROSPECTION'] || false | ||
// low info in log by default | ||
const resultLogInfoLevel = options?.resultLogInfoLevel ? options.resultLogInfoLevel : "low" | ||
const denyIntrospectionHeaderName = options?.introspection?.denyHeaderName || null | ||
const denyIntrospectionHeaderValue = options?.introspection?.denyHeaderValue || null | ||
const allowIntrospectionHeaderName = options?.introspection?.allowHeaderName || null | ||
const allowIntrospectionHeaderValue = options?.introspection?.allowHeaderValue || null | ||
const isMaskSuggestion = options?.maskSuggestion?.enabled || false | ||
const maskSuggestionMessage = options?.maskSuggestion?.message || "" | ||
return { | ||
onParse({ context }) { | ||
if (options.logOnParse) { | ||
Logger.graphqlQuery(context['request']['headers'], context['params']) | ||
} | ||
}, | ||
// Not allowed by default | ||
const allowIntrospection = | ||
options?.introspection?.allow || process.env['ENABLED_INTROSPECTION'] || false | ||
// Low info in log by default | ||
const resultLogInfoLevel = options?.resultLogInfoLevel ? options.resultLogInfoLevel : 'low' | ||
const denyIntrospectionHeaderName = options?.introspection?.denyHeaderName || null | ||
const denyIntrospectionHeaderValue = options?.introspection?.denyHeaderValue || null | ||
const allowIntrospectionHeaderName = options?.introspection?.allowHeaderName || null | ||
const allowIntrospectionHeaderValue = options?.introspection?.allowHeaderValue || null | ||
const isMaskSuggestion = options?.maskSuggestion?.enabled || false | ||
const maskSuggestionMessage = options?.maskSuggestion?.message || '' | ||
|
||
onValidate: ({ addValidationRule, context }) => { | ||
const headers = context['request'].headers | ||
let deny = true | ||
/* | ||
allowIntrospection=false : intropection deny for all | ||
denyIntrospectionHeaderName : name of the header to check to deny introspection is deny ex plublic proxy header | ||
allowIntrospectionHeaderName : name of the header allow if this header and value is presents | ||
*/ | ||
// if introspection not allow | ||
if (allowIntrospection) { | ||
// intropection may be allow | ||
deny = false | ||
// is existed a header to deny introspection | ||
if (denyIntrospectionHeaderName) { | ||
if (headers.get(denyIntrospectionHeaderName)) { | ||
if (headers.get(denyIntrospectionHeaderName).includes(denyIntrospectionHeaderValue)) { | ||
Logger.denyIntrospection("onValidate", "deny by headers " + denyIntrospectionHeaderName + ": " + headers.get(denyIntrospectionHeaderName), headers) | ||
deny = true | ||
} | ||
} | ||
} | ||
// is existed a header mandatory to allow introspection | ||
if (allowIntrospectionHeaderName) { | ||
deny = true | ||
if (headers.get(allowIntrospectionHeaderName)) { | ||
if (headers.get(allowIntrospectionHeaderName).includes(allowIntrospectionHeaderValue)) { | ||
Logger.allowIntrospection("onValidate", "allow by headers " + allowIntrospectionHeaderName + ": " + headers.get(allowIntrospectionHeaderName).substring(0, 4) + "...", headers) | ||
deny = false | ||
} else { | ||
Logger.denyIntrospection("onValidate", "deny by bad header value " + allowIntrospectionHeaderName + ": " + headers.get(allowIntrospectionHeaderName).substring(0, 4) + "...", headers) | ||
} | ||
} else { | ||
Logger.denyIntrospection("onValidate", "deny by no header " + allowIntrospectionHeaderName, headers) | ||
} | ||
} | ||
} | ||
if (deny) { | ||
addValidationRule(NoSchemaIntrospectionCustomRule) | ||
} | ||
return { | ||
onParse({ context }) { | ||
if (options.logOnParse) { | ||
Logger.graphqlQuery(context['request']['headers'], context['params']) | ||
} | ||
}, | ||
|
||
return function onValidateEnd({ valid, result, setResult }) { | ||
if (isMaskSuggestion && !valid) { | ||
setResult(result.map((error: GraphQLError) => formatter(error, maskSuggestionMessage))); | ||
} | ||
}; | ||
}, | ||
onValidate: ({ addValidationRule, context }) => { | ||
const headers = context['request'].headers | ||
let deny = true | ||
/* | ||
allowIntrospection = false: introspection denied for all | ||
denyIntrospectionHeaderName: name of the header to check to deny introspection, e.g., public proxy header | ||
allowIntrospectionHeaderName: name of the header to allow if this header and value are present | ||
*/ | ||
if (allowIntrospection) { | ||
// Introspection may be allowed | ||
deny = false | ||
// If there exists a header to deny introspection | ||
if (denyIntrospectionHeaderName) { | ||
if (headers.get(denyIntrospectionHeaderName)) { | ||
if (headers.get(denyIntrospectionHeaderName).includes(denyIntrospectionHeaderValue)) { | ||
Logger.denyIntrospection( | ||
'onValidate', | ||
'Denied by headers ' + | ||
denyIntrospectionHeaderName + | ||
': ' + | ||
headers.get(denyIntrospectionHeaderName), | ||
headers | ||
) | ||
deny = true | ||
} | ||
} | ||
} | ||
// If there exists a mandatory header to allow introspection | ||
if (allowIntrospectionHeaderName) { | ||
deny = true | ||
if (headers.get(allowIntrospectionHeaderName)) { | ||
if (headers.get(allowIntrospectionHeaderName).includes(allowIntrospectionHeaderValue)) { | ||
Logger.allowIntrospection( | ||
'onValidate', | ||
'Allowed by headers ' + | ||
allowIntrospectionHeaderName + | ||
': ' + | ||
headers.get(allowIntrospectionHeaderName).substring(0, 4) + | ||
'...', | ||
headers | ||
) | ||
deny = false | ||
} else { | ||
Logger.denyIntrospection( | ||
'onValidate', | ||
'Denied by bad header value ' + | ||
allowIntrospectionHeaderName + | ||
': ' + | ||
headers.get(allowIntrospectionHeaderName).substring(0, 4) + | ||
'...', | ||
headers | ||
) | ||
} | ||
} else { | ||
Logger.denyIntrospection( | ||
'onValidate', | ||
'Denied by missing header ' + allowIntrospectionHeaderName, | ||
headers | ||
) | ||
} | ||
} | ||
} | ||
if (deny) { | ||
addValidationRule(NoSchemaIntrospectionCustomRule) | ||
} | ||
|
||
onExecute(/*{ args, extendContext }*/) { | ||
let timestampDebut = new Date().getTime() | ||
return { | ||
before() { | ||
return function onValidateEnd({ valid, result, setResult }) { | ||
if (isMaskSuggestion && !valid) { | ||
setResult(result.map((error: GraphQLError) => formatter(error, maskSuggestionMessage))) | ||
} | ||
} | ||
}, | ||
|
||
timestampDebut = new Date().getTime() | ||
}, | ||
onExecuteDone({ result, args }) { | ||
const timestampDone = new Date().getTime(); | ||
// short cut to desabled introspection response in case of bad configuration rule | ||
if (!allowIntrospection && args.contextValue['params'].query.includes('__schema')) { | ||
result['data'] = {} | ||
result['errors'] = [{ message: "Fordidden" }] | ||
Logger.error('SECU', 'onExecute', 'Introspection query deteted not allowed', args.contextValue['params']) | ||
} | ||
if (options.loOnExecuteDone) { | ||
Logger.endExec(args.contextValue['request']['headers'], result, timestampDone - timestampDebut, resultLogInfoLevel) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
onExecute(/* { args, extendContext } */) { | ||
let timestampStart = new Date().getTime() | ||
return { | ||
before() { | ||
timestampStart = new Date().getTime() | ||
}, | ||
onExecuteDone({ result, args }) { | ||
const timestampEnd = new Date().getTime() | ||
// Shortcut to disable introspection response in case of bad configuration rule | ||
if (!allowIntrospection && args.contextValue['params'].query.includes('__schema')) { | ||
result['data'] = {} | ||
result['errors'] = [{ message: 'Forbidden' }] | ||
Logger.error( | ||
'SECU', | ||
'onExecute', | ||
'Introspection query detected and not allowed', | ||
args.contextValue['params'] | ||
) | ||
} | ||
if (options.logOnExecuteDone) { | ||
Logger.endExec( | ||
args.contextValue['request']['headers'], | ||
result, | ||
timestampEnd - timestampStart, | ||
resultLogInfoLevel | ||
) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,65 +1,63 @@ | ||
import { type Plugin } from '@envelop/core'; | ||
import { type Plugin } from '@envelop/core' | ||
|
||
import { Logger } from '../utils/logger' | ||
|
||
/** | ||
* monitor fetch source | ||
* use to : | ||
* - add log event for each fetch like, url, response status, duration | ||
* Monitor fetch source | ||
* Used to: | ||
* - add log events for each fetch such as URL, response status, and duration | ||
*/ | ||
|
||
export default ({ options }) => { | ||
return <Plugin>{ | ||
onFetch({ context, info }) { | ||
if (!info) { | ||
Logger.warn("noFeychInfo", "onFetch", "no info in on fetch") | ||
return; | ||
} | ||
const start = Date.now(); | ||
let rawSource = context[info.sourceName] | ||
let description = info.parentType._fields[info.path.key].description | ||
return <Plugin>{ | ||
onFetch({ context, info }) { | ||
if (!info) { | ||
Logger.warn('noFetchInfo', 'onFetch', 'No info in on fetch') | ||
return | ||
} | ||
const start = Date.now() | ||
let rawSource = context[info.sourceName] | ||
let description = info.parentType._fields[info.path.key].description | ||
|
||
return (fetch: any) => { | ||
if (options.logOnFetch) { | ||
const duration = Date.now() - start; | ||
let fetchInfo = {} | ||
let httpStatus = null | ||
let url = null | ||
if (options.fullFetchInfo) { | ||
fetchInfo = { | ||
fieldName: info.fieldName, | ||
sourceName: info.sourceName, | ||
pathKey: info.path.key, | ||
operation: info.operation.name, | ||
variables: info.variables, | ||
endpoint: rawSource.rawSource.handler.config.endpoint, | ||
description: description | ||
} | ||
} else { | ||
fetchInfo = { | ||
fieldName: info.fieldName, | ||
pathKey: info.path.key, | ||
operation: info.operation.name, | ||
variables: info.variableValues, | ||
endpoint: rawSource.rawSource.handler.config.endpoint, | ||
} | ||
} | ||
//const fetchResponseInfo = {} | ||
if (fetch.response) { | ||
|
||
httpStatus = fetch.response.status | ||
url = fetch.response.url | ||
const options = fetch.response.options | ||
if (options) { | ||
fetchInfo['options'] = { | ||
requestId: options.headers['x-request-id'], | ||
server: options.headers['server'] | ||
} | ||
} | ||
} | ||
Logger.onFetch(context.request, url, httpStatus, duration, fetchInfo) | ||
} | ||
} | ||
} | ||
} | ||
return (fetch: any) => { | ||
if (options.logOnFetch) { | ||
const duration = Date.now() - start | ||
let fetchInfo = {} | ||
let httpStatus = null | ||
let url = null | ||
if (options.fullFetchInfo) { | ||
fetchInfo = { | ||
fieldName: info.fieldName, | ||
sourceName: info.sourceName, | ||
pathKey: info.path.key, | ||
operation: info.operation.name, | ||
variables: info.variables, | ||
endpoint: rawSource.rawSource.handler.config.endpoint, | ||
description: description | ||
} | ||
} else { | ||
fetchInfo = { | ||
fieldName: info.fieldName, | ||
pathKey: info.path.key, | ||
operation: info.operation.name, | ||
variables: info.variableValues, | ||
endpoint: rawSource.rawSource.handler.config.endpoint | ||
} | ||
} | ||
if (fetch.response) { | ||
httpStatus = fetch.response.status | ||
url = fetch.response.url | ||
const options = fetch.response.options | ||
if (options) { | ||
fetchInfo['options'] = { | ||
requestId: options.headers['x-request-id'], | ||
server: options.headers['server'] | ||
} | ||
} | ||
} | ||
Logger.onFetch(context.request, url, httpStatus, duration, fetchInfo) | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.