@graphql-hive/plugin-opentelemetry
Version:
3,292 lines • 123 kB
JavaScript
import { isRetryExecutionRequest, getRetryInfo, Logger } from '@graphql-hive/gateway-runtime';
import { getHeadersObj } from '@graphql-mesh/utils';
import { isAsyncIterable, getOperationASTFromDocument, fakePromise } from '@graphql-tools/utils';
import { unfakePromise } from '@whatwg-node/promise-helpers';
import { hive } from './api.js';
import { SEMATTRS_HIVE_GRAPHQL_OPERATION_HASH, SEMATTRS_GRAPHQL_OPERATION_TYPE, SEMATTRS_GRAPHQL_OPERATION_NAME, SEMATTRS_HIVE_GATEWAY_OPERATION_SUBGRAPH_NAMES, SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT, SEMATTRS_HIVE_GRAPHQL_ERROR_CODES, SEMATTRS_GRAPHQL_DOCUMENT, SEMATTRS_IS_HIVE_SUBGRAPH_EXECUTION, SEMATTRS_HIVE_GATEWAY_UPSTREAM_SUBGRAPH_NAME, SEMATTRS_IS_HIVE_GRAPHQL_OPERATION, SEMATTRS_IS_HIVE_REQUEST, SEMATTRS_HIVE_REQUEST_ID } from './attributes.js';
import { trace, SpanStatusCode, context, ROOT_CONTEXT, SpanKind, DiagLogLevel, diag, propagation } from '@opentelemetry/api';
import { hashOperation } from '@graphql-hive/core';
import { defaultPrintFn } from '@graphql-mesh/transport-common';
import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_URL, SEMATTRS_NET_HOST_NAME, SEMATTRS_HTTP_HOST, SEMATTRS_HTTP_ROUTE, SEMATTRS_HTTP_SCHEME, SEMATTRS_HTTP_STATUS_CODE, SEMATTRS_EXCEPTION_STACKTRACE, SEMATTRS_EXCEPTION_MESSAGE, SEMATTRS_EXCEPTION_TYPE, SEMATTRS_HTTP_USER_AGENT, SEMATTRS_HTTP_CLIENT_IP } from '@opentelemetry/semantic-conventions';
import { printSchema, TypeInfo } from 'graphql';
import { ExportResultCode } from '@opentelemetry/core';
class OtelContextStack {
#root;
#current;
constructor(root) {
this.#root = { ctx: root };
this.#current = this.#root;
}
get current() {
return this.#current.ctx;
}
get root() {
return this.#root.ctx;
}
push = (ctx) => {
this.#current = { ctx, previous: this.#current };
};
pop = () => {
this.#current = this.#current.previous ?? this.#root;
};
toString() {
let node = this.#current;
const names = [];
while (node != void 0) {
names.push(trace.getSpan(node.ctx).name);
node = node.previous;
}
return names.join(" -> ");
}
}
function withState(pluginFactory) {
const states = {};
function getProp(scope, key) {
return {
get() {
if (!states[scope]) states[scope] = /* @__PURE__ */ new WeakMap();
let value = states[scope].get(key);
if (!value) states[scope].set(key, value = {});
return value;
},
enumerable: true
};
}
function getState(payload) {
let { executionRequest, context, request } = payload;
const state = {};
const defineState = (scope, key) => Object.defineProperty(state, scope, getProp(scope, key));
if (executionRequest) {
defineState("forSubgraphExecution", executionRequest);
if (executionRequest.context?.params) context = executionRequest.context;
}
if (context) {
defineState("forOperation", context);
if (context.request) request = context.request;
}
if (request) {
defineState("forRequest", request);
}
return state;
}
function addStateGetters(src) {
const result = {};
const properties = Object.entries(Object.getOwnPropertyDescriptors(src));
for (const [hookName, descriptor] of properties) {
const hook = descriptor.value;
if (typeof hook !== "function") {
descriptor.get &&= () => src[hookName];
descriptor.set &&= (value) => {
src[hookName] = value;
};
Object.defineProperty(result, hookName, descriptor);
} else {
result[hookName] = {
[hook.name](payload, ...args) {
return hook(
{
...payload,
get state() {
return getState(payload);
}
},
...args
);
}
}[hook.name];
}
}
return result;
}
const plugin = pluginFactory(getState);
const pluginWithState = addStateGetters(plugin);
pluginWithState.instrumentation = addStateGetters(plugin.instrumentation);
return pluginWithState;
}
function getMostSpecificState(state = {}) {
const { forOperation, forRequest, forSubgraphExecution } = state;
return forSubgraphExecution ?? forOperation ?? forRequest;
}
function createHttpSpan(input) {
const { url, request, tracer } = input;
const span = tracer.startSpan(
`${request.method || "GET"} ${url.pathname}`,
{
attributes: {
[SEMATTRS_HTTP_METHOD]: request.method || "GET",
[SEMATTRS_HTTP_URL]: request.url,
[SEMATTRS_HTTP_ROUTE]: url.pathname,
[SEMATTRS_HTTP_SCHEME]: url.protocol,
[SEMATTRS_NET_HOST_NAME]: url.hostname || url.host || request.headers.get("host") || "localhost",
[SEMATTRS_HTTP_HOST]: url.host || request.headers.get("host") || void 0,
[SEMATTRS_HTTP_CLIENT_IP]: request.headers.get("x-forwarded-for")?.split(",")[0],
[SEMATTRS_HTTP_USER_AGENT]: request.headers.get("user-agent") || void 0,
"hive.client.name": request.headers.get("graphql-client-name") || request.headers.get("x-graphql-client-name") || void 0,
"hive.client.version": request.headers.get("graphql-client-version") || request.headers.get("x-graphql-client-version") || void 0,
[SEMATTRS_IS_HIVE_REQUEST]: true
},
kind: SpanKind.SERVER
},
input.ctx
);
return {
ctx: trace.setSpan(input.ctx, span)
};
}
function setResponseAttributes(ctx, response) {
const span = trace.getSpan(ctx);
if (span) {
span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, response.status);
span.setAttribute(
"gateway.cache.response_cache",
response.status === 304 && response.headers.get("ETag") ? "hit" : "miss"
);
span.setStatus({
code: response.ok ? SpanStatusCode.OK : SpanStatusCode.ERROR,
message: response.ok ? void 0 : response.statusText
});
}
}
function createGraphQLSpan(input) {
const span = input.tracer.startSpan(
`graphql.operation`,
{
kind: SpanKind.INTERNAL,
attributes: { [SEMATTRS_IS_HIVE_GRAPHQL_OPERATION]: true }
},
input.ctx
);
return trace.setSpan(input.ctx, span);
}
function setParamsAttributes(input) {
const { ctx, params } = input;
const span = trace.getSpan(ctx);
if (!span) {
return;
}
span.setAttribute(SEMATTRS_GRAPHQL_DOCUMENT, params.query ?? "<undefined>");
if (params.operationName) {
span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, params.operationName);
}
}
const typeInfos = /* @__PURE__ */ new WeakMap();
const defaultOperationHashingFn = (input) => {
if (!typeInfos.has(input.schema)) {
typeInfos.set(input.schema, new TypeInfo(input.schema));
}
const typeInfo = typeInfos.get(input.schema);
return hashOperation({
documentNode: input.document,
operationName: input.operationName ?? null,
schema: input.schema,
variables: null,
// Unstable feature, not using it for now
typeInfo
});
};
function setDocumentAttributesOnOperationSpan(input) {
const { ctx, document } = input;
const span = trace.getSpan(ctx);
if (span) {
span.setAttribute(SEMATTRS_GRAPHQL_DOCUMENT, defaultPrintFn(document));
const operation = getOperationFromDocument(document, input.operationName);
if (operation) {
span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_TYPE, operation.operation);
const operationName = operation.name?.value;
if (operationName) {
span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, operationName);
span.updateName(`graphql.operation ${operationName}`);
}
}
}
}
function createGraphqlContextBuildingSpan(input) {
const span = input.tracer.startSpan(
"graphql.context",
{ kind: SpanKind.INTERNAL },
input.ctx
);
return trace.setSpan(input.ctx, span);
}
function createGraphQLParseSpan(input) {
const span = input.tracer.startSpan(
"graphql.parse",
{
kind: SpanKind.INTERNAL
},
input.ctx
);
return trace.setSpan(input.ctx, span);
}
function setGraphQLParseAttributes(input) {
const span = trace.getSpan(input.ctx);
if (!span) {
return;
}
if (input.query) {
span.setAttribute(SEMATTRS_GRAPHQL_DOCUMENT, input.query);
}
if (input.result instanceof Error) {
span.setAttribute(SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT, 1);
} else {
const document = input.result;
const operation = getOperationFromDocument(document, input.operationName);
if (operation) {
span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_TYPE, operation.operation);
const operationName = operation.name?.value;
if (operationName) {
span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, operationName);
}
}
}
}
function createGraphQLValidateSpan(input) {
const span = input.tracer.startSpan(
"graphql.validate",
{ kind: SpanKind.INTERNAL },
input.ctx
);
return trace.setSpan(input.ctx, span);
}
function setGraphQLValidateAttributes(input) {
const { result, ctx, document } = input;
const span = trace.getSpan(ctx);
if (!span) {
return;
}
const operation = getOperationFromDocument(document, input.operationName);
if (operation) {
const operationName = operation.name?.value;
span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_TYPE, operation.operation);
if (operationName) {
span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, operationName);
}
}
const errors = Array.isArray(result) ? result : [];
if (result instanceof Error) {
errors.push(result);
}
if (errors.length > 0) {
span.setAttribute(SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT, result.length);
span.setStatus({
code: SpanStatusCode.ERROR,
message: result.map((e) => e.message).join(", ")
});
const codes = [];
for (const error of result) {
if (error.extensions?.code) {
codes.push(`${error.extensions.code}`);
}
span.recordException(error);
}
span.setAttribute(SEMATTRS_HIVE_GRAPHQL_ERROR_CODES, codes);
}
}
function createGraphQLExecuteSpan(input) {
const span = input.tracer.startSpan(
"graphql.execute",
{ kind: SpanKind.INTERNAL },
input.ctx
);
return trace.setSpan(input.ctx, span);
}
function setGraphQLExecutionAttributes(input) {
const {
ctx,
args,
hashOperationFn = defaultOperationHashingFn,
operationCtx
} = input;
const operationSpan = trace.getSpan(operationCtx);
if (operationSpan) {
const hash = hashOperationFn?.({ ...args });
if (hash) {
operationSpan.setAttribute(SEMATTRS_HIVE_GRAPHQL_OPERATION_HASH, hash);
}
}
const span = trace.getSpan(ctx);
if (!span) {
return;
}
const operation = getOperationFromDocument(
args.document,
args.operationName
);
span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_TYPE, operation.operation);
const operationName = operation.name?.value;
if (operationName) {
span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, operationName);
}
}
function setGraphQLExecutionResultAttributes(input) {
const { ctx, result } = input;
const span = trace.getSpan(ctx);
if (!span) {
return;
}
if (input.subgraphNames?.length) {
span.setAttribute(
SEMATTRS_HIVE_GATEWAY_OPERATION_SUBGRAPH_NAMES,
input.subgraphNames
);
}
if (!isAsyncIterable(result) && // FIXME: Handle async iterable too
result.errors && result.errors.length > 0) {
span.setAttribute(SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT, result.errors.length);
span.setStatus({
code: SpanStatusCode.ERROR,
message: result.errors.map((e) => e.message).join(", ")
});
const codes = [];
for (const error of result.errors) {
span.recordException(error);
if (error.extensions?.["code"]) {
codes.push(`${error.extensions["code"]}`);
}
}
if (codes.length > 0) {
span.setAttribute(SEMATTRS_HIVE_GRAPHQL_ERROR_CODES, codes);
}
}
}
function createSubgraphExecuteSpan(input) {
const operation = getOperationASTFromDocument(
input.executionRequest.document,
input.executionRequest.operationName
);
const span = input.tracer.startSpan(
`subgraph.execute (${input.subgraphName})`,
{
attributes: {
[SEMATTRS_GRAPHQL_OPERATION_NAME]: operation.name?.value,
[SEMATTRS_GRAPHQL_DOCUMENT]: defaultPrintFn(
input.executionRequest.document
),
[SEMATTRS_GRAPHQL_OPERATION_TYPE]: operation.operation,
[SEMATTRS_HIVE_GATEWAY_UPSTREAM_SUBGRAPH_NAME]: input.subgraphName,
[SEMATTRS_IS_HIVE_SUBGRAPH_EXECUTION]: true
},
kind: SpanKind.CLIENT
},
input.ctx
);
return trace.setSpan(input.ctx, span);
}
function createUpstreamHttpFetchSpan(input) {
const span = input.tracer.startSpan(
"http.fetch",
{
attributes: {},
kind: SpanKind.CLIENT
},
input.ctx
);
return trace.setSpan(input.ctx, span);
}
function setUpstreamFetchAttributes(input) {
const { ctx, url, options: fetchOptions } = input;
const span = trace.getSpan(ctx);
if (!span) {
return;
}
const urlObj = new URL(input.url);
span.setAttribute(SEMATTRS_HTTP_METHOD, fetchOptions.method ?? "GET");
span.setAttribute(SEMATTRS_HTTP_URL, url);
span.setAttribute(SEMATTRS_NET_HOST_NAME, urlObj.hostname);
span.setAttribute(SEMATTRS_HTTP_HOST, urlObj.host);
span.setAttribute(SEMATTRS_HTTP_ROUTE, urlObj.pathname);
span.setAttribute(SEMATTRS_HTTP_SCHEME, urlObj.protocol);
if (input.executionRequest && isRetryExecutionRequest(input.executionRequest)) {
const { attempt } = getRetryInfo(input.executionRequest);
if (attempt > 0) {
span.setAttribute("http.request.resend_count", attempt);
}
}
}
function setUpstreamFetchResponseAttributes(input) {
const { ctx, response } = input;
const span = trace.getSpan(ctx);
if (!span) {
return;
}
span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, response.status);
span.setStatus({
code: response.ok ? SpanStatusCode.OK : SpanStatusCode.ERROR,
message: response.ok ? void 0 : response.statusText
});
}
function recordCacheEvent(event, payload) {
trace.getActiveSpan()?.addEvent("gateway.cache." + event, {
"gateway.cache.key": payload.key,
"gateway.cache.ttl": payload.ttl
});
}
function recordCacheError(action, error, payload) {
trace.getActiveSpan()?.addEvent("gateway.cache.error", {
"gateway.cache.key": payload.key,
"gateway.cache.ttl": payload.ttl,
"gateway.cache.action": action,
[SEMATTRS_EXCEPTION_TYPE]: "code" in error ? error.code : error.message,
[SEMATTRS_EXCEPTION_MESSAGE]: error.message,
[SEMATTRS_EXCEPTION_STACKTRACE]: error.stack
});
}
const responseCacheSymbol = Symbol.for("servedFromResponseCache");
function setExecutionResultAttributes(input) {
const span = trace.getSpan(input.ctx);
if (input.result && span) {
span.setAttribute(
"gateway.cache.response_cache",
input.result[responseCacheSymbol] ? "hit" : "miss"
);
}
}
function createSchemaLoadingSpan(inputs) {
const span = inputs.tracer.startSpan(
"gateway.schema",
{ attributes: { "gateway.schema.changed": false } },
inputs.ctx
);
const currentContext = context.active();
if (currentContext !== inputs.ctx) {
const currentSpan = trace.getActiveSpan();
currentSpan?.addLink({ context: span.spanContext() });
}
return trace.setSpan(ROOT_CONTEXT, span);
}
function setSchemaAttributes(inputs) {
const span = trace.getActiveSpan();
if (!span) {
return;
}
span.setAttribute("gateway.schema.changed", true);
span.setAttribute("graphql.schema", printSchema(inputs.schema));
}
function registerException(ctx, error) {
const span = ctx && trace.getSpan(ctx);
if (!span) {
return;
}
const message = error?.message?.toString() ?? error?.toString();
span.setStatus({ code: SpanStatusCode.ERROR, message });
span.recordException(error);
}
const operationByDocument = /* @__PURE__ */ new WeakMap();
const getOperationFromDocument = (document, operationName) => {
let operation = operationByDocument.get(document)?.get(operationName ?? null);
if (operation) {
return operation;
}
try {
operation = getOperationASTFromDocument(
document,
operationName || void 0
);
} catch {
}
let operationNameMap = operationByDocument.get(document);
if (!operationNameMap) {
operationByDocument.set(document, operationNameMap = /* @__PURE__ */ new Map());
}
operationNameMap.set(operationName ?? null, operation);
return operation;
};
function getEnvStr(key, opts = {}) {
const globalThat = opts.globalThis ?? globalThis;
let variable = globalThat.process?.env?.[key] || // @ts-expect-error can exist in wrangler and maybe other runtimes
globalThat.env?.[key] || // @ts-expect-error can exist in deno
globalThat.Deno?.env?.get(key) || // @ts-expect-error could be
globalThat[key];
if (variable != null) {
variable += "";
} else {
variable = void 0;
}
return variable?.trim();
}
function getEnvBool(key, opts = {}) {
return strToBool(getEnvStr(key, opts));
}
function strToBool(str) {
return ["1", "t", "true", "y", "yes", "on", "enabled"].includes(
(str || "").toLowerCase()
);
}
function isContextManagerCompatibleWithAsync() {
const symbol = Symbol();
const root = context.active();
return context.with(root.setValue(symbol, true), () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(context.active().getValue(symbol) || false);
});
});
});
}
const logLevelMap = {
ALL: [DiagLogLevel.ALL, "trace"],
VERBOSE: [DiagLogLevel.VERBOSE, "trace"],
DEBUG: [DiagLogLevel.DEBUG, "debug"],
INFO: [DiagLogLevel.INFO, "info"],
WARN: [DiagLogLevel.WARN, "warn"],
ERROR: [DiagLogLevel.ERROR, "error"],
NONE: [DiagLogLevel.NONE, null]
};
function diagLogLevelFromEnv() {
const value = getEnvStr("OTEL_LOG_LEVEL");
if (value == null) {
return void 0;
}
const resolvedLogLevel = logLevelMap[value.toUpperCase()];
if (resolvedLogLevel == null) {
diag.warn(
`Unknown log level "${value}", expected one of ${Object.keys(logLevelMap)}, using default`
);
return logLevelMap["INFO"];
}
return resolvedLogLevel;
}
const initializationTime = "performance" in globalThis ? performance.now() : void 0;
const ignoredRequests = /* @__PURE__ */ new WeakSet();
const otelCtxForRequestId = /* @__PURE__ */ new Map();
const HeadersTextMapGetter = {
keys(carrier) {
return [...carrier.keys()];
},
get(carrier, key) {
return carrier.get(key) || void 0;
}
};
function useOpenTelemetry(options) {
const inheritContext = options.inheritContext ?? true;
const propagateContext = options.propagateContext ?? true;
let useContextManager;
let tracer;
let traces;
let initSpan;
let pluginLogger = options.log && options.log.child("[OpenTelemetry] ");
pluginLogger?.info("Enabled");
function isParentEnabled(state) {
const parentState = getMostSpecificState(state);
return !parentState || !!parentState.otel;
}
function getContext(state) {
const specificState = getMostSpecificState(state)?.otel;
if (initSpan && !specificState) {
return initSpan;
}
if (useContextManager) {
return context.active();
}
return specificState?.current ?? ROOT_CONTEXT;
}
let preparation$ = init();
preparation$.then(() => {
preparation$ = fakePromise();
});
async function init() {
if (options.useContextManager !== false && !await isContextManagerCompatibleWithAsync()) {
useContextManager = false;
if (options.useContextManager === true) {
throw new Error(
"[OTEL] Context Manager usage is enabled, but the registered one is not compatible with async calls. Please use another context manager, such as `AsyncLocalStorageContextManager`."
);
}
} else {
useContextManager = options.useContextManager ?? true;
}
pluginLogger?.info("Initializing");
tracer = traces?.tracer ?? trace.getTracer("gateway");
traces = resolveTracesConfig(options, useContextManager, pluginLogger);
initSpan = trace.setSpan(
context.active(),
tracer.startSpan("gateway.initialization", {
startTime: initializationTime
})
);
}
const plugin = withState((getState) => {
hive.setPluginUtils({
get tracer() {
return tracer;
},
getActiveContext: (matcher) => getContext(getState(matcher)),
getHttpContext: (request) => getState({ request }).forRequest.otel?.root,
getOperationContext: (context2) => getState({ context: context2 }).forOperation.otel?.root,
getExecutionRequestContext: (executionRequest) => getState({ executionRequest }).forSubgraphExecution.otel?.root,
ignoreRequest: (request) => ignoredRequests.add(request)
});
return {
get tracer() {
return tracer;
},
instrumentation: {
request({ state: { forRequest }, request }, wrapped) {
return unfakePromise(
preparation$.then(() => {
if (!traces || !shouldTrace(traces.spans?.http, { request, ignoredRequests })) {
return wrapped();
}
const url = getURL(request);
const ctx = inheritContext ? propagation.extract(
context.active(),
request.headers,
HeadersTextMapGetter
) : context.active();
forRequest.otel = new OtelContextStack(
createHttpSpan({ ctx, request, tracer, url }).ctx
);
if (useContextManager) {
wrapped = context.bind(forRequest.otel.current, wrapped);
}
return wrapped();
}).catch((error) => {
registerException(forRequest.otel?.current, error);
throw error;
}).finally(() => {
const ctx = forRequest.otel?.root;
ctx && trace.getSpan(ctx)?.end();
})
);
},
operation({ context: gqlCtx, state: { forOperation, ...parentState } }, wrapped) {
if (!traces || !isParentEnabled(parentState) || !shouldTrace(traces.spans?.graphql, { context: gqlCtx })) {
return wrapped();
}
return unfakePromise(
preparation$.then(() => {
const ctx = getContext(parentState);
forOperation.otel = new OtelContextStack(
createGraphQLSpan({ tracer, ctx })
);
if (useContextManager) {
wrapped = context.bind(forOperation.otel.current, wrapped);
}
return fakePromise().then(wrapped).catch((err) => {
registerException(forOperation.otel?.current, err);
throw err;
}).finally(
() => trace.getSpan(forOperation.otel.current)?.end()
);
})
);
},
context({ state, context: gqlCtx }, wrapped) {
if (!traces || !isParentEnabled(state) || !shouldTrace(traces.spans?.graphqlContextBuilding, {
context: gqlCtx
})) {
return wrapped();
}
const { forOperation } = state;
const ctx = getContext(state);
forOperation.otel.push(
createGraphqlContextBuildingSpan({ ctx, tracer })
);
if (useContextManager) {
wrapped = context.bind(forOperation.otel.current, wrapped);
}
try {
wrapped();
} catch (err) {
registerException(forOperation.otel?.current, err);
throw err;
} finally {
trace.getSpan(forOperation.otel.current)?.end();
forOperation.otel.pop();
}
},
parse({ state, context: gqlCtx }, wrapped) {
if (!traces || !isParentEnabled(state) || !shouldTrace(traces.spans?.graphqlParse, { context: gqlCtx })) {
return wrapped();
}
const ctx = getContext(state);
const { forOperation } = state;
forOperation.otel.push(createGraphQLParseSpan({ ctx, tracer }));
if (useContextManager) {
wrapped = context.bind(forOperation.otel.current, wrapped);
}
try {
wrapped();
} catch (err) {
registerException(forOperation.otel.current, err);
throw err;
} finally {
trace.getSpan(forOperation.otel.current)?.end();
forOperation.otel.pop();
}
},
validate({ state, context: gqlCtx }, wrapped) {
if (!traces || !isParentEnabled(state) || !shouldTrace(traces.spans?.graphqlValidate, { context: gqlCtx })) {
return wrapped();
}
const { forOperation } = state;
forOperation.otel.push(
createGraphQLValidateSpan({ ctx: getContext(state), tracer })
);
if (useContextManager) {
wrapped = context.bind(forOperation.otel.current, wrapped);
}
try {
wrapped();
} catch (err) {
registerException(forOperation.otel?.current, err);
throw err;
} finally {
trace.getSpan(forOperation.otel.current)?.end();
forOperation.otel.pop();
}
},
execute({ state, context: gqlCtx }, wrapped) {
if (!traces || !isParentEnabled(state) || !shouldTrace(traces.spans?.graphqlExecute, { context: gqlCtx })) {
state.forOperation.skipExecuteSpan = true;
return wrapped();
}
const ctx = getContext(state);
const { forOperation } = state;
forOperation.otel?.push(createGraphQLExecuteSpan({ ctx, tracer }));
if (useContextManager) {
wrapped = context.bind(forOperation.otel.current, wrapped);
}
return unfakePromise(
fakePromise().then(wrapped).catch((err) => {
registerException(forOperation.otel.current, err);
throw err;
}).finally(() => {
trace.getSpan(forOperation.otel.current)?.end();
forOperation.otel.pop();
})
);
},
subgraphExecute({
state: { forSubgraphExecution, ...parentState },
executionRequest,
subgraphName
}, wrapped) {
const isIntrospection = !executionRequest.context.params;
if (!traces || !isParentEnabled(parentState) || parentState.forOperation?.skipExecuteSpan || !shouldTrace(
isIntrospection ? traces.spans?.schema : traces.spans?.subgraphExecute,
{
subgraphName,
executionRequest
}
)) {
return wrapped();
}
const parentContext = isIntrospection ? context.active() : getContext(parentState);
forSubgraphExecution.otel = new OtelContextStack(
createSubgraphExecuteSpan({
ctx: parentContext,
tracer,
executionRequest,
subgraphName
})
);
if (useContextManager) {
wrapped = context.bind(forSubgraphExecution.otel.current, wrapped);
}
return unfakePromise(
fakePromise().then(wrapped).catch((err) => {
registerException(forSubgraphExecution.otel.current, err);
throw err;
}).finally(() => {
trace.getSpan(forSubgraphExecution.otel.current)?.end();
forSubgraphExecution.otel.pop();
})
);
},
fetch({ state, executionRequest }, wrapped) {
if (isRetryExecutionRequest(executionRequest)) {
state = getState(getRetryInfo(executionRequest));
}
return unfakePromise(
preparation$.then(() => {
if (!traces || !isParentEnabled(state) || !shouldTrace(traces.spans?.upstreamFetch, { executionRequest })) {
return wrapped();
}
const { forSubgraphExecution } = state;
const ctx = createUpstreamHttpFetchSpan({
ctx: getContext(state),
tracer
});
forSubgraphExecution?.otel.push(ctx);
if (useContextManager) {
wrapped = context.bind(ctx, wrapped);
}
return fakePromise().then(wrapped).catch((err) => {
registerException(ctx, err);
throw err;
}).finally(() => {
trace.getSpan(ctx)?.end();
forSubgraphExecution?.otel.pop();
});
})
);
},
schema(_, wrapped) {
return unfakePromise(
preparation$.then(() => {
if (!traces || !shouldTrace(traces.spans?.schema, null)) {
return wrapped();
}
const ctx = createSchemaLoadingSpan({
ctx: initSpan ?? ROOT_CONTEXT,
tracer
});
return fakePromise().then(() => context.with(ctx, wrapped)).catch((err) => {
trace.getSpan(ctx)?.recordException(err);
}).finally(() => {
trace.getSpan(ctx)?.end();
});
})
);
}
},
onYogaInit({ yoga }) {
pluginLogger ??= new Logger({
writers: [
{
write(level, attrs, msg) {
level = level === "trace" ? "debug" : level;
yoga.logger[level](msg, attrs);
}
}
]
}).child("[OpenTelemetry] ");
pluginLogger.debug(
`Context manager is ${useContextManager ? "enabled" : "disabled"}`
);
},
onRequest({ state, serverContext }) {
if (!traces) return;
const requestId = (
// TODO: serverContext.log will not be available in Yoga, this will be fixed when Hive Logger is integrated into Yoga
serverContext.log?.attrs?.[
// @ts-expect-error even if the attrs is an array this will work
"requestId"
]
);
if (typeof requestId === "string") {
const httpCtx = state.forRequest.otel?.root;
const httpSpan = httpCtx && trace.getSpan(httpCtx);
httpSpan?.setAttribute(SEMATTRS_HIVE_REQUEST_ID, requestId);
}
if (!useContextManager && typeof requestId === "string") {
otelCtxForRequestId.set(requestId, getContext(state));
}
},
onEnveloped({ state, extendContext }) {
extendContext({
openTelemetry: {
tracer,
getHttpContext: (request) => {
const { forRequest } = request ? getState({ request }) : state;
return forRequest.otel?.root;
},
getOperationContext: (context2) => {
const { forOperation } = context2 ? getState({ context: context2 }) : state;
return forOperation.otel?.root;
},
getExecutionRequestContext: (executionRequest) => {
return getState({ executionRequest }).forSubgraphExecution.otel?.root;
},
getActiveContext: (contextMatcher) => getContext(contextMatcher ? getState(contextMatcher) : state)
}
});
},
onCacheGet: (payload) => traces && shouldTrace(traces.events?.cache, { key: payload.key, action: "read" }) ? {
onCacheMiss: () => recordCacheEvent("miss", payload),
onCacheHit: () => recordCacheEvent("hit", payload),
onCacheGetError: ({ error }) => recordCacheError("read", error, payload)
} : void 0,
onCacheSet: (payload) => traces && shouldTrace(traces.events?.cache, { key: payload.key, action: "write" }) ? {
onCacheSetDone: () => recordCacheEvent("write", payload),
onCacheSetError: ({ error }) => recordCacheError("write", error, payload)
} : void 0,
onResponse({ response, state, serverContext }) {
if (traces && state.forRequest.otel) {
setResponseAttributes(state.forRequest.otel.root, response);
}
if (!useContextManager) {
const requestId = (
// TODO: serverContext.log will not be available in Yoga, this will be fixed when Hive Logger is integrated into Yoga
serverContext.log?.attrs?.[
// @ts-expect-error even if the attrs is an array this will work
"requestId"
]
);
if (typeof requestId === "string") {
otelCtxForRequestId.delete(requestId);
}
}
},
onParams({ state, context: gqlCtx, params }) {
if (!traces || !isParentEnabled(state) || !shouldTrace(traces.spans?.graphql, { context: gqlCtx })) {
return;
}
const ctx = getContext(state);
setParamsAttributes({ ctx, params });
},
onExecutionResult({ result, context: gqlCtx, state }) {
if (!traces || !isParentEnabled(state) || !shouldTrace(traces.spans?.graphql, { context: gqlCtx })) {
return;
}
setExecutionResultAttributes({ ctx: getContext(state), result });
},
onParse({ state, context: gqlCtx }) {
if (!traces || !isParentEnabled(state) || !shouldTrace(traces.spans?.graphqlParse, { context: gqlCtx })) {
return;
}
return ({ result }) => {
setGraphQLParseAttributes({
ctx: getContext(state),
operationName: gqlCtx.params.operationName,
query: gqlCtx.params.query?.trim(),
result
});
if (!(result instanceof Error)) {
setDocumentAttributesOnOperationSpan({
ctx: state.forOperation.otel.root,
document: result,
operationName: gqlCtx.params.operationName
});
}
};
},
onValidate({ state, context: gqlCtx, params }) {
if (!traces || !isParentEnabled(state) || !shouldTrace(traces.spans?.graphqlValidate, { context: gqlCtx })) {
return;
}
return ({ result }) => {
setGraphQLValidateAttributes({
ctx: getContext(state),
result,
document: params.documentAST,
operationName: gqlCtx.params.operationName
});
};
},
onExecute({ state, args }) {
if (state.forOperation.skipExecuteSpan) {
return;
}
const ctx = getContext(state);
setGraphQLExecutionAttributes({
ctx,
operationCtx: state.forOperation.otel.root,
args,
hashOperationFn: options.hashOperation
});
state.forOperation.subgraphNames = [];
return {
onExecuteDone({ result }) {
setGraphQLExecutionResultAttributes({
ctx,
result,
subgraphNames: state.forOperation.subgraphNames
});
}
};
},
onSubgraphExecute({ subgraphName, state }) {
state.forOperation?.subgraphNames?.push(subgraphName);
},
onFetch(payload) {
const { url, setFetchFn, fetchFn, executionRequest } = payload;
let { state } = payload;
if (executionRequest && isRetryExecutionRequest(executionRequest)) {
state = getState(getRetryInfo(executionRequest));
}
if (propagateContext) {
setFetchFn((url2, options2, ...args) => {
const reqHeaders = getHeadersObj(options2?.headers || {});
propagation.inject(getContext(state), reqHeaders);
return fetchFn(url2, { ...options2, headers: reqHeaders }, ...args);
});
}
if (!traces || !isParentEnabled(state) || !shouldTrace(traces.spans?.upstreamFetch, { executionRequest })) {
return;
}
const ctx = getContext(state);
setUpstreamFetchAttributes({
ctx,
url,
options: payload.options,
executionRequest
});
return ({ response }) => {
setUpstreamFetchResponseAttributes({ ctx, response });
};
},
onSchemaChange(payload) {
if (initSpan) {
trace.getSpan(initSpan)?.end();
initSpan = null;
}
if (!traces || !shouldTrace(traces?.spans?.schema, null)) {
setSchemaAttributes(payload);
}
},
onDispose() {
if (options.flushOnDispose !== false) {
const flushMethod = options.flushOnDispose ?? "forceFlush";
const provider = trace.getTracerProvider();
if (flushMethod in provider && typeof provider[flushMethod] === "function") {
return provider[flushMethod]();
}
}
}
};
});
plugin.getActiveContext = hive.getActiveContext;
plugin.getHttpContext = hive.getHttpContext;
plugin.getOperationContext = hive.getOperationContext;
plugin.getExecutionRequestContext = hive.getExecutionRequestContext;
plugin.ignoreRequest = hive.ignoreRequest;
Object.defineProperty(plugin, "tracer", {
enumerable: true,
get: () => tracer
});
return plugin;
}
function shouldTrace(value, args) {
if (value == null) {
return true;
}
if (typeof value === "function") {
return value(args);
}
return value;
}
function getURL(request) {
if ("parsedUrl" in request) {
return request.parsedUrl;
}
return new URL(request.url, "http://localhost");
}
const defaultHttpFilter = ({ request }) => {
if (ignoredRequests.has(request)) {
return false;
}
return true;
};
function resolveTracesConfig(options, useContextManager, log) {
if (options.traces === false) {
return void 0;
}
let traces = typeof options.traces === "object" ? options.traces : {};
traces.spans ??= {};
if ((traces.spans.http ?? true) === true) {
traces.spans = { ...traces.spans, http: defaultHttpFilter };
}
if (!useContextManager) {
if (traces.spans.schema) {
log?.warn(
"Schema loading spans are disabled because no context manager is available"
);
}
traces.spans.schema = false;
}
return traces;
}
function getDefaultExportFromCjs (x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
}
var opossum = {exports: {}};
(function (module, exports) {
(function webpackUniversalModuleDefinition(root, factory) {
module.exports = factory();
})(module.exports, () => {
return /******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./index.js":
/*!******************!*\
!*** ./index.js ***!
\******************/
/***/ ((module, exports, __webpack_require__) => {
module.exports = __webpack_require__(/*! ./lib/circuit */ "./lib/circuit.js");
/***/ }),
/***/ "./lib/cache.js":
/*!**********************!*\
!*** ./lib/cache.js ***!
\**********************/
/***/ ((module, exports) => {
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), Object.defineProperty(e, "prototype", { writable: false }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return (String )(t); }
/**
* Simple in-memory cache implementation
* @class MemoryCache
* @property {Map} cache Cache map
*/
var MemoryCache = /*#__PURE__*/function () {
function MemoryCache(maxEntries) {
_classCallCheck(this, MemoryCache);
this.cache = new Map();
this.maxEntries = maxEntries !== null && maxEntries !== void 0 ? maxEntries : Math.pow(2, 24) - 1; // Max size for Map is 2^24.
}
/**
* Get cache value by key
* @param {string} key Cache key
* @return {any} Response from cache
*/
return _createClass(MemoryCache, [{
key: "get",
value: function get(key) {
var cached = this.cache.get(key);
if (cached) {
if (cached.expiresAt > Date.now() || cached.expiresAt === 0) {
return cached.value;
}
this.cache["delete"](key);
}
return undefined;
}
/**
* Set cache key with value and ttl
* @param {string} key Cache key
* @param {any} value Value to cache
* @param {number} ttl Time to live in milliseconds
* @return {void}
*/
}, {
key: "set",
value: function set(key, value, ttl) {
// Evict first entry when at capacity - only when it's a new key.
if (this.cache.size === this.maxEntries && this.get(key) === undefined) {
this.cache["delete"](this.cache.keys().next().value);
}
this.cache.set(key, {
expiresAt: ttl,
value: value
});
}
/**
* Delete cache key
* @param {string} key Cache key
* @return {void}
*/
}, {
key: "delete",
value: function _delete(key) {
this.cache["delete"](key);
}
/**
* Clear cache
* @returns {void}
*/
}, {
key: "flush",
value: function flush() {
this.cache.clear();
}
}]);
}();
module.exports = MemoryCache;
/***/ }),
/***/ "./lib/circuit.js":
/*!************************!*\
!*** ./lib/circuit.js ***!
\************************/
/***/ ((module, exports, __webpack_require__) => {
function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); }
function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: false }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return (String )(t); }
function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
function _possibleConstructorReturn(t, e) { if (e && ("object" == _typeof(e) || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); }
function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); }
function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: true, configurable: true } }), Object.defineProperty(t, "prototype", { writable: false }), e && _setPrototypeOf(t, e); }
function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); }
var EventEmitter = __webpack_require__(/*! events */ "./node_modules/events/events.js");
var Status = __webpack_require__(/*! ./status */ "./lib/status.js");
var Semaphore = __webpack_require__(/*! ./semaphore */ "./lib/semaphore.js");
var MemoryCache = __webpack_require__(/*! ./cache */ "./lib/cache.js");
var STATE = Symbol('state');
var OPEN = Symbol('open');
var CLOSED = Symbol('closed');
var HALF_OPEN = Symbol('half-open');
var PENDING_CLOSE = Symbol('pending-close');
var SHUTDOWN = Symbol('shutdown');
var FALLBACK_FUNCTION = Symbol('fallback');
var STATUS = Symbol('status');
var NAME = Symbol('name');
var GROUP = Symbol('group');
var ENABLED = Symbol('Enabled');
var WARMING_UP = Symbol('warming-up');
var VOLUME_THRESHOLD = Symbol('volume-threshold');
var OUR_ERROR = Symbol('our-error');
var RESET_TIMEOUT = Symbol('reset-timeout');
var WARMUP_TIMEOUT = Symbol('warmup-timeout');
var LAST_TIMER_AT = Symbol('last-timer-at');
var deprecation = "options.maxFailures is deprecated. Please use options.errorThresholdPercentage";
/**
* Constructs a {@link CircuitBreaker}.
*
* @class CircuitBreaker
* @extends EventEmitter
* @param {Function} action The action to fire for this {@link CircuitBreaker}
* @param {Object} options Options for the {@link CircuitBreaker}
* @param {Status} options.status A {@link Status} object that might
* have pre-prime stats
* @param {Number} options.timeout The time in milliseconds that action should
* be allowed to execute before timing out. Timeout can be disabled by setting
* this to `false`. Default 10000 (10 seconds)
* @param {Number} options.maxFailures (Deprecated) The number of times the
* circuit can fail before opening. Default 10.
* @param {Number} options.resetTimeout The time in milliseconds to wait before
* setting the breaker to `halfOpen` state, and trying the action again.
* Default: 30000 (30 seconds)
* @param {Number} options.rollingCountTimeout Sets the duration of the
* statistical rolling window, in milliseconds. This is how long Opossum keeps
* metrics for the circuit breaker to use and for publishing. Default: 10000
* @param {Number} options.rollingCountBuckets Sets the number of buckets the
* rolling statistical window is divided into. So, if
* options.rollingCountTimeout is 10000, and options.rollingCountBuckets is 10,
* then the statistical window will be 1000/1 second snapshots in the
* statistical window. Default: 10
* @param {String} options.name the circuit name to use when reporting stats.
* Default: the name of the function this circuit controls.
* @param {boolean} options.rollingPercentilesEnabled This property indicates
* whether execution latencies should be tracked and calculated as percentiles.
* If they are disabled, all summary statistics (mean, percentiles) are
* returned as -1. Default: true
* @param {Number} options.capacity the number of concurrent requests allowed.
* If the number currently executing function calls is equal to
* options.capacity, further calls to `fire()` are rejected until at least one
* of the current requests completes. Default: `Number.MAX_SAFE_INTEGER`.
* @param {Number} options.errorThresholdPercentage the error percentage at
* which to open the circuit and start short-circuiting requests to fallback.
* Default: 50
* @param {boolean} options.enabled whether this circuit is enabled upon
* construction. Default: true
* @param {boolean} options.allowWarmUp determines whether to allow failures
* without opening the circuit during a brief warmup period (this is the
* `rollingCountTimeout` property). Default: false
* This can help in situations where no matter what your
* `errorThresholdPercentage` is, if the first execution times out or fails,
* the circuit immediately opens.
* @param {Number} options.volumeThreshold the minimum number of requests within
* the rolling statistical window that must exist before the circuit breaker
* can open. This is similar to `options.allowWarmUp` in that no matter how many
* failures there are, if the number of requests within the statistical window
* does not exceed this threshold, the circuit will remain closed. Default: 0
* @param {Function} options.errorFilter an optional function that will be
* called when the circuit's function fails (returns a rejected Promise). If
* this function returns truthy, the circuit's failPure statistics will not be
* incremented. This is useful, for example, when you don't want HTTP 404 to
* trip the circuit, but still want to handle it as a failure case.
* @param {boolean} options.cache whether the return value of the first
* successful execution of the circuit's function will be cached. Once a value
* has been cached that value will be returned for every subsequent execution:
* the cache can be cleared using `clearCache`. (The metrics `cacheHit` and
* `cacheMiss` reflect cache activity.) Default: false
* @param {Number} options.cacheTTL the time to live for the cache
* in milliseconds. Set 0 for infinity cache. Default: 0 (no TTL)
* @param {Number} options.cacheSize the max amount of entries in the internal
* cache. Only used when cacheTransport is not defined.
* Default: max size of JS map (2^24).
* @param {Function} options.cacheGetKey function that returns the key to use
* when caching the result of the circuit's fire.
* Better to use custom one, because `JSON.stringify` is not good
* from performance perspective.
* Default: `(...args) => JSON.stringify(args)`
* @param {CacheTransport} options.cacheTransport custom cache transport
* should implement `get`, `set` and `flush` methods.
* @param {boolean} options.coalesce If true, this provides coalescing of
* requests to this breaker, in other words: the promise will be cached.
* Only one action (with same cache key) is executed at a time, and the other
* pending actions wait for the result. Performance will improve when rapidly
* firing the circuitbreaker with the same request, especially on a slower
* action (e.g. multiple end-users fetching same data from remote).
* Will use internal cache only. Can be used in combination with options.cache.
* The metrics `coalesceCacheHit` and `coalesceCacheMiss` are available.
* Default: false
* @param {Number} options.coalesceTTL the time to live for the coalescing
* in milliseconds. Set 0 for infinity cache. Default: same as options.timeout
* @param {Number} options.coalesceSize the max amount of entries in the
* coalescing cache. Default: max size of JS map (2^24).
* @param {string[]} options.coalesceResetOn when to reset the coalesce cache.
* Options: `error`, `success`, `timeout`. Default: not set, reset using TTL.
* @param {AbortController} options.abortController this allows Opossum to
* signal upon timeout and properly abort your on going requests instead of
* leaving it in the background
* @param {boolean} options.enableSnapshots whether to enable the rolling
* stats snapshots that opossum emits at the bucketInterval. Disable this
* as an optimization if you don't listen to the 'snapshot' event to reduce
* the number of timers opossum initiates.
* @param {EventEmitter} options.rotateBucketController if you have multiple
* breakers in your app, the number of timers across breakers can get costly.
* This option allows you to provide an EventEmitter that rotates the buckets
* so you can have one global timer in your app. Make sure that you are
* emitting a 'rotate' event from this EventEmitter
* @param {boolean} options.autoRenewAbortController Automatically recreates
* the instance of AbortController whenever the circuit transitions to
* 'halfOpen' or 'closed' state. This ensures that new requests are not
* impacted by previous signals that were triggered when the circuit was 'open'.
* Default: false
*
*
* @fires CircuitBreaker#halfOpen
* @fires CircuitBreaker#close
* @fires CircuitBreaker#open
* @fires CircuitBreaker#fire
* @fires CircuitBreaker#cacheHit
* @fires CircuitBreaker#cacheMiss
* @fires CircuitBreaker#coalesceCacheHit
* @fires CircuitBreaker#coalesceCacheMiss
* @fires CircuitBreaker#reject
* @fires CircuitBreaker#timeout
* @fires CircuitBreaker#success
* @fires CircuitBreaker#semaphoreLocked
* @fires CircuitBreaker#healthCheckFailed
* @fires CircuitBreaker#fallback
* @fires CircuitBreaker#failure
*/
var CircuitBreaker = /*#__PURE__*/function (_EventEmitter) {
function CircuitBreaker(action) {
var _options$timeout, _options$resetTimeout, _options$errorThresho, _options$rollingCount, _options$rollingCount2, _options$cacheTTL, _options$cacheGetKey, _options$coalesceTTL, _options$coalesceRese;
var _this;
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
_classCallCheck(this, CircuitBreaker);
_this = _callSuper(this, CircuitBreaker);
_this.options = options;
_this.options.timeout = (_options$timeout = options.timeout) !== null && _options$timeout !== void 0 ? _options$timeout : 10000;
_this.options.resetTimeout = (_options$resetTimeout = options.resetTimeout) !== null && _options$resetTimeout !== void 0 ? _options$resetTimeout : 30000;
_this.options.errorThresholdPercentage = (_options$errorThresho = options.errorThresholdPercentage) !== null && _options$errorThresho !== void 0 ? _options$errorThresho : 50;
_this.options.rollingCountTimeout = (_options$rollingCount = options.rollingCountTimeout) !== null && _options$rollingCount !== void 0 ? _options$rollingCount : 10000;
_this.options.rollingCountBuckets = (_options$rollingCount2 = options.rollingCountBuckets) !== null && _options$rollingCount2 !== void 0 ? _options$rollingCount2 : 10;
_this.options.rollingPercentilesEnabled = options.rollingPercentilesEnabled !== false;
_this.options.capacity = Number.isInteger(options.capacity) ? options.capacity : Number.MAX_SAFE_INTEGER;
_this.options.errorFilter = options.errorFilter || function (_) {
return false;
};
_this.options.cacheTTL = (_options$cacheTTL = options.cacheTTL) !== null && _options$cacheTTL !== void 0 ? _options$cacheTTL : 0;
_this.options.cacheGetKey = (_options$cacheGetKey = options.cacheGetKey) !== null && _options$cacheGetKey !== void 0 ? _options$cacheGetKey : function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return JSON.stringify(args);
};
_this.options.enableSnapshots = options.enableSnapshots !== false;
_this.options.rotateBucketController = options.rotateBucketController;
_this.options.coalesce = !!options.coalesce;
_this.options.coalesceTTL = (_options$coalesceTTL = options.coalesceTTL) !== null && _options$coalesceTTL !== void 0 ? _options$coalesceTTL : _this.options.timeout;
_this.options.coalesceResetOn = ((_options$coalesceRese = options.coalesceResetOn) === null || _options$coalesceRese === void 0 ? void 0 : _options$coalesceRese.filter(function (o) {
return ['error', 'success', 'timeout'].includes(o);
})) || [];
// Set default cache transport if not provided
if (_this.options.cache) {
if (_this.options.cacheTransport === undefined) {
_this.options.cacheTransport = new MemoryCache(options.cacheSize);
} else if (_typeof(_this.options.cacheTransport) !== 'object' || !_this.options.cacheTransport.get || !_this.options.cacheTransport.set || !_this.options.cacheTransport.flush) {
throw new TypeError('options.cacheTransport should be an object with `get`, `set` and `flush` methods');
}
}
if (_this.options.coalesce) {
_this.options.coalesceCache = new MemoryCache(options.coalesceSize);
}
_this.semaphore = new Semaphore(_this.options.capacity);
// check if action is defined
if (!action) {
throw new TypeError('No action provided. Cannot construct a CircuitBreaker without an invocable action.');
}
if (options.autoRenewAbortController && !options.abortController) {
options.abortController = new AbortController();
}
if (options.abortController && typeof options.abortController.abort !== 'function') {
throw new TypeError('AbortController does not contain `abort()` method');
}
_this[VOLUME_THRESHOLD] = Number.isInteger(options.volumeThreshold) ? options.volumeThreshold : 0;
_this[WARMING_UP] = options.allowWarmUp === true;
// The user can pass in a Status object to initialize the Status/stats
if (_this.options.status) {
// Do a check that this is a Status Object,
if (_this.options.status instanceof Status) {
_this[STATUS] = _this.options.status;
} else {
_this[STATUS] = new Status({
stats: _this.options.status
});
}
} else {
_this[STATUS] = new Status(_this.options);
}
_this[STATE] = CLOSED;
if (options.state) {
_this[ENABLED] = options.state.enabled !== false;
_this[WARMING_UP] = options.state.warmUp || _this[WARMING_UP];
// Closed if nothing is passed in
_this[CLOSED] = options.state.closed !== false;
// These should be in sync
_this[HALF_OPEN] = _this[PENDING_CLOSE] = options.state.halfOpen || false;
// Open should be the opposite of closed,
// but also the opposite of half_open
_this[OPEN] = !_this[CLOSED] && !_this[HALF_OPEN];
_this[SHUTDOWN] = options.state.shutdown || false;
} else {
_this[PENDING_CLOSE] = false;
_this[ENABLED] = options.enabled !== false;
}
_this[FALLBACK_FUNCTION] = null;
_this[NAME] = options.name || action.name || nextName();
_this[GROUP] = options.group || _this[NAME];
if (_this[WARMING_UP]) {
var timer = _this[WARMUP_TIMEOUT] = setTimeout(function (_) {
return _this[WARMING_UP] = false;
}, _this.options.rollingCountTimeout);
if (typeof timer.unref === 'function') {
timer.unref();
}
}
if (typeof action !== 'function') {
_this.action = function (_) {
return Promise.resolve(action);
};
} else _this.action = action;
if (options.maxFailures) console.error(deprecation);
var increment = function increment(property) {
return function (result, runTime) {
return _this[STATUS].increment(property, runTime);
};
};
_this.on('success', increment('successes'));
_this.on('failure', increment('failures'));
_this.on('fallback', increment('fallbacks'));
_this.on('timeout', increment('timeouts'));
_this.on('fire', increment('fires'));
_this.on('reject', increment('rejects'));
_this.on('cacheHit', increment('cacheHits'));
_this.on('cacheMiss', increment('cacheMisses'));
_this.on('coalesceCacheHit', increment('coalesceCacheHits'));
_this.on('coalesceCacheMiss', increment('coalesceCacheMisses'));
_this.on('open', function (_) {
return _this[STATUS].open();
});
_this.on('close', function (_) {
return _this[STATUS].close();
});
_this.on('semaphoreLocked', increment('semaphoreRejections'));
/**
* @param {CircuitBreaker} circuit This current circuit
* @returns {function(): void} A bound reset callback
* @private
*/
function _startTimer(circuit) {
circuit[LAST_TIMER_AT] = Date.now();
return function (_) {
var timer = circuit[RESET_TIMEOUT] = setTimeout(function () {
_halfOpen(circuit);
}, circuit.options.resetTimeout);
if (typeof timer.unref === 'function') {
timer.unref();
}
};
}
/**
* Sets the circuit breaker to half open
* @private
* @param {CircuitBreaker} circuit The current circuit breaker
* @returns {void}
*/
function _halfOpen(circuit) {
circuit[STATE] = HALF_OPEN;
circuit[PENDING_CLOSE] = true;
circuit._renewAbortControllerIfNeeded();
/**
* Emitted after `options.resetTimeout` has elapsed, allowing for
* a single attempt to call the service again. If that attempt is
* successful, the circuit will be closed. Otherwise it remains open.
*
* @event CircuitBreaker#halfOpen
* @type {Number} how long the circuit remained open
*/
circuit.emit('halfOpen', circuit.options.resetTimeout);
}
_this.on('open', _startTimer(_this));
_this.on('success', function (_) {
if (_this.halfOpen) {
_this.close();
}
});
// Prepopulate the State of the Breaker
if (_this[SHUTDOWN]) {
_this[STATE] = SHUTDOWN;
_this.shutdown();
} else if (_this[CLOSED]) {
_this.close();
} else if (_this[OPEN]) {
// If the state being passed in is OPEN but more time has elapsed
// than the resetTimeout, then we should be in halfOpen state
if (_this.options.state.lastTimerAt !== undefined && Date.now() - _this.options.state.lastTimerAt > _this.options.resetTimeout) {
_halfOpen(_this);
} else {
_this.open();
}
} else if (_this[HALF_OPEN]) {
// Not sure if anything needs to be done here
_this[STATE] = HALF_OPEN;
}
return _this;
}
/**
* Renews the abort controller if needed
* @private
* @returns {void}
*/
_inherits(CircuitBreaker, _EventEmitter);
return _createClass(CircuitBreaker, [{
key: "_renewAbortControllerIfNeeded",
value: function _renewAbortControllerIfNeeded() {
if (this.options.autoRenewAbortController && this.options.abortController && this.options.abortController.signal.aborted) {
this.options.abortController = new AbortController();
}
}
/**
* Closes the breaker, allowing the action to execute again
* @fires CircuitBreaker#close
* @returns {void}
*/
}, {
key: "close",
value: function close() {
if (this[STATE] !== CLOSED) {
if (this[RESET_TIMEOUT]) {
clearTimeout(this[RESET_TIMEOUT]);
}
this[STATE] = CLOSED;
this[PENDING_CLOSE] = false;
this._renewAbortControllerIfNeeded();
/**
* Emitted when the breaker is reset allowing the action to execute again
* @event CircuitBreaker#close
*/
this.emit('close');
}
}
/**
* Opens the breaker. Each time the breaker is fired while the circuit is
* opened, a failed Promise is returned, or if any fallback function
* has been provided, it is invoked.
*
* If the breaker is already open this call does nothing.
* @fires CircuitBreaker#open
* @returns {void}
*/
}, {
key: "open",
value: function open() {
if (this[STATE] !== OPEN) {
this[STATE] = OPEN;
this[PENDING_CLOSE] = false;
/**
* Emitted when the breaker opens because the action has
* failure percentage greater than `options.errorThresholdPercentage`.
* @event CircuitBreaker#open
*/
this.emit('open');
}
}
/**
* Shuts down this circuit breaker. All subsequent calls to the
* circuit will fail, returning a rejected promise.
* @returns {void}
*/
}, {
key: "shutdown",
value: function shutdown() {
/**
* Emitted when the circuit breaker has been shut down.
* @event CircuitBreaker#shutdown
*/
this.emit('shutdown');
this.disable();
this.removeAllListeners();
if (this[RESET_TIMEOUT]) {
clearTimeout(this[RESET_TIMEOUT]);
}
if (this[WARMUP_TIMEOUT]) {
clearTimeout(this[WARMUP_TIMEOUT]);
}
this.status.shutdown();
this[STATE] = SHUTDOWN;
// clear cache on shutdown
this.clearCache();
}
/**
* Determines if the circuit has been shutdown.
* @type {Boolean}
*/
}, {
key: "isShutdown",
get: function get() {
return this[STATE] === SHUTDOWN;
}
/**
* Gets the name of this circuit
* @type {String}
*/
}, {
key: "name",
get: function get() {
return this[NAME];
}
/**
* Gets the name of this circuit group
* @type {String}
*/
}, {
key: "group",
get: function get() {
return this[GROUP];
}
/**
* Gets whether this circuit is in the `pendingClosed` state
* @type {Boolean}
*/
}, {
key: "pendingClose",
get: function get() {
return this[PENDING_CLOSE];
}
/**
* True if the circuit is currently closed. False otherwise.
* @type {Boolean}
*/
}, {
key: "closed",
get: function get() {
return this[STATE] === CLOSED;
}
/**
* True if the circuit is currently opened. False otherwise.
* @type {Boolean}
*/
}, {
key: "opened",
get: function get() {
return this[STATE] === OPEN;
}
/**
* True if the circuit is currently half opened. False otherwise.
* @type {Boolean}
*/
}, {
key: "halfOpen",
get: function get() {
return this[STATE] === HALF_OPEN;
}
/**
* The current {@link Status} of this {@link CircuitBreaker}
* @type {Status}
*/
}, {
key: "status",
get: function get() {
return this[STATUS];
}
/**
* Get the current stats for the circuit.
* @see Status#stats
* @type {Object}
*/
}, {
key: "stats",
get: function get() {
return this[STATUS].stats;
}
}, {
key: "toJSON",
value: function toJSON() {
return {
state: {
name: this.name,
enabled: this.enabled,
closed: this.closed,
open: this.opened,
halfOpen: this.halfOpen,
warmUp: this.warmUp,
shutdown: this.isShutdown,
lastTimerAt: this[LAST_TIMER_AT]
},
status: this.status.stats
};
}
/**
* Gets whether the circuit is enabled or not
* @type {Boolean}
*/
}, {
key: "enabled",
get: function get() {
return this[ENABLED];
}
/**
* Gets whether the circuit is currently in warm up phase
* @type {Boolean}
*/
}, {
key: "warmUp",
get: function get() {
return this[WARMING_UP];
}
/**
* Gets the volume threshold for this circuit
* @type {Boolean}
*/
}, {
key: "volumeThreshold",
get: function get() {
return this[VOLUME_THRESHOLD];
}
/**
* Provide a fallback function for this {@link CircuitBreaker}. This
* function will be executed when the circuit is `fire`d and fails.
* It will always be preceded by a `failure` event, and `breaker.fire` returns
* a rejected Promise.
* @param {Function | CircuitBreaker} func the fallback function to execute
* when the breaker has opened or when a timeout or error occurs.
* @return {CircuitBreaker} this
*/
}, {
key: "fallback",
value: function fallback(func) {
var fb = func;
if (func instanceof CircuitBreaker) {
fb = function fb() {
return func.fire.apply(func, arguments);
};
}
this[FALLBACK_FUNCTION] = fb;
return this;
}
/**
* Execute the action for this circuit. If the action fails or times out, the
* returned promise will be rejected. If the action succeeds, the promise will
* resolve with the resolved value from action. If a fallback function was
* provided, it will be invoked in the event of any failure or timeout.
*
* Any parameters passed to this function will be proxied to the circuit
* function.
*
* @return {Promise<any>} promise resolves with the circuit function's return
* value on success or is rejected on failure of the action. Use isOurError()
* to determine if a rejection was a result of the circuit breaker or the
* action.
*
* @fires CircuitBreaker#failure
* @fires CircuitBreaker#fallback
* @fires CircuitBreaker#fire
* @fires CircuitBreaker#reject
* @fires CircuitBreaker#success
* @fires CircuitBreaker#timeout
* @fires CircuitBreaker#semaphoreLocked
*/
}, {
key: "fire",
value: function fire() {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return this.call.apply(this, [this.action].concat(args));
}
/**
* Execute the action for this circuit using `context` as `this`.
* If the action fails or times out, the
* returned promise will be rejected. If the action succeeds, the promise will
* resolve with the resolved value from action. If a fallback function was
* provided, it will be invoked in the event of any failure or timeout.
*
* Any parameters in addition to `context will be passed to the
* circuit function.
*
* @param {any} context the `this` context used for function execution
* @param {any} rest the arguments passed to the action
*
* @return {Promise<any>} promise resolves with the circuit function's return
* value on success or is rejected on failure of the action.
*
* @fires CircuitBreaker#failure
* @fires CircuitBreaker#fallback
* @fires CircuitBreaker#fire
* @fires CircuitBreaker#reject
* @fires CircuitBreaker#success
* @fires CircuitBreaker#timeout
* @fires CircuitBreaker#semaphoreLocked
*/
}, {
key: "call",
value: function call(context) {
var _this2 = this;
if (this.isShutdown) {
var err = buildError('The circuit has been shutdown.', 'ESHUTDOWN');
return Promise.reject(err);
}
for (var _len3 = arguments.length, rest = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
rest[_key3 - 1] = arguments[_key3];
}
var args = rest.slice();
/**
* Emitted when the circuit breaker action is executed
* @event CircuitBreaker#fire
* @type {any} the arguments passed to the fired function
*/
this.emit('fire', args);
// Protection, caches and coalesce disabled.
if (!this[ENABLED]) {
var result = this.action.apply(context, args);
return typeof result.then === 'function' ? result : Promise.resolve(result);
}
// Generate cachekey only when cache and/or coalesce is enabled.
var cacheKey = this.options.cache || this.options.coalesce ? this.options.cacheGetKey.apply(this, rest) : '';
// If cache is enabled, check if we have a cached value
if (this.options.cache) {
var cached = this.options.cacheTransport.get(cacheKey);
if (cached) {
/**
* Emitted when the circuit breaker is using the cache
* and finds a value.
* @event CircuitBreaker#cacheHit
*/
this.emit('cacheHit');
return cached;
}
/**
* Emitted when the circuit breaker does not find a value in
* the cache, but the cache option is enabled.
* @event CircuitBreaker#cacheMiss
*/
this.emit('cacheMiss');
}
/* When coalesce is enabled, check coalesce cache and return
promise, if any. */
if (this.options.coalesce) {
var cachedCall = this.options.coalesceCache.get(cacheKey);
if (cachedCall) {
/**
* Emitted when the circuit breaker is using coalesce cache
* and finds a cached promise.
* @event CircuitBreaker#coalesceCacheHit
*/
this.emit('coalesceCacheHit');
return cachedCall;
}
/**
* Emitted when the circuit breaker does not find a value in
* coalesce cache, but the coalesce option is enabled.
* @event CircuitBreaker#coalesceCacheMiss
*/
this.emit('coalesceCacheMiss');
}
if (!this.closed && !this.pendingClose) {
/**
* Emitted when the circuit breaker is open and failing fast
* @event CircuitBreaker#reject
* @type {Error}
*/
var error = buildError('Breaker is open', 'EOPENBREAKER');
this.emit('reject', error);
return fallback(this, error, args) || Promise.reject(error);
}
this[PENDING_CLOSE] = false;
var timeout;
var timeoutError = false;
var call = new Promise(function (resolve, reject) {
var latencyStartTime = Date.now();
if (_this2.semaphore.test()) {
if (_this2.options.timeout) {
timeout = setTimeout(function () {
timeoutError = true;
var error = buildError("Timed out after ".concat(_this2.options.timeout, "ms"), 'ETIMEDOUT');
var latency = Date.now() - latencyStartTime;
_this2.semaphore.release();
/**
* Emitted when the circuit breaker action takes longer than
* `options.timeout`
* @event CircuitBreaker#timeout
* @type {Error}
*/
_this2.emit('timeout', error, latency, args);
handleError(error, _this2, timeout, args, latency, resolve, reject);
resetCoalesce(_this2, cacheKey, 'timeout');
if (_this2.options.abortController) {
_this2.options.abortController.abort();
}
}, _this2.options.timeout);
}
try {
var _result = _this2.action.apply(context, args);
var promise = typeof _result.then === 'function' ? _result : Promise.resolve(_result);
promise.then(function (result) {
if (!timeoutError) {
clearTimeout(timeout);
/**
* Emitted when the circuit breaker action succeeds
* @event CircuitBreaker#success
* @type {any} the return value from the circuit
*/
_this2.emit('success', result, Date.now() - latencyStartTime);
resetCoalesce(_this2, cacheKey, 'success');
_this2.semaphore.release();
resolve(result);
if (_this2.options.cache) {
_this2.options.cacheTransport.set(cacheKey, promise, _this2.options.cacheTTL > 0 ? Date.now() + _this2.options.cacheTTL : 0);
}
}
})["catch"](function (error) {
if (!timeoutError) {
_this2.semaphore.release();
var latencyEndTime = Date.now() - latencyStartTime;
handleError(error, _this2, timeout, args, latencyEndTime, resolve, reject);
resetCoalesce(_this2, cacheKey, 'error');
}
});
} catch (error) {
_this2.semaphore.release();
var latency = Date.now() - latencyStartTime;
handleError(error, _this2, timeout, args, latency, resolve, reject);
resetCoalesce(_this2, cacheKey, 'error');
}
} else {
var _latency = Date.now() - latencyStartTime;
var _err = buildError('Semaphore locked', 'ESEMLOCKED');
/**
* Emitted when the rate limit has been reached and there
* are no more locks to be obtained.
* @event CircuitBreaker#semaphoreLocked
* @type {Error}
*/
_this2.emit('semaphoreLocked', _err, _latency);
handleError(_err, _this2, timeout, args, _latency, resolve, reject);
resetCoalesce(_this2, cacheKey);
}
});
/* When coalesce is enabled, store promise in coalesceCache */
if (this.options.coalesce) {
this.options.coalesceCache.set(cacheKey, call, this.options.coalesceTTL > 0 ? Date.now() + this.options.coalesceTTL : 0);
}
return call;
}
/**
* Clears the cache of this {@link CircuitBreaker}
* @returns {void}
*/
}, {
key: "clearCache",
value: function clearCache() {
if (this.options.cache) {
this.options.cacheTransport.flush();
}
if (this.options.coalesceCache) {
this.options.coalesceCache.flush();
}
}
/**
* Provide a health check function to be called periodically. The function
* should return a Promise. If the promise is rejected the circuit will open.
* This is in addition to the existing circuit behavior as defined by
* `options.errorThresholdPercentage` in the constructor. For example, if the
* health check function provided here always returns a resolved promise, the
* circuit can still trip and open if there are failures exceeding the
* configured threshold. The health check function is executed within the
* circuit breaker's execution context, so `this` within the function is the
* circuit breaker itself.
*
* @param {Function} func a health check function which returns a promise.
* @param {Number} [interval] the amount of time between calls to the health
* check function. Default: 5000 (5 seconds)
*
* @returns {void}
*
* @fires CircuitBreaker#healthCheckFailed
* @throws {TypeError} if `interval` is supplied but not a number
*/
}, {
key: "healthCheck",
value: function healthCheck(func, interval) {
var _this3 = this;
interval = interval || 5000;
if (typeof func !== 'function') {
throw new TypeError('Health check function must be a function');
}
if (isNaN(interval)) {
throw new TypeError('Health check interval must be a number');
}
var check = function check(_) {
func.apply(_this3)["catch"](function (e) {
/**
* Emitted with the user-supplied health check function
* returns a rejected promise.
* @event CircuitBreaker#healthCheckFailed
* @type {Error}
*/
_this3.emit('healthCheckFailed', e);
_this3.open();
});
};
var timer = setInterval(check, interval);
if (typeof timer.unref === 'function') {
timer.unref();
}
check();
}
/**
* Enables this circuit. If the circuit is the disabled
* state, it will be re-enabled. If not, this is essentially
* a noop.
* @returns {void}
*/
}, {
key: "enable",
value: function enable() {
this[ENABLED] = true;
this.status.startListeneningForRotateEvent();
}
/**
* Disables this circuit, causing all calls to the circuit's function
* to be executed without circuit or fallback protection.
* @returns {void}
*/
}, {
key: "disable",
value: function disable() {
this[ENABLED] = false;
this.status.removeRotateBucketControllerListener();
}
/**
* Retrieves the current AbortSignal from the abortController, if available.
* This signal can be used to monitor ongoing requests.
* @returns {AbortSignal|undefined} The AbortSignal if present,
* otherwise undefined.
*/
}, {
key: "getSignal",
value: function getSignal() {
if (this.options.abortController && this.options.abortController.signal) {
return this.options.abortController.signal;
}
return undefined;
}
/**
* Retrieves the current AbortController instance.
* This controller can be used to manually abort ongoing requests or create
* a new signal.
* @returns {AbortController|undefined} The AbortController if present,
* otherwise undefined.
*/
}, {
key: "getAbortController",
value: function getAbortController() {
return this.options.abortController;
}
}], [{
key: "isOurError",
value:
/**
* Returns true if the provided error was generated here. It will be false
* if the error came from the action itself.
* @param {Error} error The Error to check.
* @returns {Boolean} true if the error was generated here
*/
function isOurError(error) {
return !!error[OUR_ERROR];
}
/**
* Create a new Status object,
* helpful when you need to prime a breaker with stats
* @param {Object} options -
* @param {Number} options.rollingCountBuckets number of buckets in the window
* @param {Number} options.rollingCountTimeout the duration of the window
* @param {Boolean} options.rollingPercentilesEnabled whether to calculate
* @param {Object} options.stats user supplied stats
* @returns {Status} a new {@link Status} object
*/
}, {
key: "newStatus",
value: function newStatus(options) {
return new Status(options);
}
}]);
}(EventEmitter);
function handleError(error, circuit, timeout, args, latency, resolve, reject) {
var _circuit$options;
clearTimeout(timeout);
if ((_circuit$options = circuit.options).errorFilter.apply(_circuit$options, [error].concat(_toConsumableArray(args)))) {
// The error was filtered, so emit 'success'
circuit.emit('success', error, latency);
} else {
// Error was not filtered, so emit 'failure'
fail(circuit, error, args, latency);
// Only call the fallback function if errorFilter doesn't succeed
// If the fallback function succeeds, resolve
var fb = fallback(circuit, error, args);
if (fb) return resolve(fb);
}
// In all other cases, reject
reject(error);
}
function fallback(circuit, err, args) {
if (circuit[FALLBACK_FUNCTION]) {
try {
var result = circuit[FALLBACK_FUNCTION].apply(circuit[FALLBACK_FUNCTION], [].concat(_toConsumableArray(args), [err]));
/**
* Emitted when the circuit breaker executes a fallback function
* @event CircuitBreaker#fallback
* @type {any} the return value of the fallback function
*/
circuit.emit('fallback', result, err);
if (result instanceof Promise) return result;
return Promise.resolve(result);
} catch (e) {
return Promise.reject(e);
}
}
}
function fail(circuit, err, args, latency) {
/**
* Emitted when the circuit breaker action fails
* @event CircuitBreaker#failure
* @type {Error}
*/
circuit.emit('failure', err, latency, args);
if (circuit.warmUp) return;
// check stats to see if the circuit should be opened
var stats = circuit.stats;
if (stats.fires < circuit.volumeThreshold && !circuit.halfOpen) return;
var errorRate = stats.failures / stats.fires * 100;
if (errorRate > circuit.options.errorThresholdPercentage || circuit.halfOpen) {
circuit.open();
}
}
function resetCoalesce(circuit, cacheKey, event) {
/**
* Reset coalesce cache for this cacheKey, depending on
* options.coalesceResetOn set.
* @param {@link CircuitBreaker} circuit what circuit is to be cleared
* @param {string} cacheKey cache key to clear.
* @param {string} event optional, can be `error`, `success`, `timeout`
* @returns {void}
*/
if (!event || circuit.options.coalesceResetOn.includes(event)) {
var _circuit$options$coal;
(_circuit$options$coal = circuit.options.coalesceCache) === null || _circuit$options$coal === void 0 || _circuit$options$coal["delete"](cacheKey);
}
}
function buildError(msg, code) {
var error = new Error(msg);
error.code = code;
error[OUR_ERROR] = true;
return error;
}
// http://stackoverflow.com/a/2117523
var nextName = function nextName() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0;
var v = c === 'x' ? r : r & 0x3 | 0x8;
return v.toString(16);
});
};
module.exports = CircuitBreaker;
/***/ }),
/***/ "./lib/semaphore.js":
/*!**************************!*\
!*** ./lib/semaphore.js ***!
\**************************/
/***/ ((module, exports) => {
module.exports = semaphore;
function semaphore(count) {
var resolvers = [];
var counter = count;
var sem = {
take: take,
release: release,
test: test
};
Object.defineProperty(sem, 'count', {
get: function get(_) {
return counter;
},
enumerable: true
});
return sem;
function take(timeout) {
if (counter > 0) {
--counter;
return Promise.resolve(release);
}
return new Promise(function (resolve, reject) {
resolvers.push(function (_) {
--counter;
resolve(release);
});
if (timeout) {
setTimeout(function (_) {
resolvers.shift();
var err = new Error("Timed out after ".concat(timeout, "ms"));
err.code = 'ETIMEDOUT';
reject(err);
}, timeout);
}
});
}
function release() {
counter++;
if (resolvers.length > 0) {
resolvers.shift()();
}
}
function test() {
if (counter < 1) return false;
return take() && true;
}
}
/***/ }),
/***/ "./lib/status.js":
/*!***********************!*\
!*** ./lib/status.js ***!
\***********************/
/***/ ((module, exports, __webpack_require__) => {
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), true).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: true, configurable: true, writable: true }) : e[r] = t, e; }
function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); }
function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, _toPropertyKey(o.key), o); } }
function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), Object.defineProperty(e, "prototype", { writable: false }), e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return (String )(t); }
function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
function _possibleConstructorReturn(t, e) { if (e && ("object" == _typeof(e) || "function" == typeof e)) return e; if (void 0 !== e) throw new TypeError("Derived constructors may only return object or undefined"); return _assertThisInitialized(t); }
function _assertThisInitialized(e) { if (void 0 === e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); return e; }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
function _getPrototypeOf(t) { return _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function (t) { return t.__proto__ || Object.getPrototypeOf(t); }, _getPrototypeOf(t); }
function _inherits(t, e) { if ("function" != typeof e && null !== e) throw new TypeError("Super expression must either be null or a function"); t.prototype = Object.create(e && e.prototype, { constructor: { value: t, writable: true, configurable: true } }), Object.defineProperty(t, "prototype", { writable: false }), e && _setPrototypeOf(t, e); }
function _setPrototypeOf(t, e) { return _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function (t, e) { return t.__proto__ = e, t; }, _setPrototypeOf(t, e); }
var WINDOW = Symbol('window');
var BUCKETS = Symbol('buckets');
var TIMEOUT = Symbol('timeout');
var PERCENTILES = Symbol('percentiles');
var BUCKET_INTERVAL = Symbol('bucket-interval');
var SNAPSHOT_INTERVAL = Symbol('snapshot-interval');
var ROTATE_EVENT_NAME = Symbol('rotate-event-name');
var EventEmitter = (__webpack_require__(/*! events */ "./node_modules/events/events.js").EventEmitter);
/**
* Tracks execution status for a given {@link CircuitBreaker}.
* A Status instance is created for every {@link CircuitBreaker}
* and does not typically need to be created by a user.
*
* A Status instance will listen for all events on the {@link CircuitBreaker}
* and track them in a rolling statistical window. The window duration is
* determined by the `rollingCountTimeout` option provided to the
* {@link CircuitBreaker}. The window consists of an array of Objects,
* each representing the counts for a {@link CircuitBreaker}'s events.
*
* The array's length is determined by the {@link CircuitBreaker}'s
* `rollingCountBuckets` option. The duration of each slice of the window
* is determined by dividing the `rollingCountTimeout` by
* `rollingCountBuckets`.
*
* @class Status
* @extends EventEmitter
* @param {Object} options for the status window
* @param {Number} options.rollingCountBuckets number of buckets in the window
* @param {Number} options.rollingCountTimeout the duration of the window
* @param {Boolean} options.rollingPercentilesEnabled whether to calculate
* percentiles
* @param {Object} options.stats object of previous stats
* @example
* // Creates a 1 second window consisting of ten time slices,
* // each 100ms long.
* const circuit = circuitBreaker(fs.readFile,
* { rollingCountBuckets: 10, rollingCountTimeout: 1000});
*
* // get the cumulative statistics for the last second
* circuit.status.stats;
*
* // get the array of 10, 1 second time slices for the last second
* circuit.status.window;
* @fires Status#snapshot
* @see CircuitBreaker#status
*/
var Status = /*#__PURE__*/function (_EventEmitter) {
function Status(options) {
var _this;
_classCallCheck(this, Status);
_this = _callSuper(this, Status);
// Set up our statistical rolling window
_this[BUCKETS] = options.rollingCountBuckets || 10;
_this[TIMEOUT] = options.rollingCountTimeout || 10000;
_this[WINDOW] = new Array(_this[BUCKETS]);
_this[PERCENTILES] = [0.0, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.995, 1];
_this[ROTATE_EVENT_NAME] = 'rotate';
// Default this value to true
_this.rollingPercentilesEnabled = options.rollingPercentilesEnabled !== false;
// Default this value to true
_this.enableSnapshots = options.enableSnapshots !== false;
// can be undefined
_this.rotateBucketController = options.rotateBucketController;
_this.rotateBucket = nextBucket(_this[WINDOW]);
// prime the window with buckets
for (var i = 0; i < _this[BUCKETS]; i++) _this[WINDOW][i] = bucket();
var bucketInterval = Math.floor(_this[TIMEOUT] / _this[BUCKETS]);
if (_this.rotateBucketController) {
// rotate the buckets based on an optional EventEmitter
_this.startListeneningForRotateEvent();
} else {
// or rotate the buckets periodically
_this[BUCKET_INTERVAL] = setInterval(_this.rotateBucket, bucketInterval);
// No unref() in the browser
if (typeof _this[BUCKET_INTERVAL].unref === 'function') {
_this[BUCKET_INTERVAL].unref();
}
}
/**
* Emitted at each time-slice. Listeners for this
* event will receive a cumulative snapshot of the current status window.
* @event Status#snapshot
* @type {Object}
*/
if (_this.enableSnapshots) {
_this[SNAPSHOT_INTERVAL] = setInterval(function (_) {
return _this.emit('snapshot', _this.stats);
}, bucketInterval);
if (typeof _this[SNAPSHOT_INTERVAL].unref === 'function') {
_this[SNAPSHOT_INTERVAL].unref();
}
}
if (options.stats) {
_this[WINDOW][0] = _objectSpread(_objectSpread({}, bucket()), options.stats);
}
return _this;
}
/**
* Get the cumulative stats for the current window
* @type {Object}
*/
_inherits(Status, _EventEmitter);
return _createClass(Status, [{
key: "stats",
get: function get() {
var _this2 = this;
var totals = this[WINDOW].reduce(function (acc, val) {
if (!val) {
return acc;
}
Object.keys(acc).forEach(function (key) {
if (key !== 'latencyTimes' && key !== 'percentiles') {
acc[key] += val[key] || 0;
}
});
if (_this2.rollingPercentilesEnabled) {
if (val.latencyTimes) {
acc.latencyTimes = acc.latencyTimes.concat(val.latencyTimes);
}
}
return acc;
}, bucket());
if (this.rollingPercentilesEnabled) {
// Sort the latencyTimes
totals.latencyTimes.sort(function (a, b) {
return a - b;
});
// Get the mean latency
// Mean = sum of all values in the array/length of array
if (totals.latencyTimes.length) {
totals.latencyMean = totals.latencyTimes.reduce(function (a, b) {
return a + b;
}, 0) / totals.latencyTimes.length;
} else {
totals.latencyMean = 0;
}
// Calculate Percentiles
this[PERCENTILES].forEach(function (percentile) {
totals.percentiles[percentile] = calculatePercentile(percentile, totals.latencyTimes);
});
} else {
totals.latencyMean = -1;
this[PERCENTILES].forEach(function (percentile) {
totals.percentiles[percentile] = -1;
});
}
return totals;
}
/**
* Gets the stats window as an array of time-sliced objects.
* @type {Array}
*/
}, {
key: "window",
get: function get() {
return this[WINDOW].slice();
}
}, {
key: "increment",
value: function increment(property, latencyRunTime) {
this[WINDOW][0][property]++;
if (property === 'successes' || property === 'failures' || property === 'timeouts') {
this[WINDOW][0].latencyTimes.push(latencyRunTime || 0);
}
}
}, {
key: "open",
value: function open() {
this[WINDOW][0].isCircuitBreakerOpen = true;
}
}, {
key: "close",
value: function close() {
this[WINDOW][0].isCircuitBreakerOpen = false;
}
}, {
key: "shutdown",
value: function shutdown() {
this.removeAllListeners();
// interval is not set if rotateBucketController is provided
if (this.rotateBucketController === undefined) {
clearInterval(this[BUCKET_INTERVAL]);
} else {
this.removeRotateBucketControllerListener();
}
if (this.enableSnapshots) {
clearInterval(this[SNAPSHOT_INTERVAL]);
}
}
}, {
key: "removeRotateBucketControllerListener",
value: function removeRotateBucketControllerListener() {
if (this.rotateBucketController) {
this.rotateBucketController.removeListener(this[ROTATE_EVENT_NAME], this.rotateBucket);
}
}
}, {
key: "startListeneningForRotateEvent",
value: function startListeneningForRotateEvent() {
if (this.rotateBucketController && this.rotateBucketController.listenerCount(this[ROTATE_EVENT_NAME], this.rotateBucket) === 0) {
this.rotateBucketController.on(this[ROTATE_EVENT_NAME], this.rotateBucket);
}
}
}]);
}(EventEmitter);
var nextBucket = function nextBucket(window) {
return function (_) {
window.pop();
window.unshift(bucket());
};
};
var bucket = function bucket(_) {
return {
failures: 0,
fallbacks: 0,
successes: 0,
rejects: 0,
fires: 0,
timeouts: 0,
cacheHits: 0,
cacheMisses: 0,
coalesceCacheHits: 0,
coalesceCacheMisses: 0,
semaphoreRejections: 0,
percentiles: {},
latencyTimes: []
};
};
function calculatePercentile(percentile, arr) {
if (percentile === 0) {
return arr[0] || 0;
}
var idx = Math.ceil(percentile * arr.length);
return arr[idx - 1] || 0;
}
module.exports = Status;
/***/ }),
/***/ "./node_modules/events/events.js":
/*!***************************************!*\
!*** ./node_modules/events/events.js ***!
\***************************************/
/***/ ((module) => {
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
var R = typeof Reflect === 'object' ? Reflect : null;
var ReflectApply = R && typeof R.apply === 'function'
? R.apply
: function ReflectApply(target, receiver, args) {
return Function.prototype.apply.call(target, receiver, args);
};
var ReflectOwnKeys;
if (R && typeof R.ownKeys === 'function') {
ReflectOwnKeys = R.ownKeys;
} else if (Object.getOwnPropertySymbols) {
ReflectOwnKeys = function ReflectOwnKeys(target) {
return Object.getOwnPropertyNames(target)
.concat(Object.getOwnPropertySymbols(target));
};
} else {
ReflectOwnKeys = function ReflectOwnKeys(target) {
return Object.getOwnPropertyNames(target);
};
}
function ProcessEmitWarning(warning) {
if (console && console.warn) console.warn(warning);
}
var NumberIsNaN = Number.isNaN || function NumberIsNaN(value) {
return value !== value;
};
function EventEmitter() {
EventEmitter.init.call(this);
}
module.exports = EventEmitter;
module.exports.once = once;
// Backwards-compat with node 0.10.x
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.prototype._events = undefined;
EventEmitter.prototype._eventsCount = 0;
EventEmitter.prototype._maxListeners = undefined;
// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
var defaultMaxListeners = 10;
function checkListener(listener) {
if (typeof listener !== 'function') {
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
}
}
Object.defineProperty(EventEmitter, 'defaultMaxListeners', {
enumerable: true,
get: function() {
return defaultMaxListeners;
},
set: function(arg) {
if (typeof arg !== 'number' || arg < 0 || NumberIsNaN(arg)) {
throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received ' + arg + '.');
}
defaultMaxListeners = arg;
}
});
EventEmitter.init = function() {
if (this._events === undefined ||
this._events === Object.getPrototypeOf(this)._events) {
this._events = Object.create(null);
this._eventsCount = 0;
}
this._maxListeners = this._maxListeners || undefined;
};
// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
if (typeof n !== 'number' || n < 0 || NumberIsNaN(n)) {
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
}
this._maxListeners = n;
return this;
};
function _getMaxListeners(that) {
if (that._maxListeners === undefined)
return EventEmitter.defaultMaxListeners;
return that._maxListeners;
}
EventEmitter.prototype.getMaxListeners = function getMaxListeners() {
return _getMaxListeners(this);
};
EventEmitter.prototype.emit = function emit(type) {
var args = [];
for (var i = 1; i < arguments.length; i++) args.push(arguments[i]);
var doError = (type === 'error');
var events = this._events;
if (events !== undefined)
doError = (doError && events.error === undefined);
else if (!doError)
return false;
// If there is no 'error' event listener then throw.
if (doError) {
var er;
if (args.length > 0)
er = args[0];
if (er instanceof Error) {
// Note: The comments on the `throw` lines are intentional, they show
// up in Node's output if this results in an unhandled exception.
throw er; // Unhandled 'error' event
}
// At least give some kind of context to the user
var err = new Error('Unhandled error.' + (er ? ' (' + er.message + ')' : ''));
err.context = er;
throw err; // Unhandled 'error' event
}
var handler = events[type];
if (handler === undefined)
return false;
if (typeof handler === 'function') {
ReflectApply(handler, this, args);
} else {
var len = handler.length;
var listeners = arrayClone(handler, len);
for (var i = 0; i < len; ++i)
ReflectApply(listeners[i], this, args);
}
return true;
};
function _addListener(target, type, listener, prepend) {
var m;
var events;
var existing;
checkListener(listener);
events = target._events;
if (events === undefined) {
events = target._events = Object.create(null);
target._eventsCount = 0;
} else {
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (events.newListener !== undefined) {
target.emit('newListener', type,
listener.listener ? listener.listener : listener);
// Re-assign `events` because a newListener handler could have caused the
// this._events to be assigned to a new object
events = target._events;
}
existing = events[type];
}
if (existing === undefined) {
// Optimize the case of one listener. Don't need the extra array object.
existing = events[type] = listener;
++target._eventsCount;
} else {
if (typeof existing === 'function') {
// Adding the second element, need to change to array.
existing = events[type] =
prepend ? [listener, existing] : [existing, listener];
// If we've already got an array, just append.
} else if (prepend) {
existing.unshift(listener);
} else {
existing.push(listener);
}
// Check for listener leak
m = _getMaxListeners(target);
if (m > 0 && existing.length > m && !existing.warned) {
existing.warned = true;
// No error code for this since it is a Warning
// eslint-disable-next-line no-restricted-syntax
var w = new Error('Possible EventEmitter memory leak detected. ' +
existing.length + ' ' + String(type) + ' listeners ' +
'added. Use emitter.setMaxListeners() to ' +
'increase limit');
w.name = 'MaxListenersExceededWarning';
w.emitter = target;
w.type = type;
w.count = existing.length;
ProcessEmitWarning(w);
}
}
return target;
}
EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.prependListener =
function prependListener(type, listener) {
return _addListener(this, type, listener, true);
};
function onceWrapper() {
if (!this.fired) {
this.target.removeListener(this.type, this.wrapFn);
this.fired = true;
if (arguments.length === 0)
return this.listener.call(this.target);
return this.listener.apply(this.target, arguments);
}
}
function _onceWrap(target, type, listener) {
var state = { fired: false, wrapFn: undefined, target: target, type: type, listener: listener };
var wrapped = onceWrapper.bind(state);
wrapped.listener = listener;
state.wrapFn = wrapped;
return wrapped;
}
EventEmitter.prototype.once = function once(type, listener) {
checkListener(listener);
this.on(type, _onceWrap(this, type, listener));
return this;
};
EventEmitter.prototype.prependOnceListener =
function prependOnceListener(type, listener) {
checkListener(listener);
this.prependListener(type, _onceWrap(this, type, listener));
return this;
};
// Emits a 'removeListener' event if and only if the listener was removed.
EventEmitter.prototype.removeListener =
function removeListener(type, listener) {
var list, events, position, i, originalListener;
checkListener(listener);
events = this._events;
if (events === undefined)
return this;
list = events[type];
if (list === undefined)
return this;
if (list === listener || list.listener === listener) {
if (--this._eventsCount === 0)
this._events = Object.create(null);
else {
delete events[type];
if (events.removeListener)
this.emit('removeListener', type, list.listener || listener);
}
} else if (typeof list !== 'function') {
position = -1;
for (i = list.length - 1; i >= 0; i--) {
if (list[i] === listener || list[i].listener === listener) {
originalListener = list[i].listener;
position = i;
break;
}
}
if (position < 0)
return this;
if (position === 0)
list.shift();
else {
spliceOne(list, position);
}
if (list.length === 1)
events[type] = list[0];
if (events.removeListener !== undefined)
this.emit('removeListener', type, originalListener || listener);
}
return this;
};
EventEmitter.prototype.off = EventEmitter.prototype.removeListener;
EventEmitter.prototype.removeAllListeners =
function removeAllListeners(type) {
var listeners, events, i;
events = this._events;
if (events === undefined)
return this;
// not listening for removeListener, no need to emit
if (events.removeListener === undefined) {
if (arguments.length === 0) {
this._events = Object.create(null);
this._eventsCount = 0;
} else if (events[type] !== undefined) {
if (--this._eventsCount === 0)
this._events = Object.create(null);
else
delete events[type];
}
return this;
}
// emit removeListener for all listeners on all events
if (arguments.length === 0) {
var keys = Object.keys(events);
var key;
for (i = 0; i < keys.length; ++i) {
key = keys[i];
if (key === 'removeListener') continue;
this.removeAllListeners(key);
}
this.removeAllListeners('removeListener');
this._events = Object.create(null);
this._eventsCount = 0;
return this;
}
listeners = events[type];
if (typeof listeners === 'function') {
this.removeListener(type, listeners);
} else if (listeners !== undefined) {
// LIFO order
for (i = listeners.length - 1; i >= 0; i--) {
this.removeListener(type, listeners[i]);
}
}
return this;
};
function _listeners(target, type, unwrap) {
var events = target._events;
if (events === undefined)
return [];
var evlistener = events[type];
if (evlistener === undefined)
return [];
if (typeof evlistener === 'function')
return unwrap ? [evlistener.listener || evlistener] : [evlistener];
return unwrap ?
unwrapListeners(evlistener) : arrayClone(evlistener, evlistener.length);
}
EventEmitter.prototype.listeners = function listeners(type) {
return _listeners(this, type, true);
};
EventEmitter.prototype.rawListeners = function rawListeners(type) {
return _listeners(this, type, false);
};
EventEmitter.listenerCount = function(emitter, type) {
if (typeof emitter.listenerCount === 'function') {
return emitter.listenerCount(type);
} else {
return listenerCount.call(emitter, type);
}
};
EventEmitter.prototype.listenerCount = listenerCount;
function listenerCount(type) {
var events = this._events;
if (events !== undefined) {
var evlistener = events[type];
if (typeof evlistener === 'function') {
return 1;
} else if (evlistener !== undefined) {
return evlistener.length;
}
}
return 0;
}
EventEmitter.prototype.eventNames = function eventNames() {
return this._eventsCount > 0 ? ReflectOwnKeys(this._events) : [];
};
function arrayClone(arr, n) {
var copy = new Array(n);
for (var i = 0; i < n; ++i)
copy[i] = arr[i];
return copy;
}
function spliceOne(list, index) {
for (; index + 1 < list.length; index++)
list[index] = list[index + 1];
list.pop();
}
function unwrapListeners(arr) {
var ret = new Array(arr.length);
for (var i = 0; i < ret.length; ++i) {
ret[i] = arr[i].listener || arr[i];
}
return ret;
}
function once(emitter, name) {
return new Promise(function (resolve, reject) {
function errorListener(err) {
emitter.removeListener(name, resolver);
reject(err);
}
function resolver() {
if (typeof emitter.removeListener === 'function') {
emitter.removeListener('error', errorListener);
}
resolve([].slice.call(arguments));
}
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
if (name !== 'error') {
addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
}
});
}
function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
if (typeof emitter.on === 'function') {
eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
}
}
function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
if (typeof emitter.on === 'function') {
if (flags.once) {
emitter.once(name, listener);
} else {
emitter.on(name, listener);
}
} else if (typeof emitter.addEventListener === 'function') {
// EventTarget does not have `error` event semantics like Node
// EventEmitters, we do not listen for `error` events here.
emitter.addEventListener(name, function wrapListener(arg) {
// IE does not have builtin `{ once: true }` support so we
// have to do it manually.
if (flags.once) {
emitter.removeEventListener(name, wrapListener);
}
listener(arg);
});
} else {
throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
}
}
/***/ })
/******/ });
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ var cachedModule = __webpack_module_cache__[moduleId];
/******/ if (cachedModule !== undefined) {
/******/ return cachedModule.exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
/******/ // no module.id needed
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module is referenced by other modules so it can't be inlined
/******/ var __webpack_exports__ = __webpack_require__("./index.js");
/******/
/******/ return __webpack_exports__;
/******/ })()
;
});
} (opossum));
var opossumExports = opossum.exports;
var CircuitBreaker = /*@__PURE__*/getDefaultExportFromCjs(opossumExports);
const defaultCircuitBreakerConfiguration = {
// each failed fire already represents 5 HTTP retries, so 3 fires = 15 failed HTTP calls, which
// is a very clear signal. since each "fire" is already 5 retries, 3 fires = strong signal
// the endpoint is down
volumeThreshold: 3,
// tolerates 1 transient failure in 3 before tripping; avoids opening on a single issue
// any failure rate at or above 50% should open the circuit.
// a single transient failure out of 3 = 33%, won't trip.
// 2 out of 3 = 66%, will trip.
errorThresholdPercentage: 50,
// after opening, wait 60s before trying again. 30s (default) is too short - if the
// endpoint is down, it's likely down for at least a minute. no point hammering it sooner.
// btw, 30s is the BatchSpanProcessor export timeout exactly, which means the circuit could
// immediately time out again in HALF_OPEN before getting a real result if it were below or at
// 30s; 60s gives it room to breathe
resetTimeout: 6e4
};
class CircuitBreakerExporter {
constructor(_exporter, config = defaultCircuitBreakerConfiguration, log) {
this._exporter = _exporter;
const resolvedConfig = {
...defaultCircuitBreakerConfiguration,
...config
};
this.circuitBreaker = new CircuitBreaker(
(spans) => new Promise((resolve, reject) => {
this._exporter.export(spans, (result) => {
if (result.error) {
reject(result.error);
} else {
resolve(result);
}
});
}),
{
// the retrying transport in OTEL already respects the BatchSpanProcessor's exportTimeoutMillis
// deadline (30s), so opossum's own timeout would fire unnecessarely (or mid-retry), causing the retry
// backoff setTimeouts to keep running in the background and hitting the collector even
// after opossum has given up on the fire() — defeating the circuit breaker entirely.
timeout: false,
// one full failed export cycle takes ~13s: 5s (scheduledDelayMillis) + ~8s (retry backoffs).
// to have all 3 failures (volumeThreshold) visible in the rolling window simultaneously, it
// needs to cover at least 3 cycles: 3 × 13s = ~39s. 60s gives comfortable headroom.
rollingCountTimeout: 6e4,
...resolvedConfig
}
);
if (log) {
log.info(resolvedConfig, "Circuit breaker span exporter configured");
this.circuitBreaker.on("open", () => {
const stats = this.circuitBreaker.stats;
log.error(
{
state: "open",
resetTimeout: resolvedConfig.resetTimeout,
errorThresholdPercentage: resolvedConfig.errorThresholdPercentage,
volumeThreshold: resolvedConfig.volumeThreshold,
stats: {
fires: stats.fires,
failures: stats.failures,
successes: stats.successes,
rejects: stats.rejects,
timeouts: stats.timeouts,
latencyMean: stats.latencyMean
}
},
"Circuit breaker opened: span export failures exceeded threshold, dropping spans until circuit resets"
);
});
this.circuitBreaker.on("halfOpen", (resetTimeout) => {
const stats = this.circuitBreaker.stats;
log.warn(
{
state: "halfOpen",
resetTimeout,
stats: {
fires: stats.fires,
failures: stats.failures,
successes: stats.successes,
rejects: stats.rejects,
timeouts: stats.timeouts,
latencyMean: stats.latencyMean
}
},
"Circuit breaker half-open: probing span exporter before closing"
);
});
this.circuitBreaker.on("close", () => {
const stats = this.circuitBreaker.stats;
log.info(
{
state: "closed",
stats: {
fires: stats.fires,
failures: stats.failures,
successes: stats.successes,
rejects: stats.rejects,
timeouts: stats.timeouts,
latencyMean: stats.latencyMean
}
},
"Circuit breaker closed: span exporter recovered, resuming normal export"
);
});
this.circuitBreaker.on("failure", (error, latencyMs) => {
log.warn(
{
state: this.circuitBreaker.opened ? "open" : this.circuitBreaker.pendingClose ? "halfOpen" : "closed",
latencyMs,
error
},
"Circuit breaker recorded a span export failure"
);
});
this.circuitBreaker.on("reject", () => {
log.debug(
{ state: "open" },
"Circuit breaker rejected span export: circuit is open"
);
});
}
if (this._exporter.forceFlush) {
this.forceFlush = () => this._exporter.forceFlush();
}
}
circuitBreaker;
export(spans, resultCallback) {
this.circuitBreaker.fire(spans).then(resultCallback).catch((error) => {
if (error?.code === "EOPENBREAKER") {
return resultCallback({ code: ExportResultCode.SUCCESS });
}
return resultCallback({ code: ExportResultCode.FAILED, error });
});
}
forceFlush;
shutdown() {
this.circuitBreaker.shutdown();
return this._exporter.shutdown();
}
}
export { CircuitBreakerExporter as C, getEnvStr as a, diagLogLevelFromEnv as d, getEnvBool as g, otelCtxForRequestId as o, useOpenTelemetry as u };