@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.
@@ -1,177 +0,0 @@
1
- import { Effect } from "effect"
2
- import type { GraphQLResolveInfo } from "graphql"
3
- import type { MiddlewareRegistration, MiddlewareContext } from "@effect-gql/core"
4
- import { pathToString, getFieldDepth, isIntrospectionField } from "./utils"
5
-
6
- /**
7
- * Configuration for resolver tracing middleware
8
- */
9
- export interface ResolverTracingConfig {
10
- /**
11
- * Minimum field depth to trace.
12
- * Depth 0 = root fields (Query.*, Mutation.*).
13
- * Default: 0 (trace all fields)
14
- */
15
- readonly minDepth?: number
16
-
17
- /**
18
- * Maximum field depth to trace.
19
- * Default: Infinity (no limit)
20
- */
21
- readonly maxDepth?: number
22
-
23
- /**
24
- * Field patterns to exclude from tracing.
25
- * Patterns are matched against "TypeName.fieldName".
26
- *
27
- * @example
28
- * // Exclude introspection and internal fields
29
- * excludePatterns: [/^Query\.__/, /\.id$/]
30
- */
31
- readonly excludePatterns?: readonly RegExp[]
32
-
33
- /**
34
- * Whether to include field arguments in span attributes.
35
- * Default: false (for security - args may contain sensitive data)
36
- */
37
- readonly includeArgs?: boolean
38
-
39
- /**
40
- * Whether to include parent type in span attributes.
41
- * Default: true
42
- */
43
- readonly includeParentType?: boolean
44
-
45
- /**
46
- * Whether to trace introspection fields (__schema, __type, etc.).
47
- * Default: false
48
- */
49
- readonly traceIntrospection?: boolean
50
-
51
- /**
52
- * Custom span name generator.
53
- * Default: "graphql.resolve TypeName.fieldName"
54
- */
55
- readonly spanNameGenerator?: (info: GraphQLResolveInfo) => string
56
- }
57
-
58
- /**
59
- * Check if a field should be traced based on configuration
60
- */
61
- const shouldTraceField = (info: GraphQLResolveInfo, config?: ResolverTracingConfig): boolean => {
62
- // Skip introspection fields unless explicitly enabled
63
- if (!config?.traceIntrospection && isIntrospectionField(info)) {
64
- return false
65
- }
66
-
67
- const depth = getFieldDepth(info)
68
-
69
- // Check depth bounds
70
- if (config?.minDepth !== undefined && depth < config.minDepth) {
71
- return false
72
- }
73
- if (config?.maxDepth !== undefined && depth > config.maxDepth) {
74
- return false
75
- }
76
-
77
- // Check exclude patterns
78
- if (config?.excludePatterns) {
79
- const fieldPath = `${info.parentType.name}.${info.fieldName}`
80
- for (const pattern of config.excludePatterns) {
81
- if (pattern.test(fieldPath)) {
82
- return false
83
- }
84
- }
85
- }
86
-
87
- return true
88
- }
89
-
90
- /**
91
- * Creates middleware that wraps each resolver in an OpenTelemetry span.
92
- *
93
- * Each resolver execution creates a child span with GraphQL-specific attributes:
94
- * - `graphql.field.name`: The field being resolved
95
- * - `graphql.field.path`: Full path to the field (e.g., "Query.users.0.posts")
96
- * - `graphql.field.type`: The return type of the field
97
- * - `graphql.parent.type`: The parent type name
98
- * - `graphql.operation.name`: The operation name (if available)
99
- * - `error`: Set to true if the resolver fails
100
- * - `error.type`: Error type/class name
101
- * - `error.message`: Error message
102
- *
103
- * Requires an OpenTelemetry tracer to be provided via Effect's tracing layer.
104
- *
105
- * @example
106
- * ```typescript
107
- * import { resolverTracingMiddleware } from "@effect-gql/opentelemetry"
108
- *
109
- * const builder = GraphQLSchemaBuilder.empty.pipe(
110
- * middleware(resolverTracingMiddleware({
111
- * minDepth: 0,
112
- * excludePatterns: [/^Query\.__/],
113
- * includeArgs: false
114
- * })),
115
- * query("users", { ... })
116
- * )
117
- * ```
118
- */
119
- export const resolverTracingMiddleware = (
120
- config?: ResolverTracingConfig
121
- ): MiddlewareRegistration<never> => ({
122
- name: "opentelemetry-resolver-tracing",
123
- description: "Wraps resolvers in OpenTelemetry spans",
124
-
125
- match: (info: GraphQLResolveInfo) => shouldTraceField(info, config),
126
-
127
- apply: <A, E, R>(
128
- effect: Effect.Effect<A, E, R>,
129
- context: MiddlewareContext
130
- ): Effect.Effect<A, E, R> => {
131
- const { info } = context
132
-
133
- const spanName = config?.spanNameGenerator
134
- ? config.spanNameGenerator(info)
135
- : `graphql.resolve ${info.parentType.name}.${info.fieldName}`
136
-
137
- return Effect.withSpan(spanName)(
138
- Effect.gen(function* () {
139
- // Add standard attributes
140
- yield* Effect.annotateCurrentSpan("graphql.field.name", info.fieldName)
141
- yield* Effect.annotateCurrentSpan("graphql.field.path", pathToString(info.path))
142
- yield* Effect.annotateCurrentSpan("graphql.field.type", String(info.returnType))
143
-
144
- if (config?.includeParentType !== false) {
145
- yield* Effect.annotateCurrentSpan("graphql.parent.type", info.parentType.name)
146
- }
147
-
148
- if (info.operation?.name?.value) {
149
- yield* Effect.annotateCurrentSpan("graphql.operation.name", info.operation.name.value)
150
- }
151
-
152
- if (config?.includeArgs && context.args && Object.keys(context.args).length > 0) {
153
- yield* Effect.annotateCurrentSpan("graphql.field.args", JSON.stringify(context.args))
154
- }
155
-
156
- // Execute resolver and handle errors
157
- const result = yield* effect.pipe(
158
- Effect.tapError((error) =>
159
- Effect.gen(function* () {
160
- yield* Effect.annotateCurrentSpan("error", true)
161
- yield* Effect.annotateCurrentSpan(
162
- "error.type",
163
- error instanceof Error ? error.constructor.name : "Error"
164
- )
165
- yield* Effect.annotateCurrentSpan(
166
- "error.message",
167
- error instanceof Error ? error.message : String(error)
168
- )
169
- })
170
- )
171
- )
172
-
173
- return result
174
- })
175
- ) as Effect.Effect<A, E, R>
176
- },
177
- })
package/src/utils.ts DELETED
@@ -1,48 +0,0 @@
1
- import type { GraphQLResolveInfo, ResponsePath } from "graphql"
2
-
3
- /**
4
- * Convert a GraphQL response path to a string representation.
5
- *
6
- * @example
7
- * // For path: Query -> users -> 0 -> posts -> 1 -> title
8
- * // Returns: "Query.users.0.posts.1.title"
9
- */
10
- export const pathToString = (path: ResponsePath | undefined): string => {
11
- if (!path) return ""
12
-
13
- const segments: (string | number)[] = []
14
- let current: ResponsePath | undefined = path
15
-
16
- while (current) {
17
- segments.unshift(current.key)
18
- current = current.prev
19
- }
20
-
21
- return segments.join(".")
22
- }
23
-
24
- /**
25
- * Get the depth of a field in the query tree.
26
- * Root fields (Query.*, Mutation.*) have depth 0.
27
- */
28
- export const getFieldDepth = (info: GraphQLResolveInfo): number => {
29
- let depth = 0
30
- let current: ResponsePath | undefined = info.path
31
-
32
- while (current?.prev) {
33
- // Skip array indices in depth calculation
34
- if (typeof current.key === "string") {
35
- depth++
36
- }
37
- current = current.prev
38
- }
39
-
40
- return depth
41
- }
42
-
43
- /**
44
- * Check if a field is an introspection field (__schema, __type, etc.)
45
- */
46
- export const isIntrospectionField = (info: GraphQLResolveInfo): boolean => {
47
- return info.fieldName.startsWith("__")
48
- }