@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,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
|
-
}
|