Skip to content

Commit 6b8adfc

Browse files
feat: export single envvar span during pre app start (#77)
* export single pre app start envvar span * remove old env var code, update schemas
1 parent 27c1f55 commit 6b8adfc

20 files changed

+91
-540
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"@types/supertest": "^6.0.2",
6868
"@typescript-eslint/eslint-plugin": "^6.0.0",
6969
"@typescript-eslint/parser": "^6.0.0",
70-
"@use-tusk/drift-schemas": "^0.1.18",
70+
"@use-tusk/drift-schemas": "^0.1.19",
7171
"ava": "^6.4.1",
7272
"axios": "^1.6.0",
7373
"eslint": "^8.57.1",

src/core/ProtobufCommunicator.ts

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
SendAlertRequest,
1616
InstrumentationVersionMismatchAlert,
1717
UnpatchedDependencyAlert,
18-
EnvVarRequest,
1918
} from "@use-tusk/drift-schemas/core/communication";
2019
import { context, Context, SpanKind as OtSpanKind } from "@opentelemetry/api";
2120
import { Value } from "@use-tusk/drift-schemas/google/protobuf/struct";
@@ -229,17 +228,6 @@ try {
229228
// If CLI rejects -> CLI closes the connection and terminates the service.
230229
}
231230

232-
private getStackTrace(): string {
233-
Error.stackTraceLimit = 100;
234-
const s = new Error().stack || "";
235-
Error.stackTraceLimit = 10;
236-
return s
237-
.split("\n")
238-
.slice(2)
239-
.filter((l) => !l.includes("ProtobufCommunicator"))
240-
.join("\n");
241-
}
242-
243231
async requestMockAsync(mockRequest: MockRequestInput): Promise<MockResponseOutput> {
244232
const requestId = this.generateRequestId();
245233

@@ -288,7 +276,7 @@ try {
288276
/**
289277
* Generic synchronous request handler that spawns a child process.
290278
* @param sdkMessage The SDK message to send
291-
* @param filePrefix Prefix for temporary files (e.g., 'envvar', 'mock')
279+
* @param filePrefix Prefix for temporary files (e.g., 'mock')
292280
* @param responseHandler Function to extract and return the desired response
293281
*/
294282
private executeSyncRequest<TResponse>(
@@ -389,56 +377,6 @@ try {
389377
}
390378
}
391379

392-
/**
393-
* Request environment variables from CLI synchronously using a child process.
394-
* This blocks the main thread, so it should be used carefully.
395-
* Similar to requestMockSync but for environment variables.
396-
*/
397-
requestEnvVarsSync(traceTestServerSpanId: string): Record<string, string> {
398-
const requestId = this.generateRequestId();
399-
400-
const envVarRequest = EnvVarRequest.create({
401-
traceTestServerSpanId,
402-
});
403-
404-
const sdkMessage = SDKMessage.create({
405-
type: MessageType.ENV_VAR_REQUEST,
406-
requestId: requestId,
407-
payload: {
408-
oneofKind: "envVarRequest",
409-
envVarRequest,
410-
},
411-
});
412-
413-
logger.debug(
414-
`[ProtobufCommunicator] Requesting env vars (sync) for trace: ${traceTestServerSpanId}`,
415-
);
416-
417-
return this.executeSyncRequest(sdkMessage, "envvar", (cliMessage) => {
418-
if (cliMessage.payload.oneofKind !== "envVarResponse") {
419-
throw new Error(`Unexpected response type: ${cliMessage.type}`);
420-
}
421-
422-
const envVarResponse = cliMessage.payload.envVarResponse;
423-
if (!envVarResponse) {
424-
throw new Error("No env var response received");
425-
}
426-
427-
// Convert protobuf map to Record<string, string>
428-
const envVars: Record<string, string> = {};
429-
if (envVarResponse.envVars) {
430-
Object.entries(envVarResponse.envVars).forEach(([key, value]) => {
431-
envVars[key] = value;
432-
});
433-
}
434-
435-
logger.debug(
436-
`[ProtobufCommunicator] Received env vars (sync), count: ${Object.keys(envVars).length}`,
437-
);
438-
return envVars;
439-
});
440-
}
441-
442380
/**
443381
* This function uses a separate Node.js child process to communicate with the CLI over a socket.
444382
* The child process creates its own connection and event loop, allowing proper async socket handling.
@@ -737,33 +675,6 @@ try {
737675
});
738676
}
739677
}
740-
741-
if (message.payload.oneofKind === "envVarResponse") {
742-
const envVarResponse = message.payload.envVarResponse;
743-
logger.debug(`[ProtobufCommunicator] Received env var response for requestId: ${requestId}`);
744-
const pendingRequest = this.pendingRequests.get(requestId);
745-
746-
if (!pendingRequest) {
747-
logger.warn(
748-
"[ProtobufCommunicator] received env var response for unknown request:",
749-
requestId,
750-
);
751-
return;
752-
}
753-
754-
this.pendingRequests.delete(requestId);
755-
756-
// Convert protobuf map to Record<string, string>
757-
const envVars: Record<string, string> = {};
758-
if (envVarResponse?.envVars) {
759-
Object.entries(envVarResponse.envVars).forEach(([key, value]) => {
760-
envVars[key] = value;
761-
});
762-
}
763-
764-
pendingRequest.resolve(envVars);
765-
return;
766-
}
767678
}
768679

769680
/**

src/core/TuskDrift.ts

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
JsonwebtokenInstrumentation,
1212
DateInstrumentation,
1313
JwksRsaInstrumentation,
14-
EnvInstrumentation,
1514
PostgresInstrumentation,
1615
Mysql2Instrumentation,
1716
IORedisInstrumentation,
@@ -23,13 +22,15 @@ import {
2322
MysqlInstrumentation,
2423
} from "../instrumentation/libraries";
2524
import { TdSpanExporter } from "./tracing/TdSpanExporter";
26-
import { trace, Tracer } from "@opentelemetry/api";
25+
import { trace, Tracer, SpanKind, SpanStatusCode } from "@opentelemetry/api";
2726
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
2827
import { ProtobufCommunicator, MockRequestInput } from "./ProtobufCommunicator";
2928
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-node";
3029
import { CleanSpanData, TD_INSTRUMENTATION_LIBRARY_NAME } from "./types";
3130
import { TuskDriftInstrumentationModuleNames } from "./TuskDriftInstrumentationModuleNames";
3231
import { SDK_VERSION } from "../version";
32+
import { SpanUtils } from "./tracing/SpanUtils";
33+
import { PackageType } from "@use-tusk/drift-schemas/core/span";
3334
import {
3435
LogLevel,
3536
initializeGlobalLogger,
@@ -254,11 +255,6 @@ export class TuskDriftCore {
254255
mode: this.mode,
255256
});
256257

257-
new EnvInstrumentation({
258-
enabled: this.config.recording?.enable_env_var_recording || false,
259-
mode: this.mode,
260-
});
261-
262258
new PostgresInstrumentation({
263259
enabled: true,
264260
mode: this.mode,
@@ -317,7 +313,7 @@ export class TuskDriftCore {
317313
observableServiceId: this.config.service?.id,
318314
apiKey: this.initParams.apiKey,
319315
tuskBackendBaseUrl: this.config.tusk_api?.url || "https://api.usetusk.ai",
320-
environment: this.initParams.env || "unknown",
316+
environment: this.initParams.env,
321317
sdkVersion: SDK_VERSION,
322318
sdkInstanceId: this.generateSdkInstanceId(),
323319
});
@@ -350,6 +346,55 @@ export class TuskDriftCore {
350346
return `sdk-${originalDate.getTime()}-${Math.random().toString(36).substr(2, 9)}`;
351347
}
352348

349+
/**
350+
* Creates a pre-app-start span containing a snapshot of all environment variables.
351+
* Only runs in RECORD mode when env var recording is enabled.
352+
*/
353+
private createEnvVarsSnapshot(): void {
354+
// Only create snapshot in RECORD mode and if env var recording is enabled
355+
if (this.mode !== TuskDriftMode.RECORD || !this.config.recording?.enable_env_var_recording) {
356+
return;
357+
}
358+
359+
try {
360+
// Capture all env vars from process.env
361+
const envVarsSnapshot: Record<string, string | undefined> = {};
362+
for (const key of Object.keys(process.env)) {
363+
envVarsSnapshot[key] = process.env[key];
364+
}
365+
366+
logger.debug(
367+
`Creating env vars snapshot with ${Object.keys(envVarsSnapshot).length} variables`,
368+
);
369+
370+
// Create a span to hold the env vars snapshot
371+
SpanUtils.createAndExecuteSpan(
372+
this.mode,
373+
() => {}, // No-op function since this is just a metadata snapshot
374+
{
375+
name: "ENV_VARS_SNAPSHOT",
376+
kind: SpanKind.INTERNAL,
377+
packageName: "process.env",
378+
packageType: PackageType.UNSPECIFIED,
379+
instrumentationName: "TuskDriftCore",
380+
submodule: "env",
381+
inputValue: {},
382+
outputValue: {
383+
ENV_VARS: envVarsSnapshot,
384+
},
385+
isPreAppStart: true,
386+
},
387+
(spanInfo) => {
388+
// Span is created with metadata, just end it immediately
389+
SpanUtils.endSpan(spanInfo.span, { code: SpanStatusCode.OK });
390+
logger.debug(`Env vars snapshot span created: ${spanInfo.spanId}`);
391+
},
392+
);
393+
} catch (error) {
394+
logger.error("Failed to create env vars snapshot:", error);
395+
}
396+
}
397+
353398
initialize(initParams: InitParams): void {
354399
// Initialize logging with provided level or default to 'info'
355400
initializeGlobalLogger({
@@ -487,6 +532,9 @@ export class TuskDriftCore {
487532
// which imports the gRPC exporter
488533
this.initializeTracing({ baseDirectory });
489534

535+
// Create env vars snapshot span (only in RECORD mode with env var recording enabled)
536+
this.createEnvVarsSnapshot();
537+
490538
this.initialized = true;
491539
logger.info("SDK initialized successfully");
492540
}
@@ -576,35 +624,6 @@ export class TuskDriftCore {
576624
}
577625
}
578626

579-
/**
580-
* Request environment variables from CLI for a specific trace (synchronously).
581-
* This blocks the main thread, so it should be used carefully.
582-
*/
583-
requestEnvVarsSync(traceTestServerSpanId: string): Record<string, string> {
584-
if (!this.isConnectedWithCLI) {
585-
logger.error("Requesting sync env vars but CLI is not ready yet");
586-
throw new Error("Requesting sync env vars but CLI is not ready yet");
587-
}
588-
589-
if (!this.communicator || this.mode !== TuskDriftMode.REPLAY) {
590-
logger.debug("Cannot request env vars: not in replay mode or no CLI connection");
591-
return {};
592-
}
593-
594-
try {
595-
logger.debug(`Requesting env vars (sync) for trace: ${traceTestServerSpanId}`);
596-
const envVars = this.communicator.requestEnvVarsSync(traceTestServerSpanId);
597-
logger.debug(`Received env vars from CLI, count: ${Object.keys(envVars).length}`);
598-
logger.debug(
599-
`First 10 env vars: ${JSON.stringify(Object.keys(envVars).slice(0, 10), null, 2)}`,
600-
);
601-
return envVars;
602-
} catch (error) {
603-
logger.error(`[TuskDrift] Error requesting env vars from CLI:`, error);
604-
return {};
605-
}
606-
}
607-
608627
requestMockSync(mockRequest: MockRequestInput): {
609628
found: boolean;
610629
response?: unknown;
@@ -682,6 +701,10 @@ export class TuskDriftCore {
682701
return this.initParams;
683702
}
684703

704+
getEnvironment(): string | undefined {
705+
return this.initParams.env;
706+
}
707+
685708
getTracer(): Tracer {
686709
return trace.getTracer(TD_INSTRUMENTATION_LIBRARY_NAME);
687710
}

src/core/tracing/SpanTransformer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ReadableSpan } from "@opentelemetry/sdk-trace-base";
22
import { SpanKind as OtSpanKind } from "@opentelemetry/api";
33
import { JsonSchemaHelper, JsonSchemaType, JsonSchema } from "./JsonSchemaHelper";
4-
import { CleanSpanData, MetadataObject, TdSpanAttributes } from "../types";
4+
import { CleanSpanData, TdSpanAttributes } from "../types";
55
import { PackageType, StatusCode } from "@use-tusk/drift-schemas/core/span";
66
import { logger, OriginalGlobalUtils } from "../utils";
77

@@ -14,7 +14,7 @@ export class SpanTransformer {
1414
* Return type is derived from protobuf schema but uses clean JSON.
1515
* We use JSON because serialized protobuf is extremely verbose and not readable.
1616
*/
17-
static transformSpanToCleanJSON(span: ReadableSpan): CleanSpanData {
17+
static transformSpanToCleanJSON(span: ReadableSpan, environment?: string): CleanSpanData {
1818
const isRootSpan = !span.parentSpanId || span.kind === OtSpanKind.SERVER;
1919

2020
// Extract data from span attributes
@@ -68,7 +68,7 @@ export class SpanTransformer {
6868
} = JsonSchemaHelper.generateSchemaAndHash(outputData));
6969
}
7070

71-
let metadata: MetadataObject | undefined = undefined;
71+
let metadata: Record<string, unknown> | undefined = undefined;
7272
if (attributes[TdSpanAttributes.METADATA]) {
7373
metadata = JSON.parse(attributes[TdSpanAttributes.METADATA] as string);
7474
}
@@ -99,6 +99,7 @@ export class SpanTransformer {
9999
submoduleName: submoduleName || "",
100100

101101
packageType: (attributes[TdSpanAttributes.PACKAGE_TYPE] as PackageType) || undefined,
102+
environment,
102103

103104
inputValue: inputData,
104105
outputValue: outputData,

src/core/tracing/SpanUtils.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
} from "@opentelemetry/api";
1111
import {
1212
IS_PRE_APP_START_CONTEXT_KEY,
13-
MetadataObject,
1413
REPLAY_TRACE_ID_CONTEXT_KEY,
1514
SPAN_KIND_CONTEXT_KEY,
1615
STOP_RECORDING_CHILD_SPANS_CONTEXT_KEY,
@@ -47,9 +46,10 @@ export interface SpanExecutorOptions {
4746
instrumentationName: string;
4847
submodule: string;
4948
inputValue: Record<string, unknown>;
49+
outputValue?: Record<string, unknown>;
5050
isPreAppStart: boolean;
5151
inputSchemaMerges?: SchemaMerges;
52-
metadata?: MetadataObject;
52+
metadata?: Record<string, unknown>;
5353
stopRecordingChildSpans?: boolean;
5454
}
5555

@@ -64,7 +64,7 @@ export interface AddSpanAttributesOptions {
6464
outputValue?: Record<string, unknown>;
6565
inputSchemaMerges?: SchemaMerges;
6666
outputSchemaMerges?: SchemaMerges;
67-
metadata?: MetadataObject;
67+
metadata?: Record<string, unknown>;
6868
transformMetadata?: {
6969
transformed: boolean;
7070
actions: Array<{
@@ -181,6 +181,7 @@ export class SpanUtils {
181181
packageType,
182182
submodule,
183183
inputValue,
184+
outputValue,
184185
inputSchemaMerges,
185186
isPreAppStart,
186187
metadata,
@@ -202,6 +203,9 @@ export class SpanUtils {
202203
[TdSpanAttributes.INSTRUMENTATION_NAME]: instrumentationName,
203204
[TdSpanAttributes.PACKAGE_TYPE]: packageType,
204205
[TdSpanAttributes.INPUT_VALUE]: createSpanInputValue(inputValue),
206+
...(outputValue && {
207+
[TdSpanAttributes.OUTPUT_VALUE]: JSON.stringify(outputValue),
208+
}),
205209
[TdSpanAttributes.IS_PRE_APP_START]: isPreAppStart,
206210
...(inputSchemaMerges && {
207211
[TdSpanAttributes.INPUT_SCHEMA_MERGES]: JSON.stringify(inputSchemaMerges),

0 commit comments

Comments
 (0)