diff --git a/pjs/config.json b/pjs/config.json index 541d6be..f30964f 100644 --- a/pjs/config.json +++ b/pjs/config.json @@ -132,6 +132,7 @@ "http/access-log.js", "http/auth.js", "http/route.js", + "http/fault-injection.js", "filter/request-redirect.js", "filter/header-modifier.js", "filter/url-rewrite.js", @@ -156,6 +157,7 @@ "http/access-log.js", "http/auth.js", "http/route.js", + "http/fault-injection.js", "filter/request-redirect.js", "filter/header-modifier.js", "filter/url-rewrite.js", diff --git a/pjs/http/fault-injection.js b/pjs/http/fault-injection.js new file mode 100644 index 0000000..c54f54d --- /dev/null +++ b/pjs/http/fault-injection.js @@ -0,0 +1,108 @@ +(( + { config, isDebugEnabled } = pipy.solve('config.js'), + + delayFaultCache = new algo.Cache( + route => ( + ( + range, + min = 0, + max = 0, + unit = 0.001, // default ms + delay = route?.config?.Fault?.Delay || __domain?.Fault?.Delay || config?.Configs?.Fault?.Delay, + percent = delay?.Percent, + fixed = delay?.Fixed, + ) => ( + percent > 0 && ( + delay.Unit?.toLowerCase?.() === 's' ? ( + unit = 1 + ) : delay.Unit?.toLowerCase?.() === 'm' && ( + unit = 60 + ), + delay.Range ? ( + range = delay.Range.split('-'), + min = range[0], + max = range[1], + min >= 0 && max >= min && ( + () => ( + Math.floor(Math.random() * 100) <= percent ? ( + (Math.floor(Math.random() * (max - min + 1)) + min) * unit + ) : 0 + ) + ) + ) : fixed > 0 && ( + fixed *= unit, + () => ( + Math.floor(Math.random() * 100) <= percent ? ( + fixed + ) : 0 + ) + ) + ) || null + ) + )() + ), + + abortFaultCache = new algo.Cache( + route => ( + ( + abort = route?.config?.Fault?.Abort || __domain?.Fault?.Abort || config?.Configs?.Fault?.Abort, + percent = abort?.Percent, + status = abort?.Status, + message, + ) => ( + percent > 0 && status > 0 && ( + __domain.RouteType === 'GRPC' ? ( + message = new Message({ status: 200, headers: { 'content-type': 'application/grpc', 'grpc-encoding': 'identity' } }, null, { headers: { 'grpc-status': status, 'grpc-message': abort?.Message || '' } }) + ) : ( + message = new Message({ status }, abort?.Message || '') + ), + () => ( + Math.floor(Math.random() * 100) <= percent ? ( + message + ) : null + ) + ) || null + ) + )() + ), + +) => pipy({ + _timeout: 0, + _message: null, +}) + +.import({ + __domain: 'route', + __route: 'route', +}) + +.pipeline() +.branch( + () => (_timeout = delayFaultCache.get(__route)?.()) > 0, ( + $=>$.handleMessageStart( + () => new Timeout(_timeout).wait() + ) + ), ( + $=>$ + ) +) +.branch( + () => _message = abortFaultCache.get(__route)?.(), ( + $=>$.replaceMessage( + () => _message + ) + ), ( + $=>$.chain() + ) +) +.branch( + isDebugEnabled, ( + $=>$.handleMessageStart( + () => (_timeout || _message) && ( + console.log('[fault-injection] delay, message:', _timeout, _message) + ) + ) + ) +) + +)() \ No newline at end of file diff --git a/pjs/samples/fault-injection/config.json b/pjs/samples/fault-injection/config.json new file mode 100644 index 0000000..880ce17 --- /dev/null +++ b/pjs/samples/fault-injection/config.json @@ -0,0 +1,249 @@ +{ + "Configs": { + "EnableDebug": true, + "ErrorPage": [ + { + "Error": [ + 404 + ], + "Page": "404.html", + "Directory": "pages/" + }, + { + "Error": [ + 502 + ], + "Page": "502.html", + "Directory": "pages/" + } + ], + "Gzip": { + "GzipMinLength": 1024, + "GzipTypes": [ + "text/css", + "text/xml", + "text/html", + "text/plain", + "application/xhtml+xml", + "application/javascript" + ] + } + }, + "Listeners": [ + { + "Protocol": "HTTP", + "Port": 8080 + }, + { + "Protocol": "HTTP", + "Port": 8081 + }, + { + "Protocol": "HTTP", + "Port": 8082 + }, + { + "Protocol": "HTTP", + "Port": 50052 + } + ], + "RouteRules": { + "8080": { + "*": { + "RouteType": "HTTP", + "AccessLog": "/var/log/fgw-access.log", + "Fault": { + "Delay": { + "Percent": 50, + "Fixed": 300, + "Unit": "ms" + }, + "Abort": { + "Percent": 50, + "Status": 503, + "Message": "fault-injection-message" + } + }, + "Matches": [ + { + "Path": { + "Type": "Prefix", + "Path": "/" + }, + "BackendService": { + "backendService1": { + "Weight": 100 + } + } + } + ] + } + }, + "8081": { + "*": { + "Matches": [ + { + "ServerRoot": "/var/www/html", + "Index": [ + "index.html", + "index.htm" + ], + "TryFiles": [ + "$uri", + "$uri/default/", + "=404" + ] + } + ] + } + }, + "8082": { + "*": { + "Matches": [ + { + "ServerRoot": "www2", + "Index": [ + "default.html", + "index.html" + ] + } + ] + } + }, + "50052": { + "*": { + "RouteType": "GRPC", + "Matches": [ + { + "Fault": { + "Delay": { + "Percent": 50, + "Fixed": 300 + }, + "Abort": { + "Percent": 50, + "Status": 14, + "Message": "fault-injection-message" + } + }, + "Method": { + "Type": "Exact", + "Service": "helloworld.Greeter", + "Method": "SayHello" + }, + "BackendService": { + "backendService2": { + "Weight": 100 + } + } + } + ] + } + } + }, + "Services": { + "backendService1": { + "StickyCookieName": "_srv_id", + "StickyCookieExpires": 3600, + "HealthCheck0": { + "Interval": 10, + "MaxFails": 3, + "FailTimeout": 30, + "Uri": "/", + "Matches": [ + { + "Type": "status", + "Value": [ + 200 + ] + } + ] + }, + "Endpoints": { + "127.0.0.1:8081": { + "Weight": 50 + }, + "127.0.0.1:8082": { + "Weight": 50 + } + } + }, + "backendService2": { + "Endpoints": { + "127.0.0.1:50051": { + "Weight": 50 + } + } + } + }, + "Chains": { + "HTTPRoute": [ + "common/access-control.js", + "common/ratelimit.js", + "common/consumer.js", + "http/codec.js", + "http/access-log.js", + "http/auth.js", + "http/route.js", + "http/fault-injection.js", + "filter/request-redirect.js", + "filter/header-modifier.js", + "filter/url-rewrite.js", + "http/service.js", + "http/metrics.js", + "http/tracing.js", + "http/logging.js", + "http/circuit-breaker.js", + "http/throttle-domain.js", + "http/throttle-route.js", + "http/error-page.js", + "http/proxy-redirect.js", + "http/forward.js", + "http/default.js" + ], + "HTTPSRoute": [ + "common/access-control.js", + "common/ratelimit.js", + "common/tls-termination.js", + "common/consumer.js", + "http/codec.js", + "http/access-log.js", + "http/auth.js", + "http/route.js", + "http/fault-injection.js", + "filter/request-redirect.js", + "filter/header-modifier.js", + "filter/url-rewrite.js", + "http/service.js", + "http/metrics.js", + "http/tracing.js", + "http/logging.js", + "http/circuit-breaker.js", + "http/throttle-domain.js", + "http/throttle-route.js", + "http/error-page.js", + "http/proxy-redirect.js", + "http/forward.js", + "http/default.js" + ], + "TLSPassthrough": [ + "common/access-control.js", + "common/ratelimit.js", + "tls/passthrough.js", + "common/consumer.js" + ], + "TLSTerminate": [ + "common/access-control.js", + "common/ratelimit.js", + "common/tls-termination.js", + "common/consumer.js", + "tls/forward.js" + ], + "TCPRoute": [ + "common/access-control.js", + "common/ratelimit.js", + "tcp/forward.js" + ] + }, + "Version": "0" +} \ No newline at end of file