Skip to content

Commit

Permalink
Merge branch 'feat/monitoring' of https://github.com/BouyguesTelecom/…
Browse files Browse the repository at this point in the history
…graphql-mesh into feat/monitoring
  • Loading branch information
Thierry DEGREMONT committed Sep 16, 2024
2 parents efe6300 + 3303f98 commit 36c96b9
Show file tree
Hide file tree
Showing 12 changed files with 1,010 additions and 830 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
"preinstall": "npm run build:local:packages",
"start": "npm start -w graphql-mesh",
"startmesh": "npm run startmesh -w graphql-mesh",
"serve": "npm run serve -w graphql-mesh"
},
"serve": "npm run serve -w graphql-mesh"
},
"devDependencies": {
"concurrently": "^8.2.2",
"patch-package": "^8.0.0"
Expand Down
232 changes: 134 additions & 98 deletions packages/graphql-mesh/custom-plugins/monitor-envelop.ts
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
)
}
}
}
}
}
}


110 changes: 54 additions & 56 deletions packages/graphql-mesh/custom-plugins/monitor-fetch.ts
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)
}
}
}
}
}
Loading

0 comments on commit 36c96b9

Please sign in to comment.