@effect-gql/opentelemetry 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/index.cjs +374 -0
- package/index.cjs.map +1 -0
- package/index.d.cts +416 -0
- package/index.d.ts +416 -0
- package/index.js +358 -0
- package/index.js.map +1 -0
- package/package.json +14 -30
- package/dist/context-propagation.d.ts +0 -78
- package/dist/context-propagation.d.ts.map +0 -1
- package/dist/context-propagation.js +0 -125
- package/dist/context-propagation.js.map +0 -1
- package/dist/index.d.ts +0 -111
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -120
- package/dist/index.js.map +0 -1
- package/dist/traced-router.d.ts +0 -90
- package/dist/traced-router.d.ts.map +0 -1
- package/dist/traced-router.js +0 -154
- package/dist/traced-router.js.map +0 -1
- package/dist/tracing-extension.d.ts +0 -52
- package/dist/tracing-extension.d.ts.map +0 -1
- package/dist/tracing-extension.js +0 -110
- package/dist/tracing-extension.js.map +0 -1
- package/dist/tracing-middleware.d.ts +0 -78
- package/dist/tracing-middleware.d.ts.map +0 -1
- package/dist/tracing-middleware.js +0 -96
- package/dist/tracing-middleware.js.map +0 -1
- package/dist/utils.d.ts +0 -19
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -47
- package/dist/utils.js.map +0 -1
- package/src/context-propagation.ts +0 -177
- package/src/index.ts +0 -139
- package/src/traced-router.ts +0 -240
- package/src/tracing-extension.ts +0 -175
- package/src/tracing-middleware.ts +0 -177
- package/src/utils.ts +0 -48
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { GraphQLExtension } from "@effect-gql/core";
|
|
2
|
-
import { ExtensionsService } from "@effect-gql/core";
|
|
3
|
-
/**
|
|
4
|
-
* Configuration for the GraphQL tracing extension
|
|
5
|
-
*/
|
|
6
|
-
export interface TracingExtensionConfig {
|
|
7
|
-
/**
|
|
8
|
-
* Include the query source in span attributes.
|
|
9
|
-
* Default: false (for security - queries may contain sensitive data)
|
|
10
|
-
*/
|
|
11
|
-
readonly includeQuery?: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Include variables in span attributes.
|
|
14
|
-
* Default: false (for security - variables may contain sensitive data)
|
|
15
|
-
*/
|
|
16
|
-
readonly includeVariables?: boolean;
|
|
17
|
-
/**
|
|
18
|
-
* Add trace ID and span ID to the GraphQL response extensions.
|
|
19
|
-
* Useful for correlating client requests with backend traces.
|
|
20
|
-
* Default: false
|
|
21
|
-
*/
|
|
22
|
-
readonly exposeTraceIdInResponse?: boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Custom attributes to add to all spans.
|
|
25
|
-
*/
|
|
26
|
-
readonly customAttributes?: Record<string, string | number | boolean>;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Creates a GraphQL extension that adds OpenTelemetry tracing to all execution phases.
|
|
30
|
-
*
|
|
31
|
-
* This extension:
|
|
32
|
-
* - Creates spans for parse, validate phases
|
|
33
|
-
* - Annotates the current span with operation metadata during execution
|
|
34
|
-
* - Optionally exposes trace ID in response extensions
|
|
35
|
-
*
|
|
36
|
-
* Requires an OpenTelemetry tracer to be provided via Effect's tracing layer
|
|
37
|
-
* (e.g., `@effect/opentelemetry` NodeSdk.layer or OtlpTracer.layer).
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```typescript
|
|
41
|
-
* import { tracingExtension } from "@effect-gql/opentelemetry"
|
|
42
|
-
*
|
|
43
|
-
* const builder = GraphQLSchemaBuilder.empty.pipe(
|
|
44
|
-
* extension(tracingExtension({
|
|
45
|
-
* exposeTraceIdInResponse: true
|
|
46
|
-
* })),
|
|
47
|
-
* query("hello", { ... })
|
|
48
|
-
* )
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
51
|
-
export declare const tracingExtension: (config?: TracingExtensionConfig) => GraphQLExtension<ExtensionsService>;
|
|
52
|
-
//# sourceMappingURL=tracing-extension.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tracing-extension.d.ts","sourceRoot":"","sources":["../src/tracing-extension.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAiB,MAAM,kBAAkB,CAAA;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEpD;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAA;IAE/B;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAEnC;;;;OAIG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAA;IAE1C;;OAEG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAA;CACtE;AA0BD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,SAAS,sBAAsB,KAC9B,gBAAgB,CAAC,iBAAiB,CA2FnC,CAAA"}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.tracingExtension = void 0;
|
|
4
|
-
const effect_1 = require("effect");
|
|
5
|
-
const core_1 = require("@effect-gql/core");
|
|
6
|
-
/**
|
|
7
|
-
* Extract the operation name from a parsed GraphQL document
|
|
8
|
-
*/
|
|
9
|
-
const getOperationName = (document) => {
|
|
10
|
-
for (const definition of document.definitions) {
|
|
11
|
-
if (definition.kind === "OperationDefinition") {
|
|
12
|
-
return definition.name?.value;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return undefined;
|
|
16
|
-
};
|
|
17
|
-
/**
|
|
18
|
-
* Extract the operation type (query, mutation, subscription) from a parsed document
|
|
19
|
-
*/
|
|
20
|
-
const getOperationType = (document) => {
|
|
21
|
-
for (const definition of document.definitions) {
|
|
22
|
-
if (definition.kind === "OperationDefinition") {
|
|
23
|
-
return definition.operation;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return "unknown";
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Creates a GraphQL extension that adds OpenTelemetry tracing to all execution phases.
|
|
30
|
-
*
|
|
31
|
-
* This extension:
|
|
32
|
-
* - Creates spans for parse, validate phases
|
|
33
|
-
* - Annotates the current span with operation metadata during execution
|
|
34
|
-
* - Optionally exposes trace ID in response extensions
|
|
35
|
-
*
|
|
36
|
-
* Requires an OpenTelemetry tracer to be provided via Effect's tracing layer
|
|
37
|
-
* (e.g., `@effect/opentelemetry` NodeSdk.layer or OtlpTracer.layer).
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* ```typescript
|
|
41
|
-
* import { tracingExtension } from "@effect-gql/opentelemetry"
|
|
42
|
-
*
|
|
43
|
-
* const builder = GraphQLSchemaBuilder.empty.pipe(
|
|
44
|
-
* extension(tracingExtension({
|
|
45
|
-
* exposeTraceIdInResponse: true
|
|
46
|
-
* })),
|
|
47
|
-
* query("hello", { ... })
|
|
48
|
-
* )
|
|
49
|
-
* ```
|
|
50
|
-
*/
|
|
51
|
-
const tracingExtension = (config) => ({
|
|
52
|
-
name: "opentelemetry-tracing",
|
|
53
|
-
description: "Adds OpenTelemetry tracing to GraphQL execution phases",
|
|
54
|
-
onParse: (source, document) => effect_1.Effect.withSpan("graphql.parse")(effect_1.Effect.gen(function* () {
|
|
55
|
-
const operationName = getOperationName(document) ?? "anonymous";
|
|
56
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.document.name", operationName);
|
|
57
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.document.operation_count", document.definitions.filter((d) => d.kind === "OperationDefinition").length);
|
|
58
|
-
if (config?.includeQuery) {
|
|
59
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.source", source);
|
|
60
|
-
}
|
|
61
|
-
// Add custom attributes if provided
|
|
62
|
-
if (config?.customAttributes) {
|
|
63
|
-
for (const [key, value] of Object.entries(config.customAttributes)) {
|
|
64
|
-
yield* effect_1.Effect.annotateCurrentSpan(key, value);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
})),
|
|
68
|
-
onValidate: (document, errors) => effect_1.Effect.withSpan("graphql.validate")(effect_1.Effect.gen(function* () {
|
|
69
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.validation.error_count", errors.length);
|
|
70
|
-
if (errors.length > 0) {
|
|
71
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.validation.errors", JSON.stringify(errors.map((e) => e.message)));
|
|
72
|
-
yield* effect_1.Effect.annotateCurrentSpan("error", true);
|
|
73
|
-
}
|
|
74
|
-
})),
|
|
75
|
-
onExecuteStart: (args) => effect_1.Effect.gen(function* () {
|
|
76
|
-
const operationName = args.operationName ?? getOperationName(args.document) ?? "anonymous";
|
|
77
|
-
const operationType = getOperationType(args.document);
|
|
78
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.operation.name", operationName);
|
|
79
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.operation.type", operationType);
|
|
80
|
-
if (config?.includeVariables && args.variableValues) {
|
|
81
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.variables", JSON.stringify(args.variableValues));
|
|
82
|
-
}
|
|
83
|
-
// Expose trace ID in response extensions if configured
|
|
84
|
-
if (config?.exposeTraceIdInResponse) {
|
|
85
|
-
const currentSpanOption = yield* effect_1.Effect.option(effect_1.Effect.currentSpan);
|
|
86
|
-
if (effect_1.Option.isSome(currentSpanOption)) {
|
|
87
|
-
const span = currentSpanOption.value;
|
|
88
|
-
const ext = yield* core_1.ExtensionsService;
|
|
89
|
-
yield* ext.set("tracing", {
|
|
90
|
-
traceId: span.traceId,
|
|
91
|
-
spanId: span.spanId,
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}),
|
|
96
|
-
onExecuteEnd: (result) => effect_1.Effect.gen(function* () {
|
|
97
|
-
const hasErrors = result.errors !== undefined && result.errors.length > 0;
|
|
98
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.response.has_errors", hasErrors);
|
|
99
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.response.has_data", result.data !== null && result.data !== undefined);
|
|
100
|
-
if (hasErrors) {
|
|
101
|
-
yield* effect_1.Effect.annotateCurrentSpan("error", true);
|
|
102
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.errors", JSON.stringify(result.errors.map((e) => ({
|
|
103
|
-
message: e.message,
|
|
104
|
-
path: e.path,
|
|
105
|
-
}))));
|
|
106
|
-
}
|
|
107
|
-
}),
|
|
108
|
-
});
|
|
109
|
-
exports.tracingExtension = tracingExtension;
|
|
110
|
-
//# sourceMappingURL=tracing-extension.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tracing-extension.js","sourceRoot":"","sources":["../src/tracing-extension.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type { GraphQLResolveInfo } from "graphql";
|
|
2
|
-
import type { MiddlewareRegistration } from "@effect-gql/core";
|
|
3
|
-
/**
|
|
4
|
-
* Configuration for resolver tracing middleware
|
|
5
|
-
*/
|
|
6
|
-
export interface ResolverTracingConfig {
|
|
7
|
-
/**
|
|
8
|
-
* Minimum field depth to trace.
|
|
9
|
-
* Depth 0 = root fields (Query.*, Mutation.*).
|
|
10
|
-
* Default: 0 (trace all fields)
|
|
11
|
-
*/
|
|
12
|
-
readonly minDepth?: number;
|
|
13
|
-
/**
|
|
14
|
-
* Maximum field depth to trace.
|
|
15
|
-
* Default: Infinity (no limit)
|
|
16
|
-
*/
|
|
17
|
-
readonly maxDepth?: number;
|
|
18
|
-
/**
|
|
19
|
-
* Field patterns to exclude from tracing.
|
|
20
|
-
* Patterns are matched against "TypeName.fieldName".
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* // Exclude introspection and internal fields
|
|
24
|
-
* excludePatterns: [/^Query\.__/, /\.id$/]
|
|
25
|
-
*/
|
|
26
|
-
readonly excludePatterns?: readonly RegExp[];
|
|
27
|
-
/**
|
|
28
|
-
* Whether to include field arguments in span attributes.
|
|
29
|
-
* Default: false (for security - args may contain sensitive data)
|
|
30
|
-
*/
|
|
31
|
-
readonly includeArgs?: boolean;
|
|
32
|
-
/**
|
|
33
|
-
* Whether to include parent type in span attributes.
|
|
34
|
-
* Default: true
|
|
35
|
-
*/
|
|
36
|
-
readonly includeParentType?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Whether to trace introspection fields (__schema, __type, etc.).
|
|
39
|
-
* Default: false
|
|
40
|
-
*/
|
|
41
|
-
readonly traceIntrospection?: boolean;
|
|
42
|
-
/**
|
|
43
|
-
* Custom span name generator.
|
|
44
|
-
* Default: "graphql.resolve TypeName.fieldName"
|
|
45
|
-
*/
|
|
46
|
-
readonly spanNameGenerator?: (info: GraphQLResolveInfo) => string;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Creates middleware that wraps each resolver in an OpenTelemetry span.
|
|
50
|
-
*
|
|
51
|
-
* Each resolver execution creates a child span with GraphQL-specific attributes:
|
|
52
|
-
* - `graphql.field.name`: The field being resolved
|
|
53
|
-
* - `graphql.field.path`: Full path to the field (e.g., "Query.users.0.posts")
|
|
54
|
-
* - `graphql.field.type`: The return type of the field
|
|
55
|
-
* - `graphql.parent.type`: The parent type name
|
|
56
|
-
* - `graphql.operation.name`: The operation name (if available)
|
|
57
|
-
* - `error`: Set to true if the resolver fails
|
|
58
|
-
* - `error.type`: Error type/class name
|
|
59
|
-
* - `error.message`: Error message
|
|
60
|
-
*
|
|
61
|
-
* Requires an OpenTelemetry tracer to be provided via Effect's tracing layer.
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```typescript
|
|
65
|
-
* import { resolverTracingMiddleware } from "@effect-gql/opentelemetry"
|
|
66
|
-
*
|
|
67
|
-
* const builder = GraphQLSchemaBuilder.empty.pipe(
|
|
68
|
-
* middleware(resolverTracingMiddleware({
|
|
69
|
-
* minDepth: 0,
|
|
70
|
-
* excludePatterns: [/^Query\.__/],
|
|
71
|
-
* includeArgs: false
|
|
72
|
-
* })),
|
|
73
|
-
* query("users", { ... })
|
|
74
|
-
* )
|
|
75
|
-
* ```
|
|
76
|
-
*/
|
|
77
|
-
export declare const resolverTracingMiddleware: (config?: ResolverTracingConfig) => MiddlewareRegistration<never>;
|
|
78
|
-
//# sourceMappingURL=tracing-middleware.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tracing-middleware.d.ts","sourceRoot":"","sources":["../src/tracing-middleware.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AACjD,OAAO,KAAK,EAAE,sBAAsB,EAAqB,MAAM,kBAAkB,CAAA;AAGjF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAE1B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAE1B;;;;;;;OAOG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAE5C;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAA;IAE9B;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAEpC;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAErC;;;OAGG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,MAAM,CAAA;CAClE;AAkCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,yBAAyB,GACpC,SAAS,qBAAqB,KAC7B,sBAAsB,CAAC,KAAK,CAwD7B,CAAA"}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolverTracingMiddleware = void 0;
|
|
4
|
-
const effect_1 = require("effect");
|
|
5
|
-
const utils_1 = require("./utils");
|
|
6
|
-
/**
|
|
7
|
-
* Check if a field should be traced based on configuration
|
|
8
|
-
*/
|
|
9
|
-
const shouldTraceField = (info, config) => {
|
|
10
|
-
// Skip introspection fields unless explicitly enabled
|
|
11
|
-
if (!config?.traceIntrospection && (0, utils_1.isIntrospectionField)(info)) {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
const depth = (0, utils_1.getFieldDepth)(info);
|
|
15
|
-
// Check depth bounds
|
|
16
|
-
if (config?.minDepth !== undefined && depth < config.minDepth) {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
if (config?.maxDepth !== undefined && depth > config.maxDepth) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
// Check exclude patterns
|
|
23
|
-
if (config?.excludePatterns) {
|
|
24
|
-
const fieldPath = `${info.parentType.name}.${info.fieldName}`;
|
|
25
|
-
for (const pattern of config.excludePatterns) {
|
|
26
|
-
if (pattern.test(fieldPath)) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return true;
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Creates middleware that wraps each resolver in an OpenTelemetry span.
|
|
35
|
-
*
|
|
36
|
-
* Each resolver execution creates a child span with GraphQL-specific attributes:
|
|
37
|
-
* - `graphql.field.name`: The field being resolved
|
|
38
|
-
* - `graphql.field.path`: Full path to the field (e.g., "Query.users.0.posts")
|
|
39
|
-
* - `graphql.field.type`: The return type of the field
|
|
40
|
-
* - `graphql.parent.type`: The parent type name
|
|
41
|
-
* - `graphql.operation.name`: The operation name (if available)
|
|
42
|
-
* - `error`: Set to true if the resolver fails
|
|
43
|
-
* - `error.type`: Error type/class name
|
|
44
|
-
* - `error.message`: Error message
|
|
45
|
-
*
|
|
46
|
-
* Requires an OpenTelemetry tracer to be provided via Effect's tracing layer.
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* ```typescript
|
|
50
|
-
* import { resolverTracingMiddleware } from "@effect-gql/opentelemetry"
|
|
51
|
-
*
|
|
52
|
-
* const builder = GraphQLSchemaBuilder.empty.pipe(
|
|
53
|
-
* middleware(resolverTracingMiddleware({
|
|
54
|
-
* minDepth: 0,
|
|
55
|
-
* excludePatterns: [/^Query\.__/],
|
|
56
|
-
* includeArgs: false
|
|
57
|
-
* })),
|
|
58
|
-
* query("users", { ... })
|
|
59
|
-
* )
|
|
60
|
-
* ```
|
|
61
|
-
*/
|
|
62
|
-
const resolverTracingMiddleware = (config) => ({
|
|
63
|
-
name: "opentelemetry-resolver-tracing",
|
|
64
|
-
description: "Wraps resolvers in OpenTelemetry spans",
|
|
65
|
-
match: (info) => shouldTraceField(info, config),
|
|
66
|
-
apply: (effect, context) => {
|
|
67
|
-
const { info } = context;
|
|
68
|
-
const spanName = config?.spanNameGenerator
|
|
69
|
-
? config.spanNameGenerator(info)
|
|
70
|
-
: `graphql.resolve ${info.parentType.name}.${info.fieldName}`;
|
|
71
|
-
return effect_1.Effect.withSpan(spanName)(effect_1.Effect.gen(function* () {
|
|
72
|
-
// Add standard attributes
|
|
73
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.field.name", info.fieldName);
|
|
74
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.field.path", (0, utils_1.pathToString)(info.path));
|
|
75
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.field.type", String(info.returnType));
|
|
76
|
-
if (config?.includeParentType !== false) {
|
|
77
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.parent.type", info.parentType.name);
|
|
78
|
-
}
|
|
79
|
-
if (info.operation?.name?.value) {
|
|
80
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.operation.name", info.operation.name.value);
|
|
81
|
-
}
|
|
82
|
-
if (config?.includeArgs && context.args && Object.keys(context.args).length > 0) {
|
|
83
|
-
yield* effect_1.Effect.annotateCurrentSpan("graphql.field.args", JSON.stringify(context.args));
|
|
84
|
-
}
|
|
85
|
-
// Execute resolver and handle errors
|
|
86
|
-
const result = yield* effect.pipe(effect_1.Effect.tapError((error) => effect_1.Effect.gen(function* () {
|
|
87
|
-
yield* effect_1.Effect.annotateCurrentSpan("error", true);
|
|
88
|
-
yield* effect_1.Effect.annotateCurrentSpan("error.type", error instanceof Error ? error.constructor.name : "Error");
|
|
89
|
-
yield* effect_1.Effect.annotateCurrentSpan("error.message", error instanceof Error ? error.message : String(error));
|
|
90
|
-
})));
|
|
91
|
-
return result;
|
|
92
|
-
}));
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
exports.resolverTracingMiddleware = resolverTracingMiddleware;
|
|
96
|
-
//# sourceMappingURL=tracing-middleware.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"tracing-middleware.js","sourceRoot":"","sources":["../src/tracing-middleware.ts"],"names":[],"mappings":"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"}
|
package/dist/utils.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { GraphQLResolveInfo, ResponsePath } from "graphql";
|
|
2
|
-
/**
|
|
3
|
-
* Convert a GraphQL response path to a string representation.
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* // For path: Query -> users -> 0 -> posts -> 1 -> title
|
|
7
|
-
* // Returns: "Query.users.0.posts.1.title"
|
|
8
|
-
*/
|
|
9
|
-
export declare const pathToString: (path: ResponsePath | undefined) => string;
|
|
10
|
-
/**
|
|
11
|
-
* Get the depth of a field in the query tree.
|
|
12
|
-
* Root fields (Query.*, Mutation.*) have depth 0.
|
|
13
|
-
*/
|
|
14
|
-
export declare const getFieldDepth: (info: GraphQLResolveInfo) => number;
|
|
15
|
-
/**
|
|
16
|
-
* Check if a field is an introspection field (__schema, __type, etc.)
|
|
17
|
-
*/
|
|
18
|
-
export declare const isIntrospectionField: (info: GraphQLResolveInfo) => boolean;
|
|
19
|
-
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,YAAY,GAAG,SAAS,KAAG,MAY7D,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,kBAAkB,KAAG,MAaxD,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,kBAAkB,KAAG,OAE/D,CAAA"}
|
package/dist/utils.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isIntrospectionField = exports.getFieldDepth = exports.pathToString = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* Convert a GraphQL response path to a string representation.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* // For path: Query -> users -> 0 -> posts -> 1 -> title
|
|
9
|
-
* // Returns: "Query.users.0.posts.1.title"
|
|
10
|
-
*/
|
|
11
|
-
const pathToString = (path) => {
|
|
12
|
-
if (!path)
|
|
13
|
-
return "";
|
|
14
|
-
const segments = [];
|
|
15
|
-
let current = path;
|
|
16
|
-
while (current) {
|
|
17
|
-
segments.unshift(current.key);
|
|
18
|
-
current = current.prev;
|
|
19
|
-
}
|
|
20
|
-
return segments.join(".");
|
|
21
|
-
};
|
|
22
|
-
exports.pathToString = pathToString;
|
|
23
|
-
/**
|
|
24
|
-
* Get the depth of a field in the query tree.
|
|
25
|
-
* Root fields (Query.*, Mutation.*) have depth 0.
|
|
26
|
-
*/
|
|
27
|
-
const getFieldDepth = (info) => {
|
|
28
|
-
let depth = 0;
|
|
29
|
-
let current = info.path;
|
|
30
|
-
while (current?.prev) {
|
|
31
|
-
// Skip array indices in depth calculation
|
|
32
|
-
if (typeof current.key === "string") {
|
|
33
|
-
depth++;
|
|
34
|
-
}
|
|
35
|
-
current = current.prev;
|
|
36
|
-
}
|
|
37
|
-
return depth;
|
|
38
|
-
};
|
|
39
|
-
exports.getFieldDepth = getFieldDepth;
|
|
40
|
-
/**
|
|
41
|
-
* Check if a field is an introspection field (__schema, __type, etc.)
|
|
42
|
-
*/
|
|
43
|
-
const isIntrospectionField = (info) => {
|
|
44
|
-
return info.fieldName.startsWith("__");
|
|
45
|
-
};
|
|
46
|
-
exports.isIntrospectionField = isIntrospectionField;
|
|
47
|
-
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":";;;AAEA;;;;;;GAMG;AACI,MAAM,YAAY,GAAG,CAAC,IAA8B,EAAU,EAAE;IACrE,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAA;IAEpB,MAAM,QAAQ,GAAwB,EAAE,CAAA;IACxC,IAAI,OAAO,GAA6B,IAAI,CAAA;IAE5C,OAAO,OAAO,EAAE,CAAC;QACf,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QAC7B,OAAO,GAAG,OAAO,CAAC,IAAI,CAAA;IACxB,CAAC;IAED,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC3B,CAAC,CAAA;AAZY,QAAA,YAAY,gBAYxB;AAED;;;GAGG;AACI,MAAM,aAAa,GAAG,CAAC,IAAwB,EAAU,EAAE;IAChE,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,IAAI,OAAO,GAA6B,IAAI,CAAC,IAAI,CAAA;IAEjD,OAAO,OAAO,EAAE,IAAI,EAAE,CAAC;QACrB,0CAA0C;QAC1C,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;YACpC,KAAK,EAAE,CAAA;QACT,CAAC;QACD,OAAO,GAAG,OAAO,CAAC,IAAI,CAAA;IACxB,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC,CAAA;AAbY,QAAA,aAAa,iBAazB;AAED;;GAEG;AACI,MAAM,oBAAoB,GAAG,CAAC,IAAwB,EAAW,EAAE;IACxE,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,CAAA;AACxC,CAAC,CAAA;AAFY,QAAA,oBAAoB,wBAEhC"}
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { Effect, Context } from "effect"
|
|
2
|
-
import { HttpServerRequest } from "@effect/platform"
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* W3C Trace Context header names
|
|
6
|
-
* @see https://www.w3.org/TR/trace-context/
|
|
7
|
-
*/
|
|
8
|
-
export const TRACEPARENT_HEADER = "traceparent"
|
|
9
|
-
export const TRACESTATE_HEADER = "tracestate"
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Parsed W3C Trace Context from incoming HTTP headers
|
|
13
|
-
*/
|
|
14
|
-
export interface TraceContext {
|
|
15
|
-
/**
|
|
16
|
-
* Version of the trace context format (always "00" for current spec)
|
|
17
|
-
*/
|
|
18
|
-
readonly version: string
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Trace ID - 32 character lowercase hex string
|
|
22
|
-
*/
|
|
23
|
-
readonly traceId: string
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Parent Span ID - 16 character lowercase hex string
|
|
27
|
-
*/
|
|
28
|
-
readonly parentSpanId: string
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Trace flags - determines if trace is sampled
|
|
32
|
-
* Bit 0 (0x01) = sampled flag
|
|
33
|
-
*/
|
|
34
|
-
readonly traceFlags: number
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Optional trace state from upstream services
|
|
38
|
-
*/
|
|
39
|
-
readonly traceState?: string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Context tag for TraceContext
|
|
44
|
-
*/
|
|
45
|
-
export class TraceContextTag extends Context.Tag("@effect-gql/opentelemetry/TraceContext")<
|
|
46
|
-
TraceContextTag,
|
|
47
|
-
TraceContext
|
|
48
|
-
>() {}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Parse a W3C traceparent header value.
|
|
52
|
-
*
|
|
53
|
-
* Format: {version}-{trace-id}-{parent-id}-{trace-flags}
|
|
54
|
-
* Example: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
|
|
55
|
-
*
|
|
56
|
-
* @see https://www.w3.org/TR/trace-context/#traceparent-header
|
|
57
|
-
*
|
|
58
|
-
* @param header - The traceparent header value
|
|
59
|
-
* @returns Parsed trace context or null if invalid
|
|
60
|
-
*/
|
|
61
|
-
export const parseTraceParent = (header: string): TraceContext | null => {
|
|
62
|
-
const trimmed = header.trim().toLowerCase()
|
|
63
|
-
const parts = trimmed.split("-")
|
|
64
|
-
|
|
65
|
-
if (parts.length !== 4) {
|
|
66
|
-
return null
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const [version, traceId, parentSpanId, flagsHex] = parts
|
|
70
|
-
|
|
71
|
-
// Validate version (2 hex chars)
|
|
72
|
-
if (version.length !== 2 || !/^[0-9a-f]{2}$/.test(version)) {
|
|
73
|
-
return null
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Validate trace ID (32 hex chars, not all zeros)
|
|
77
|
-
if (traceId.length !== 32 || !/^[0-9a-f]{32}$/.test(traceId)) {
|
|
78
|
-
return null
|
|
79
|
-
}
|
|
80
|
-
if (traceId === "00000000000000000000000000000000") {
|
|
81
|
-
return null
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Validate parent span ID (16 hex chars, not all zeros)
|
|
85
|
-
if (parentSpanId.length !== 16 || !/^[0-9a-f]{16}$/.test(parentSpanId)) {
|
|
86
|
-
return null
|
|
87
|
-
}
|
|
88
|
-
if (parentSpanId === "0000000000000000") {
|
|
89
|
-
return null
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Validate trace flags (2 hex chars)
|
|
93
|
-
if (flagsHex.length !== 2 || !/^[0-9a-f]{2}$/.test(flagsHex)) {
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const traceFlags = parseInt(flagsHex, 16)
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
version,
|
|
101
|
-
traceId,
|
|
102
|
-
parentSpanId,
|
|
103
|
-
traceFlags,
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Check if a trace context is sampled (should be recorded)
|
|
109
|
-
*/
|
|
110
|
-
export const isSampled = (context: TraceContext): boolean => {
|
|
111
|
-
return (context.traceFlags & 0x01) === 0x01
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Extract trace context from HTTP request headers.
|
|
116
|
-
*
|
|
117
|
-
* Looks for the W3C Trace Context headers:
|
|
118
|
-
* - `traceparent`: Required, contains trace ID, span ID, and flags
|
|
119
|
-
* - `tracestate`: Optional, vendor-specific trace data
|
|
120
|
-
*
|
|
121
|
-
* @example
|
|
122
|
-
* ```typescript
|
|
123
|
-
* const context = yield* extractTraceContext
|
|
124
|
-
* if (context) {
|
|
125
|
-
* console.log(`Continuing trace: ${context.traceId}`)
|
|
126
|
-
* }
|
|
127
|
-
* ```
|
|
128
|
-
*/
|
|
129
|
-
export const extractTraceContext: Effect.Effect<
|
|
130
|
-
TraceContext | null,
|
|
131
|
-
never,
|
|
132
|
-
HttpServerRequest.HttpServerRequest
|
|
133
|
-
> = Effect.gen(function* () {
|
|
134
|
-
const request = yield* HttpServerRequest.HttpServerRequest
|
|
135
|
-
const headers = request.headers
|
|
136
|
-
|
|
137
|
-
// Get traceparent header (case-insensitive)
|
|
138
|
-
const traceparentKey = Object.keys(headers).find((k) => k.toLowerCase() === TRACEPARENT_HEADER)
|
|
139
|
-
|
|
140
|
-
if (!traceparentKey) {
|
|
141
|
-
return null
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const traceparentValue = headers[traceparentKey]
|
|
145
|
-
const traceparent = Array.isArray(traceparentValue) ? traceparentValue[0] : traceparentValue
|
|
146
|
-
|
|
147
|
-
if (!traceparent) {
|
|
148
|
-
return null
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const context = parseTraceParent(traceparent)
|
|
152
|
-
if (!context) {
|
|
153
|
-
return null
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Get optional tracestate header
|
|
157
|
-
const tracestateKey = Object.keys(headers).find((k) => k.toLowerCase() === TRACESTATE_HEADER)
|
|
158
|
-
|
|
159
|
-
if (tracestateKey) {
|
|
160
|
-
const tracestateValue = headers[tracestateKey]
|
|
161
|
-
const tracestate = Array.isArray(tracestateValue) ? tracestateValue[0] : tracestateValue
|
|
162
|
-
|
|
163
|
-
if (tracestate) {
|
|
164
|
-
return { ...context, traceState: tracestate }
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return context
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Format a trace context as a traceparent header value
|
|
173
|
-
*/
|
|
174
|
-
export const formatTraceParent = (context: TraceContext): string => {
|
|
175
|
-
const flags = context.traceFlags.toString(16).padStart(2, "0")
|
|
176
|
-
return `${context.version}-${context.traceId}-${context.parentSpanId}-${flags}`
|
|
177
|
-
}
|