Skip to content

Commit

Permalink
add plugin monitor
Browse files Browse the repository at this point in the history
  • Loading branch information
Thierry DEGREMONT committed Jul 19, 2024
1 parent b2da181 commit bbbc95a
Show file tree
Hide file tree
Showing 14 changed files with 1,202 additions and 154 deletions.
384 changes: 272 additions & 112 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"postinstall": "npm run postinstall -w graphql-mesh && patch-package && npm run generate:mesh:lock",
"build:local:packages": "concurrently \"npm run pack -w directive-spl\" \"npm run pack -w inject-additional-transforms\"",
"preinstall": "npm run build:local:packages",
"start": "npm start -w graphql-mesh"
"start": "npm start -w graphql-mesh",
"startmesh": "npm run startmesh -w graphql-mesh"
},
"devDependencies": {
"concurrently": "^8.2.2",
Expand Down
63 changes: 63 additions & 0 deletions packages/graphql-mesh/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,66 @@ serve:
browser: false

skipSSLValidation: true
#additionalEnvelopPlugins: "./custom-plugins"
# ...
plugins:
- filter-null:
filter: true
- monitor-fetch:
options:
logLevel: INFO
fullFetcInfo: true
responseInfo: true
- monitor-yoga:
options:
logLevel: INFO
maskError:
enabled: false
message: "error"
filterError:
enabled: true
resultLogInfoLevel: "low"
#masErrorMessage: "somthing is wrong"
- monitor-envelop:
options:
denyInstropection: false
logLevel: INFOG
maskSuggestion:
enabled: true
message: "no suggestion"

- prometheus:
# all optional, and by default, all set to false, please opt-in to the metrics you wish to get

# requires `execute` to be true
requestCount: true
# requires `execute` to be true
requestSummary: true
parse: true
validate: true
contextBuilding: true
execute: true
errors: true

# reports metrics for the delegation to the individual sources
delegation: true

# reports metrics for the outgoing HTTP requests
fetchMetrics: true
# Adds the request headers to the metrics
fetchRequestHeaders: true
# Adds the response headers to the metrics
fetchResponseHeaders: true

# reports metrics for the incoming HTTP requests (this sets a custom name for http)
# If you pass a string instead of boolean, it will be used as the name of the metric
http: true
# Adds the request headers to the metrics
httpRequestHeaders: true
# Adds the response headers to the metrics
httpResponseHeaders: true

# by default all fields are reported
deprecatedFields: true
# the path of the endpoint to expose the metrics, default is /metrics
endpoint: /metrics
51 changes: 51 additions & 0 deletions packages/graphql-mesh/custom-plugins/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { type Plugin } from '@envelop/core';

import {Logger} from '../utils/logger'


export default () => {
return <Plugin>{
onFetch({ context, info }) {

if (!info) {
return;
}
//rawSource.rawSource.handler.config.source
//info.parentType._fields.getFeatureToggles_v1.description
//info.variableValues
const start = Date.now();
let rawSource=context[info.sourceName]
console.log("rawSource", rawSource)
let endpoint=rawSource.rawSource.handler.config.endpoint
let operation=info.operation.name
let description=info.parentType._fields[info.path.key].description
return (tt) => {
const duration = Date.now() - start;
let response=tt.response
let status=response.options.status
let url=response.options.url
//const timing = `${info.fieldName};desc="${info.fieldName} (${info.sourceName})";dur=${duration}`;
Logger.onFetch(context.request,info.fieldName,info.sourceName,info.path.key,endpoint,operation,info.variableValues,description,duration)
};
},

onExecute() {
return {
onExecuteDone({ args }) {
// @ts-ignore
const { timings } = args.contextValue;
if (!timings) {
return;
}

// @ts-ignore
args.contextValue.res.setHeader?.(
'Server-Timing',
timings.join(', ')
);
},
};
},

};
};
71 changes: 71 additions & 0 deletions packages/graphql-mesh/custom-plugins/monitor-envelop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { type Plugin } from '@envelop/core'
import { Logger } from '../utils/logger'
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 instropection alidation rule
* - remove suggestion
* - log the execute result summary with executes duration
* - remove not allowed instropection in 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;
};
export default ({ options }): Plugin => {
// not allow by default
// do not enabled enabledIntrospection in production
const enabledIntrospection = process.env['IS_PROUCTION_ENV'] != 'true' && ( options.enabledIntrospection || process.env['ENABLED_INTROSPECTION'] || false )
// low info in log by default
const resultLogInfoLevel= options.resultLogInfoLevel ? options.resultLogInfoLevel : "low"

return {
onParse({ params, context }) {
Logger.graphqlQuery(context['request']['headers'], context['params'])
/*return ({ result, context, replaceParseResult }) => {
Logger.info('LOG', 'onParse', 'result', result)
}*/
},

onValidate: ({ addValidationRule, context }) => {
if (!enabledIntrospection) {
addValidationRule(NoSchemaIntrospectionCustomRule)
}
return function onValidateEnd({ valid, result, setResult }) {
if (options.maskSuggestion.enabled && !valid) {
setResult(result.map((error) => formatter(error, options.maskSuggestion.message)));
}
};
},

onExecute({ args, extendContext }) {

let timestampDebut = new Date().getTime()
return {
before() {

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 (!enabledIntrospection && args.contextValue['params'].query.includes('__schema')) {
result['data'] = {}
result['errors'] = [{ message: "Fordidden" }]
Logger.error('SECU', 'onExecute', 'Intropection query deteted not allowed', args.contextValue['params'])
}
Logger.endExec(args.contextValue['request']['headers'], result, timestampDone - timestampDebut, resultLogInfoLevel)
}
}
}
}
}


81 changes: 81 additions & 0 deletions packages/graphql-mesh/custom-plugins/monitor-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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
*/

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 (fetch: any) => {
const duration = Date.now() - start;
let fetchInfo = {}
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) {

fetchResponseInfo['status'] = fetch.response.status
fetchResponseInfo['url'] = fetch.response.url
const options = fetch.response.options
if (options) {
fetchResponseInfo['options'] = {
requestId: options.headers['x-request-id'],
server: options.headers['server']
}
}
}
Logger.onFetch(context.request, fetchInfo, fetchResponseInfo, duration)
};
},

onExecute() {
return {
onExecuteDone({ args }) {
// @ts-ignore
const { timings } = args.contextValue;
if (!timings) {
return;
}

// @ts-ignore
args.contextValue.res.setHeader?.(
'Server-Timing',
timings.join(', ')
);
},
};
},

};
};
83 changes: 83 additions & 0 deletions packages/graphql-mesh/custom-plugins/monitor-yoga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Plugin } from 'graphql-yoga'
import { Logger } from '../utils/logger'
import { GraphQLError } from 'graphql'

/**
* monitor plugin in order to get event contextual log and add some security rules
* useful to :
* - log new request comming event
* - add request timestamp in headers to get duration time
* - monitor instropection request
* - mask error in result is required ( ex in production )
* - log response sumary event
* - remove a eventualy not allowed instropection data in result
*/
export function useYagaMonitoring({ options }): Plugin {
const isMaskErrors = options.maskError?.enabled || process.env['MASK_ERRORS'] || false
// filter in production anyway
const isFilterError = options.filterError?.enabled || process.env['FILTER_ERRORS'] == 'true' || process.env['IS_PROUCTION_ENV'] == 'true' || false

const errorMaskMessage = options.maskError?.message ? options.maskError.message : "something goes wrong"
const reponseLogInfoLevel = options.reponseLogInfoLevel ? options.reponseLogInfoLevel : "low"
const resultLogInfoLevel = options.resultLogInfoLevel ? options.resultLogInfoLevel : "low"


return {
onRequest({ request/*, fetchAPI, endResponse */ }) {
Logger.onRequest(request)

// add resuestTimestamp in headers
const timestamp = new Date().getTime();
request.headers.append("requestTimestamp", String(timestamp))
},
onRequestParse(args) {
const beforeTimestamp = new Date().getTime();
let requestHeaders = args.request.headers
return {
onRequestParseDone(nRequestParseDoneEventPayload) {
const timestamp = new Date().getTime();
Logger.onRequestParseDone(requestHeaders, nRequestParseDoneEventPayload.requestParserResult['query'], nRequestParseDoneEventPayload.requestParserResult['operationName'], nRequestParseDoneEventPayload.requestParserResult['variables'], timestamp - beforeTimestamp)
if (nRequestParseDoneEventPayload.requestParserResult['query'].includes('__schema')) {
Logger.info("IntrospectionQuery", "onRequestParseDone", "introspection detected", nRequestParseDoneEventPayload.requestParserResult['query'])
}
}
}
},
onResultProcess(args) {
Logger.onResultProcess(args.request, args.result, resultLogInfoLevel)
if (isMaskErrors) {
if (args.result['errors']) {
let errors = args.result['errors']
for (let i = 0; i < errors.length; i++) {
errors[i] = errorMaskMessage
}
}
} else {
if (isFilterError) {
if (args.result['errors']) {
let errors = args.result['errors']

for (let i = 0; i < errors.length; i++) {
errors[i]= new GraphQLError(filter(errors[i]['message']))
}

}

}
}
},

onResponse({ request, response }) {
if (request.method != 'OPTION') {
Logger.onResponse(request, response, reponseLogInfoLevel)
}
}
}
}

function filter(message: string) {
if (message.includes("introspection has been disabled")) {
return "forbidden"
}
return message
}
Binary file modified packages/graphql-mesh/local-pkg/directive-spl-1.0.0.tgz
Binary file not shown.
Binary file not shown.
Loading

0 comments on commit bbbc95a

Please sign in to comment.