Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monitoring #33

Closed
wants to merge 6 commits into from
Closed
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
303 changes: 288 additions & 15 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@
"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",
"server": "npm run server -w graphql-mesh"
},
"devDependencies": {
"concurrently": "^8.2.2",
"patch-package": "^8.0.0"
},
"engines": {
"node": "18"
},
"packageManager": "[email protected]"
"packageManager": "[email protected]",
"dependencies": {
"prom-client": "^15.1.3",
"uuid": "^10.0.0"
}
}
111 changes: 111 additions & 0 deletions packages/graphql-mesh/custom-plugins/monitor-envelop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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 introspection validation rule
* - remove suggestion message
* - log the execute result summary with executes duration
* - remove not allowed introspection 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 allowIntrospection in production
const allowIntrospection = process.env['IS_PROUCTION_ENV'] != 'true' && (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'])
}
},

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 function onValidateEnd({ valid, result, setResult }) {
if (isMaskSuggestion && !valid) {
setResult(result.map((error: GraphQLError) => formatter(error, maskSuggestionMessage)));
}
};
},

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 (!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)
}
}
}
}
}
}


65 changes: 65 additions & 0 deletions packages/graphql-mesh/custom-plugins/monitor-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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) => {
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)
}
}
}
}
}
118 changes: 118 additions & 0 deletions packages/graphql-mesh/custom-plugins/monitor-yoga.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Plugin } from 'graphql-yoga'
import { Logger } from '../utils/logger'
import { GraphQLError } from 'graphql'
import { v4 as uuidv4 } from 'uuid'
/**
* 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 error 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 responseLogInfoLevel = options?.responseLogInfoLevel ? options.responseLogInfoLevel : "low"
const resultLogInfoLevel = options?.resultLogInfoLevel ? options.resultLogInfoLevel : "medium"

return {
onRequest({ request/*, fetchAPI, endResponse */ }) {
if (options.LogOnRequest) {
// log only graphql request, avoid log other request like metric requests
if (request.url.includes("/graphql")) {
Logger.onRequest(request)
}
}

// add resuestTimestamp in headers
const timestamp = new Date().getTime();
request.headers.append("requestTimestamp", String(timestamp))

// add x-request-id in header if not present
if (!request.headers.get('x-request-id')) {
request.headers.append("x-request-id", uuidv4())
}

},
onRequestParse(args) {
const beforeTimestamp = new Date().getTime();
let requestHeaders = args.request.headers
return {
onRequestParseDone(nRequestParseDoneEventPayload) {
const timestamp = new Date().getTime();
if (options.logOnRequestParseDone) {
Logger.onRequestParseDone(requestHeaders, nRequestParseDoneEventPayload.requestParserResult['query'], nRequestParseDoneEventPayload.requestParserResult['operationName'], nRequestParseDoneEventPayload.requestParserResult['variables'], timestamp - beforeTimestamp)
}
if (nRequestParseDoneEventPayload.requestParserResult['query'].includes('__schema')) {
Logger.introspection( requestHeaders, nRequestParseDoneEventPayload.requestParserResult['query'])
}
}
}
},
onResultProcess(args) {
if (options.logOnResultProcess) {
// calculate duration from request timestamp
let requestTimestamp: number = 0
if (args.request['headers']) {
const requestTimestampString = args.request['headers'].get('requesttimestamp')
if (requestTimestampString) {
requestTimestamp = parseInt(requestTimestampString)
}
}
const responseTimestamp = new Date().getTime();
Logger.onResultProcess(args.request, args.result, requestTimestamp > 0 ? responseTimestamp - requestTimestamp : 0, resultLogInfoLevel)
}
// if we want to replace all message with a generic message
if (isMaskErrors) {
if (args.result['errors']) {
let errors = args.result['errors']
for (let i = 0; i < errors.length; i++) {
errors[i] = errorMaskMessage
}
}
} else {
// if we want to filter error to only return the message, don't return extend information like stacktrace
if (isFilterError) {
if (args.result['errors']) {
let errors = args.result['errors']
for (let i = 0; i < errors.length; i++) {
errors[i] = new GraphQLError(filterErrorMessage(errors[i]['message']))
}

}

}
}
},

onResponse({ request, response }) {
// dont log options http
if (request.method != 'OPTIONS') {
if (options.logOnResponse) {
// only log graphql request don't log metrics or other requests
if (request.url.includes("/graphql")) {
Logger.onResponse(request, response, responseLogInfoLevel)
}
}
}
}
}
}

/** filterErrorMessage
* use to filter error message :
* - remove disabled introspection
* todo: add other filter rules to remove non expecting message
*/
function filterErrorMessage(message: string) {
if (message.includes("introspection has been disabled")) {
return "forbidden"
}
return message
}
Loading
Loading