|
17 | 17 | import assert from 'assert'; |
18 | 18 | import {PassThrough} from 'stream'; |
19 | 19 | import {status} from '@grpc/grpc-js'; |
20 | | -import {SpanKind, SpanStatusCode} from '@opentelemetry/api'; |
| 20 | +import { |
| 21 | + Context, |
| 22 | + ContextManager, |
| 23 | + ROOT_CONTEXT, |
| 24 | + Span, |
| 25 | + SpanKind, |
| 26 | + SpanStatusCode, |
| 27 | + context, |
| 28 | + trace, |
| 29 | +} from '@opentelemetry/api'; |
21 | 30 | import {GoogleAuth, PassThroughClient} from 'google-auth-library'; |
22 | 31 | import * as loggingUtils from 'google-logging-utils'; |
23 | 32 | import {afterEach, beforeEach, describe, it} from 'mocha'; |
@@ -402,6 +411,7 @@ describe('createApiCall', () => { |
402 | 411 | clientName: 'EchoClient', |
403 | 412 | methodName: 'Echo', |
404 | 413 | rpcType: 'grpc', |
| 414 | + apiName: 'google.example.v1.Echo', |
405 | 415 | }); |
406 | 416 | assert.deepStrictEqual(staticArgs, { |
407 | 417 | gcpClientService: 'example', |
@@ -468,6 +478,7 @@ describe('createApiCall', () => { |
468 | 478 | clientName: '', |
469 | 479 | methodName: '', |
470 | 480 | rpcType: 'grpc', |
| 481 | + apiName: '', |
471 | 482 | }); |
472 | 483 | }); |
473 | 484 |
|
@@ -2130,6 +2141,206 @@ describe('createApiCall', () => { |
2130 | 2141 | assert.ok(spans.find(s => s.name === 'google.example.v1.Echo/Echo')); |
2131 | 2142 | assert.ok(spans.find(s => s.name === 'EchoClient.Echo')); |
2132 | 2143 | }); |
| 2144 | + |
| 2145 | + it('preserves outer OpenTelemetry span correlation across funcPromise and retries in logging-only mode without async context propagation', async () => { |
| 2146 | + // Replace OtelHarness's AsyncLocalStorageContextManager with a synchronous |
| 2147 | + // stack-only ContextManager so context.active() resets to ROOT_CONTEXT across |
| 2148 | + // funcPromise.then(...) microtasks and retry timers. |
| 2149 | + harness.teardown(); |
| 2150 | + class StackOnlyContextManager implements ContextManager { |
| 2151 | + private stack: Context[] = []; |
| 2152 | + active(): Context { |
| 2153 | + return this.stack[this.stack.length - 1] ?? ROOT_CONTEXT; |
| 2154 | + } |
| 2155 | + with<A extends unknown[], F extends (...args: A) => ReturnType<F>>( |
| 2156 | + ctx: Context, |
| 2157 | + fn: F, |
| 2158 | + thisArg?: ThisParameterType<F>, |
| 2159 | + ...args: A |
| 2160 | + ): ReturnType<F> { |
| 2161 | + this.stack.push(ctx); |
| 2162 | + try { |
| 2163 | + return fn.apply(thisArg, args); |
| 2164 | + } finally { |
| 2165 | + this.stack.pop(); |
| 2166 | + } |
| 2167 | + } |
| 2168 | + bind<T>(_ctx: Context, target: T): T { |
| 2169 | + return target; |
| 2170 | + } |
| 2171 | + enable(): this { |
| 2172 | + return this; |
| 2173 | + } |
| 2174 | + disable(): this { |
| 2175 | + this.stack = []; |
| 2176 | + return this; |
| 2177 | + } |
| 2178 | + } |
| 2179 | + context.setGlobalContextManager(new StackOnlyContextManager().enable()); |
| 2180 | + trace.setGlobalTracerProvider(harness.provider); |
| 2181 | + |
| 2182 | + let attempts = 0; |
| 2183 | + function failingRetriedFunc( |
| 2184 | + argument: {}, |
| 2185 | + metadata: {}, |
| 2186 | + options: {}, |
| 2187 | + callback: (err: GoogleError | null, resp?: unknown) => void, |
| 2188 | + ) { |
| 2189 | + attempts++; |
| 2190 | + setTimeout(() => { |
| 2191 | + const error = new GoogleError( |
| 2192 | + attempts === 1 ? 'retryable failure' : 'terminal failure', |
| 2193 | + ); |
| 2194 | + error.code = attempts === 1 ? FAKE_STATUS_CODE_1 : FAKE_STATUS_CODE_2; |
| 2195 | + callback(error); |
| 2196 | + }, 0); |
| 2197 | + return {cancel: () => {}}; |
| 2198 | + } |
| 2199 | + |
| 2200 | + const settings = new gax.CallSettings({ |
| 2201 | + apiName: 'google.example.v1.Echo', |
| 2202 | + enableTelemetryTracing: false, |
| 2203 | + enableTelemetryLogging: true, |
| 2204 | + timeout: 0, |
| 2205 | + retry: utils.createRetryOptions(1, 1, 1, 0, 0, 0, 500), |
| 2206 | + otherArgs: { |
| 2207 | + internalMethodName: 'Echo', |
| 2208 | + }, |
| 2209 | + }); |
| 2210 | + |
| 2211 | + const apiCall = gaxCreateApiCall( |
| 2212 | + Promise.resolve(failingRetriedFunc), |
| 2213 | + settings, |
| 2214 | + ); |
| 2215 | + |
| 2216 | + let outerSpan: Span | undefined; |
| 2217 | + const callPromise = trace |
| 2218 | + .getTracer('external-app') |
| 2219 | + .startActiveSpan('outer-request', span => { |
| 2220 | + outerSpan = span; |
| 2221 | + const p = apiCall({}, undefined); |
| 2222 | + span.end(); |
| 2223 | + return p; |
| 2224 | + }); |
| 2225 | + |
| 2226 | + await assert.rejects(async () => { |
| 2227 | + await callPromise; |
| 2228 | + }); |
| 2229 | + |
| 2230 | + assert.ok(outerSpan); |
| 2231 | + assert.strictEqual(attempts, 2); |
| 2232 | + // 2 L4 DEBUG attempt logs + 1 L3 WARNING call log, all correlated to outerSpan. |
| 2233 | + assert.strictEqual(sink.logs.length, 3); |
| 2234 | + assert.deepStrictEqual( |
| 2235 | + sink.logs.map(l => l.fields.severity), |
| 2236 | + ['DEBUG', 'DEBUG', 'WARNING'], |
| 2237 | + ); |
| 2238 | + for (const logEntry of sink.logs) { |
| 2239 | + assert.strictEqual( |
| 2240 | + logEntry.fields.telemetryTraceId, |
| 2241 | + outerSpan.spanContext().traceId, |
| 2242 | + ); |
| 2243 | + assert.strictEqual( |
| 2244 | + logEntry.fields.telemetrySpanId, |
| 2245 | + outerSpan.spanContext().spanId, |
| 2246 | + ); |
| 2247 | + } |
| 2248 | + }); |
| 2249 | + |
| 2250 | + it('emits one L4 DEBUG log and one L3 WARNING log when a streaming API call emits an error', async () => { |
| 2251 | + const settings = new gax.CallSettings({ |
| 2252 | + apiName: 'google.example.v1.Echo', |
| 2253 | + enableTelemetryTracing: true, |
| 2254 | + enableTelemetryLogging: true, |
| 2255 | + otherArgs: { |
| 2256 | + internalMethodName: 'Echo', |
| 2257 | + }, |
| 2258 | + }); |
| 2259 | + |
| 2260 | + const streamError = Object.assign( |
| 2261 | + new GoogleError('streaming test failure'), |
| 2262 | + { |
| 2263 | + code: status.UNAVAILABLE, |
| 2264 | + }, |
| 2265 | + ); |
| 2266 | + const spy = sinon.spy(() => { |
| 2267 | + const s = new PassThrough({objectMode: true}); |
| 2268 | + setImmediate(() => { |
| 2269 | + s.emit('error', streamError); |
| 2270 | + }); |
| 2271 | + return Object.assign(s, {cancel: () => {}}); |
| 2272 | + }); |
| 2273 | + |
| 2274 | + const apiCall = gaxCreateApiCall( |
| 2275 | + spy as unknown as GRPCCall, |
| 2276 | + settings, |
| 2277 | + new StreamDescriptor(StreamType.SERVER_STREAMING, true), |
| 2278 | + ); |
| 2279 | + const stream = apiCall({}, undefined) as CancellableStream; |
| 2280 | + assert.strictEqual(sink.logs.length, 0); |
| 2281 | + |
| 2282 | + const caught = await new Promise<GoogleError>(resolve => { |
| 2283 | + stream.on('error', (err: GoogleError) => resolve(err)); |
| 2284 | + }); |
| 2285 | + // Allow StreamProxy / downstream 'error' listeners installed after caller listener (if any) to settle. |
| 2286 | + await new Promise<void>(resolve => setImmediate(resolve)); |
| 2287 | + |
| 2288 | + assert.strictEqual(caught, streamError); |
| 2289 | + assert.strictEqual(sink.logs.length, 2); |
| 2290 | + |
| 2291 | + const debugLogs = sink.logs.filter(l => l.fields.severity === 'DEBUG'); |
| 2292 | + const warnLogs = sink.logs.filter(l => l.fields.severity === 'WARNING'); |
| 2293 | + assert.strictEqual(debugLogs.length, 1); |
| 2294 | + assert.strictEqual(warnLogs.length, 1); |
| 2295 | + |
| 2296 | + const attemptLog = debugLogs[0]; |
| 2297 | + const callLog = warnLogs[0]; |
| 2298 | + assert.deepStrictEqual(attemptLog.args, [ |
| 2299 | + 'RPC attempt failed: %s', |
| 2300 | + 'streaming test failure', |
| 2301 | + ]); |
| 2302 | + assert.deepStrictEqual(callLog.args, [ |
| 2303 | + 'Client API call failed: %s', |
| 2304 | + 'streaming test failure', |
| 2305 | + ]); |
| 2306 | + |
| 2307 | + const attemptOther = attemptLog.fields.other as loggerHelper.LogPayload; |
| 2308 | + const callOther = callLog.fields.other as loggerHelper.LogPayload; |
| 2309 | + assert.strictEqual( |
| 2310 | + attemptOther['rpc.method'], |
| 2311 | + 'google.example.v1.Echo/Echo', |
| 2312 | + ); |
| 2313 | + assert.strictEqual( |
| 2314 | + attemptOther['rpc.response.status_code'], |
| 2315 | + 'UNAVAILABLE', |
| 2316 | + ); |
| 2317 | + assert.strictEqual('exception.type' in attemptOther, false); |
| 2318 | + assert.strictEqual( |
| 2319 | + callOther['rpc.method'], |
| 2320 | + 'google.example.v1.Echo/Echo', |
| 2321 | + ); |
| 2322 | + assert.strictEqual(callOther['rpc.response.status_code'], 'UNAVAILABLE'); |
| 2323 | + assert.strictEqual(callOther['exception.type'], 'GoogleError'); |
| 2324 | + assert.strictEqual( |
| 2325 | + callOther['exception.message'], |
| 2326 | + 'streaming test failure', |
| 2327 | + ); |
| 2328 | + |
| 2329 | + const spans = harness.getSpans('google-gax'); |
| 2330 | + assert.strictEqual(spans.length, 2); |
| 2331 | + const attemptSpan = spans.find( |
| 2332 | + s => s.name === 'google.example.v1.Echo/Echo', |
| 2333 | + )!; |
| 2334 | + const clientRequestSpan = spans.find(s => s.name === 'EchoClient.Echo')!; |
| 2335 | + assert.strictEqual( |
| 2336 | + attemptLog.fields.telemetrySpanId, |
| 2337 | + attemptSpan.spanContext().spanId, |
| 2338 | + ); |
| 2339 | + assert.strictEqual( |
| 2340 | + callLog.fields.telemetrySpanId, |
| 2341 | + clientRequestSpan.spanContext().spanId, |
| 2342 | + ); |
| 2343 | + }); |
2133 | 2344 | }); |
2134 | 2345 | }); |
2135 | 2346 |
|
|
0 commit comments