@grepr/cli 1.14.13-15e1f25 → 1.15.0-48d68ee
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/build/dist/commands/job-draft-command.d.ts.map +1 -1
- package/build/dist/commands/job-draft-command.js +6 -71
- package/build/dist/commands/job-draft-command.js.map +1 -1
- package/build/dist/docs-index/index.json +1 -1
- package/build/dist/grepr.d.ts.map +1 -1
- package/build/dist/grepr.js +0 -2
- package/build/dist/grepr.js.map +1 -1
- package/build/dist/lib/backfill.js +4 -8
- package/build/dist/lib/backfill.js.map +1 -1
- package/build/dist/lib/job-patch.d.ts +15 -31
- package/build/dist/lib/job-patch.d.ts.map +1 -1
- package/build/dist/lib/job-patch.js +39 -709
- package/build/dist/lib/job-patch.js.map +1 -1
- package/build/dist/lib/job-plan.d.ts +3 -6
- package/build/dist/lib/job-plan.d.ts.map +1 -1
- package/build/dist/lib/job-plan.js +5 -55
- package/build/dist/lib/job-plan.js.map +1 -1
- package/build/dist/lib/option-parsers.d.ts +0 -5
- package/build/dist/lib/option-parsers.d.ts.map +1 -1
- package/build/dist/lib/option-parsers.js +0 -7
- package/build/dist/lib/option-parsers.js.map +1 -1
- package/build/dist/lib/signal-source.d.ts +1 -1
- package/build/dist/lib/signal-source.d.ts.map +1 -1
- package/build/dist/lib/signal-source.js +10 -69
- package/build/dist/lib/signal-source.js.map +1 -1
- package/build/dist/openapi/openApiTypes.d.ts +50 -7
- package/build/dist/openapi/openApiTypes.d.ts.map +1 -1
- package/build/dist/openapi/openApiTypes.js +5 -0
- package/build/dist/openapi/openApiTypes.js.map +1 -1
- package/build/dist/openapi-docs/schemas/AgentConfigCreate.md +3 -0
- package/build/dist/openapi-docs/schemas/AgentConfigPayload.md +3 -0
- package/build/dist/openapi-docs/schemas/AgentConfigUpdate.md +3 -0
- package/build/dist/openapi-docs/schemas/AgentDecisionConfiguration.md +19 -0
- package/build/dist/openapi-docs/schemas/AthenaAgentSessionsSource.md +1 -1
- package/build/dist/openapi-docs/schemas/AthenaAnalyticsSource.md +1 -1
- package/build/dist/openapi-docs/schemas/ChangeFinding.md +1 -1
- package/build/dist/openapi-docs/schemas/DecisionOutcome.md +11 -0
- package/build/dist/openapi-docs/schemas/InvestigationSummary.md +1 -1
- package/build/dist/openapi-docs/schemas/InvestigationTranscript.md +4 -1
- package/build/dist/openapi-docs/schemas/IssueRun.md +1 -1
- package/build/dist/openapi-docs/schemas/PipelineChangeRequest.md +4 -0
- package/build/dist/openapi-docs/schemas/SignalDecision.md +14 -0
- package/build/dist/openapi-docs/schemas/TrinoAnalyticsSource.md +1 -1
- package/build/dist/openapi-docs/schemas/TrinoRawLogsSource.md +1 -1
- package/build/dist/openapi-docs/schemas/TrinoRawSpanSource.md +1 -1
- package/build/dist/openapi-docs/schemas/TrinoReducerLogSource.md +1 -1
- package/build/dist/types.d.ts +8 -37
- package/build/dist/types.d.ts.map +1 -1
- package/build/dist/types.js +0 -69
- package/build/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/build/dist/commands/job-to-test-command.d.ts +0 -74
- package/build/dist/commands/job-to-test-command.d.ts.map +0 -1
- package/build/dist/commands/job-to-test-command.js +0 -161
- package/build/dist/commands/job-to-test-command.js.map +0 -1
- package/build/dist/lib/job-graph-log-pipeline-constants.d.ts +0 -12
- package/build/dist/lib/job-graph-log-pipeline-constants.d.ts.map +0 -1
- package/build/dist/lib/job-graph-log-pipeline-constants.js +0 -16
- package/build/dist/lib/job-graph-log-pipeline-constants.js.map +0 -1
- package/build/dist/lib/job-graph-transformer.d.ts +0 -119
- package/build/dist/lib/job-graph-transformer.d.ts.map +0 -1
- package/build/dist/lib/job-graph-transformer.js +0 -767
- package/build/dist/lib/job-graph-transformer.js.map +0 -1
- package/build/dist/lib/job-graph-utils.d.ts +0 -50
- package/build/dist/lib/job-graph-utils.d.ts.map +0 -1
- package/build/dist/lib/job-graph-utils.js +0 -90
- package/build/dist/lib/job-graph-utils.js.map +0 -1
|
@@ -1,767 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Job graph transformation utilities for converting production jobs to test configurations.
|
|
3
|
-
*
|
|
4
|
-
* This module provides functions to transform a production job definition into a test job
|
|
5
|
-
* by replacing sources, sinks, and adding test-specific tagging. The transformations are
|
|
6
|
-
* purely functional - they do not make any API calls or create resources.
|
|
7
|
-
*
|
|
8
|
-
* Key transformations:
|
|
9
|
-
* - Source replacement: Replace vendor sources with dataset queries or keep originals with limits
|
|
10
|
-
* - Sink replacement: Replace vendor sinks with synchronous test sinks or test dataset sinks
|
|
11
|
-
* - Test tagging: Add test run ID and sink name tags to track test data
|
|
12
|
-
*
|
|
13
|
-
* The transformed job can then be executed using the standard job:create command.
|
|
14
|
-
*/
|
|
15
|
-
import { DEFAULT_INPUT, DEFAULT_OUTPUT, JobExecution, JobProcessing, Vertex, DEFAULT_LIMIT } from '../types.js';
|
|
16
|
-
import { LogsIcebergTableSourceType, LogsSynchronousSinkType, LogsIcebergTableSinkType, LogTransformActionType, DatadogQueryPredicateType, TagActionModification, TagActionType, LogsValuesSourceType, JsonLogProcessorType, GrokParserType, LogAttributesRemapperType, LogsFilterType, LogReducerType, LogsEventSamplerType, GreprUploadedLogFileSourceType, isSinkType, } from '../openapi/openApiTypes.js';
|
|
17
|
-
import { canLimit, parseEdge, generateUUID, } from './job-graph-utils.js';
|
|
18
|
-
import { RAW_JSON_PROCESSOR, RAW_PRE_EXCEPTIONS_FILTER, RAW_PRE_PARSER_FILTER, RAW_PRE_WAREHOUSE_FILTER, } from './job-graph-log-pipeline-constants.js';
|
|
19
|
-
import fs from 'fs-extra';
|
|
20
|
-
/**
|
|
21
|
-
* Default `maxBurstLimit` for a draft logs-event-sampler. Matches the UI's
|
|
22
|
-
* template-draft fallback (`logSampler?.maxBurstLimit ?? 1000`) so CLI raw-job
|
|
23
|
-
* drafts sample at the same rate as the UI's template drafts. `maxAllowedRate`
|
|
24
|
-
* has no CLI default: when unset we omit it and let the server default apply,
|
|
25
|
-
* exactly as the UI does.
|
|
26
|
-
*/
|
|
27
|
-
export const DEFAULT_DRAFT_SAMPLER_BURST = 1000;
|
|
28
|
-
/**
|
|
29
|
-
* Transforms a production job configuration into a test job configuration.
|
|
30
|
-
*
|
|
31
|
-
* This is the main entry point for job transformation. It applies the following changes:
|
|
32
|
-
* 1. Renames the job (appends "_test" or uses custom name)
|
|
33
|
-
* 2. Updates execution and processing types if specified
|
|
34
|
-
* 3. Adds test run ID tag to job tags
|
|
35
|
-
* 4. Transforms sources (replace with dataset query, sample data, or add limits)
|
|
36
|
-
* 5. Transforms sinks (replace with sync sink or test dataset sinks)
|
|
37
|
-
* 6. Adds test tagging operations before sinks
|
|
38
|
-
*
|
|
39
|
-
* The transformation is purely functional and does not make any API calls.
|
|
40
|
-
*
|
|
41
|
-
* @param originalJob - The production job to transform
|
|
42
|
-
* @param options - Transformation options
|
|
43
|
-
* @returns A new job configuration suitable for testing
|
|
44
|
-
* @throws Error if job graph is missing or invalid
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* const testJob = transformJobToTest(prodJob, {
|
|
48
|
-
* execution: 'SYNCHRONOUS',
|
|
49
|
-
* processing: 'BATCH',
|
|
50
|
-
* datasetId: 'my_dataset',
|
|
51
|
-
* start: '2025-01-01T00:00:00Z',
|
|
52
|
-
* end: '2025-01-01T01:00:00Z',
|
|
53
|
-
* limitRecords: 100
|
|
54
|
-
* });
|
|
55
|
-
*/
|
|
56
|
-
export function transformJobToTest(originalJob, options) {
|
|
57
|
-
const job = { ...originalJob };
|
|
58
|
-
// Determine target execution and processing types (use options or keep original)
|
|
59
|
-
const targetExecution = options.execution || job.execution;
|
|
60
|
-
const targetProcessing = options.processing || job.processing;
|
|
61
|
-
// Create base transformed job with updated metadata
|
|
62
|
-
const transformedJob = {
|
|
63
|
-
...job,
|
|
64
|
-
name: options.testName || `${job.name}_test`,
|
|
65
|
-
execution: targetExecution,
|
|
66
|
-
processing: targetProcessing,
|
|
67
|
-
tags: {
|
|
68
|
-
...(job.tags || {}),
|
|
69
|
-
'grepr.test_run_id': options.testTag || generateUUID()
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
if (!transformedJob.jobGraph) {
|
|
73
|
-
throw new Error('Job graph is required');
|
|
74
|
-
}
|
|
75
|
-
const parsedGraph = parseGraph(transformedJob.jobGraph);
|
|
76
|
-
// Step 1: Add test tagging operations on all edges BEFORE transforming sources/sinks
|
|
77
|
-
// This captures the original source/sink names in the edge tags
|
|
78
|
-
addTestTagging(parsedGraph, transformedJob.tags?.['grepr.test_run_id'] || generateUUID());
|
|
79
|
-
// Step 2: Transform sources (replace with dataset, sample data, or add limits)
|
|
80
|
-
transformSources(parsedGraph, targetProcessing, options);
|
|
81
|
-
// Step 3: Transform sinks (replace with sync sink or test dataset sinks)
|
|
82
|
-
transformSinks(parsedGraph, targetExecution, options);
|
|
83
|
-
// Convert the parsed/modified graph back to SchemaGreprJobGraph format
|
|
84
|
-
const transformedVertices = [];
|
|
85
|
-
const transformedEdges = [];
|
|
86
|
-
for (const vertex of parsedGraph.values()) {
|
|
87
|
-
transformedVertices.push(vertex.operation);
|
|
88
|
-
for (const [outputPort, nextIos] of vertex.next) {
|
|
89
|
-
for (const nextIo of nextIos) {
|
|
90
|
-
const edgeStr = `${vertex.name}:${outputPort} -> ${nextIo.vertex.name}:${nextIo.name}`;
|
|
91
|
-
transformedEdges.push(edgeStr);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
transformedJob.jobGraph = {
|
|
96
|
-
vertices: transformedVertices,
|
|
97
|
-
edges: transformedEdges
|
|
98
|
-
};
|
|
99
|
-
return transformedJob;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Parses the job graph into a vertex-based representation, returning all vertices.
|
|
103
|
-
*/
|
|
104
|
-
function parseGraph(jobGraph) {
|
|
105
|
-
const verticesMap = new Map();
|
|
106
|
-
// Create Vertex objects for each operation
|
|
107
|
-
for (const operation of jobGraph.vertices) {
|
|
108
|
-
verticesMap.set(operation.name, new Vertex(operation));
|
|
109
|
-
}
|
|
110
|
-
// Link vertices based on edges
|
|
111
|
-
for (const edge of jobGraph.edges) {
|
|
112
|
-
const { sourceVertex: sourceVertexName, targetVertex: targetVertexName, sourcePort, targetPort } = parseEdge(edge);
|
|
113
|
-
const sourceVertex = verticesMap.get(sourceVertexName);
|
|
114
|
-
const targetVertex = verticesMap.get(targetVertexName);
|
|
115
|
-
if (sourceVertex && targetVertex) {
|
|
116
|
-
sourceVertex.addNext(sourcePort, { name: targetPort, vertex: targetVertex });
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return verticesMap;
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Transforms job graph sources based on the specified options.
|
|
123
|
-
*
|
|
124
|
-
* Three transformation strategies are supported:
|
|
125
|
-
* 1. Sample data file: Replace all sources with a LogsValuesSource (not yet implemented)
|
|
126
|
-
* 2. Dataset query: Replace all sources with a single IcebergTableSource
|
|
127
|
-
* 3. In-place: Keep original sources but add record limits for batch processing
|
|
128
|
-
*
|
|
129
|
-
* @param jobGraph - The job graph to transform
|
|
130
|
-
* @param processing - Target processing type
|
|
131
|
-
* @param options - Source transformation options
|
|
132
|
-
* @returns Transformed job graph with updated sources
|
|
133
|
-
*/
|
|
134
|
-
function transformSources(jobGraph, processing, options) {
|
|
135
|
-
if (options.sampleDataFile) {
|
|
136
|
-
transformSourcesToSampleData(jobGraph, options.sampleDataFile);
|
|
137
|
-
}
|
|
138
|
-
else if (options.datasetId) {
|
|
139
|
-
transformSourcesToDatasetQuery(jobGraph, options.datasetId, options.query, options.start, options.end, processing, options.limitRecords);
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
transformSourcesInPlace(jobGraph, processing, options.limitRecords);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Transforms sources to use sample data from a file.
|
|
147
|
-
*
|
|
148
|
-
* This replaces all existing sources with a single LogsValuesSource that
|
|
149
|
-
* emits the log events from the sample data file. The file should contain
|
|
150
|
-
* a JSON array of log events matching the LogEvent schema.
|
|
151
|
-
*
|
|
152
|
-
* All edges from the old sources are rewired to connect from the new source
|
|
153
|
-
* to the first downstream operation in the pipeline.
|
|
154
|
-
*
|
|
155
|
-
* @param jobGraph - The job graph to transform
|
|
156
|
-
* @param sampleDataFile - Path to sample data file (JSON array of log events)
|
|
157
|
-
* @throws Error if the file cannot be read or parsed
|
|
158
|
-
*/
|
|
159
|
-
function transformSourcesToSampleData(jobGraph, sampleDataFile) {
|
|
160
|
-
if (!fs.existsSync(sampleDataFile)) {
|
|
161
|
-
throw new Error(`Sample data file not found: ${sampleDataFile}`);
|
|
162
|
-
}
|
|
163
|
-
// Read and parse the sample data file
|
|
164
|
-
let sampleLogs;
|
|
165
|
-
try {
|
|
166
|
-
const fileContent = fs.readFileSync(sampleDataFile, 'utf-8');
|
|
167
|
-
sampleLogs = JSON.parse(fileContent);
|
|
168
|
-
if (!Array.isArray(sampleLogs)) {
|
|
169
|
-
throw new Error('Sample data must be a JSON array of log events');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch (error) {
|
|
173
|
-
if (error.message.includes('Sample data')) {
|
|
174
|
-
throw error;
|
|
175
|
-
}
|
|
176
|
-
throw new Error(`Failed to parse sample data file: ${error.message}`);
|
|
177
|
-
}
|
|
178
|
-
// Create a new LogsValuesSource with the sample data
|
|
179
|
-
const newSource = {
|
|
180
|
-
type: LogsValuesSourceType.logs_values_source,
|
|
181
|
-
name: 'test_sample_source',
|
|
182
|
-
values: sampleLogs,
|
|
183
|
-
indefinite: false
|
|
184
|
-
};
|
|
185
|
-
const newSourceVertex = new Vertex(newSource);
|
|
186
|
-
// Remove all old sources and connect them to the new source
|
|
187
|
-
for (const vertex of Array.from(jobGraph.values())) {
|
|
188
|
-
if (isSource(vertex)) {
|
|
189
|
-
// Rewire edges from old source's next vertices to new source
|
|
190
|
-
for (const [port, nextIos] of vertex.next) {
|
|
191
|
-
for (const nextIo of nextIos) {
|
|
192
|
-
vertex.removeNext(port, nextIo);
|
|
193
|
-
newSourceVertex.addNext(DEFAULT_OUTPUT, nextIo);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
jobGraph.delete(vertex.name);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Add the new source vertex to the graph
|
|
200
|
-
jobGraph.set(newSourceVertex.name, newSourceVertex);
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Transforms sources to use an Iceberg dataset as the data source.
|
|
204
|
-
*
|
|
205
|
-
* This replaces all existing sources with a single IcebergTableSource that:
|
|
206
|
-
* - Reads from the specified dataset
|
|
207
|
-
* - Optionally filters data using a query
|
|
208
|
-
* - Reads data within the specified time range
|
|
209
|
-
* - Limits records for batch processing if specified
|
|
210
|
-
*
|
|
211
|
-
* All edges from the old sources are rewired to connect from the new source
|
|
212
|
-
* to the first downstream operation in the pipeline.
|
|
213
|
-
*
|
|
214
|
-
* @param jobGraph - The job graph to transform
|
|
215
|
-
* @param datasetId - ID of the dataset to read from
|
|
216
|
-
* @param query - Optional query to filter dataset data
|
|
217
|
-
* @param start - Start time for data query
|
|
218
|
-
* @param end - End time for data query
|
|
219
|
-
* @param processing - Processing type (BATCH or STREAMING)
|
|
220
|
-
* @param limitRecords - Optional record limit for batch processing
|
|
221
|
-
* @returns Transformed job graph with dataset source
|
|
222
|
-
*/
|
|
223
|
-
function transformSourcesToDatasetQuery(jobGraph, datasetId, query, start, end, processing, limitRecords) {
|
|
224
|
-
const newSource = {
|
|
225
|
-
type: LogsIcebergTableSourceType.logs_iceberg_table_source,
|
|
226
|
-
name: 'test_dataset_source',
|
|
227
|
-
datasetId,
|
|
228
|
-
query: {
|
|
229
|
-
type: DatadogQueryPredicateType.datadog_query,
|
|
230
|
-
query: query || ''
|
|
231
|
-
},
|
|
232
|
-
start: start || '',
|
|
233
|
-
end: end || '',
|
|
234
|
-
limit: limitRecords ?? DEFAULT_LIMIT
|
|
235
|
-
};
|
|
236
|
-
const newSourceVertex = new Vertex(newSource);
|
|
237
|
-
// Remove all old sources and connect them to the new source
|
|
238
|
-
for (const vertex of Array.from(jobGraph.values())) {
|
|
239
|
-
if (isSource(vertex)) {
|
|
240
|
-
// This is a source vertex since it has no inputs.
|
|
241
|
-
// Rewire edges from old source's next vertices to new source
|
|
242
|
-
for (const [port, nextIos] of vertex.next) {
|
|
243
|
-
for (const nextIo of nextIos) {
|
|
244
|
-
vertex.removeNext(port, nextIo);
|
|
245
|
-
newSourceVertex.addNext(DEFAULT_OUTPUT, nextIo);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
jobGraph.delete(vertex.name);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
// Add the new source vertex to the graph
|
|
252
|
-
jobGraph.set(newSourceVertex.name, newSourceVertex);
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Transforms sources in place by adding record limits for batch processing.
|
|
256
|
-
*
|
|
257
|
-
* This keeps the original sources but adds a 'limit' parameter to sources
|
|
258
|
-
* that support it (e.g., IcebergTableSource). This is useful when you want
|
|
259
|
-
* to test with the same data sources but limit the amount of data processed.
|
|
260
|
-
*
|
|
261
|
-
* @param jobGraph - The job graph to transform
|
|
262
|
-
* @param processing - Processing type
|
|
263
|
-
* @param limitRecords - Optional record limit to apply
|
|
264
|
-
* @returns Transformed job graph with limited sources
|
|
265
|
-
*/
|
|
266
|
-
function transformSourcesInPlace(jobGraph, processing, limitRecords) {
|
|
267
|
-
for (const vertex of jobGraph.values()) {
|
|
268
|
-
if (!isSource(vertex)) {
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
if (processing === JobProcessing.BATCH && limitRecords && canLimit(vertex.operation)) {
|
|
272
|
-
vertex.operation = {
|
|
273
|
-
...vertex.operation,
|
|
274
|
-
limit: limitRecords
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Transforms job graph sinks based on the target execution type.
|
|
281
|
-
*
|
|
282
|
-
* Two transformation strategies:
|
|
283
|
-
* 1. SYNCHRONOUS: Replace all sinks with a single synchronous sink that returns results
|
|
284
|
-
* 2. ASYNCHRONOUS: Replace vendor sinks with test dataset sinks (if testDataset provided)
|
|
285
|
-
*
|
|
286
|
-
* @param jobGraph - The job graph to transform
|
|
287
|
-
* @param execution - Target execution type
|
|
288
|
-
* @param options - Sink transformation options
|
|
289
|
-
* @returns Transformed job graph with updated sinks
|
|
290
|
-
*/
|
|
291
|
-
function transformSinks(jobGraph, execution, options) {
|
|
292
|
-
if (execution === JobExecution.SYNCHRONOUS) {
|
|
293
|
-
return transformSinksToSynchronous(jobGraph);
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
if (!options.testDataset) {
|
|
297
|
-
throw new Error('Test dataset ID is required for ASYNCHRONOUS test job');
|
|
298
|
-
}
|
|
299
|
-
return transformSinksToAsynchronous(jobGraph, options.testDataset);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Transforms all sinks to a single synchronous sink.
|
|
304
|
-
*
|
|
305
|
-
* For synchronous testing, we replace all sink operations with a single
|
|
306
|
-
* LogsSyncSink that returns the processed data in the API response.
|
|
307
|
-
* This allows immediate verification of results.
|
|
308
|
-
*
|
|
309
|
-
* All operations that were feeding into the original sinks are rewired
|
|
310
|
-
* to feed into the new synchronous sink.
|
|
311
|
-
*
|
|
312
|
-
* @param jobGraph - The job graph to transform
|
|
313
|
-
*/
|
|
314
|
-
function transformSinksToSynchronous(jobGraph) {
|
|
315
|
-
// Create a single synchronous sink to replace them all
|
|
316
|
-
const syncSink = {
|
|
317
|
-
type: LogsSynchronousSinkType.logs_sync_sink,
|
|
318
|
-
name: 'test_synchronous_sink'
|
|
319
|
-
};
|
|
320
|
-
const sinkVertex = new Vertex(syncSink);
|
|
321
|
-
wireNewSink(jobGraph, sinkVertex);
|
|
322
|
-
}
|
|
323
|
-
/**
|
|
324
|
-
* Transforms sinks for asynchronous testing with a test dataset.
|
|
325
|
-
*
|
|
326
|
-
* If a test dataset ID is provided, this replaces vendor sinks, raw data sinks,
|
|
327
|
-
* and event dedup sinks with Iceberg table sinks that write to the test dataset.
|
|
328
|
-
*
|
|
329
|
-
* The original sink names are preserved to maintain the graph structure.
|
|
330
|
-
* Test tagging (added separately) allows distinguishing which sink the data came from.
|
|
331
|
-
*
|
|
332
|
-
* @param jobGraph - The job graph to transform
|
|
333
|
-
* @param testDatasetId - Optional test dataset ID for output
|
|
334
|
-
*/
|
|
335
|
-
function transformSinksToAsynchronous(jobGraph, testDatasetId) {
|
|
336
|
-
// For each sink (no outputs), replace with test dataset sink.
|
|
337
|
-
const sinkOp = {
|
|
338
|
-
type: LogsIcebergTableSinkType.logs_iceberg_table_sink,
|
|
339
|
-
name: 'test_async_sink',
|
|
340
|
-
datasetId: testDatasetId
|
|
341
|
-
};
|
|
342
|
-
const sinkVertex = new Vertex(sinkOp);
|
|
343
|
-
wireNewSink(jobGraph, sinkVertex);
|
|
344
|
-
}
|
|
345
|
-
function wireNewSink(jobGraph, sinkVertex) {
|
|
346
|
-
// Find all vertices that feed into sinks and rewire them to the new sync sink
|
|
347
|
-
for (const vertex of Array.from(jobGraph.values())) {
|
|
348
|
-
if (isSink(vertex)) {
|
|
349
|
-
// This is a sink vertex since it has no outputs.
|
|
350
|
-
// Rewire edges from old sink's prev vertices to new sync sink
|
|
351
|
-
for (const [port, prevIos] of vertex.prev) {
|
|
352
|
-
for (const prevIo of prevIos) {
|
|
353
|
-
vertex.removePrev(port, prevIo);
|
|
354
|
-
prevIo.vertex.addNext(prevIo.name, { name: DEFAULT_INPUT, vertex: sinkVertex });
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
jobGraph.delete(vertex.name);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
// Add the new sink vertex to the graph
|
|
361
|
-
jobGraph.set(sinkVertex.name, sinkVertex);
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Adds test tagging operations on all edges, with the id of the
|
|
365
|
-
* original edge and the test run ID.
|
|
366
|
-
*
|
|
367
|
-
* @param jobGraph - The job graph to transform
|
|
368
|
-
* @param testRunId - Unique test run identifier
|
|
369
|
-
*/
|
|
370
|
-
function addTestTagging(jobGraph, testRunId) {
|
|
371
|
-
for (const vertex of Array.from(jobGraph.values())) {
|
|
372
|
-
for (const [output, nextIos] of Array.from(vertex.next)) {
|
|
373
|
-
for (const nextIo of nextIos) {
|
|
374
|
-
// insert a tagging operation before this io
|
|
375
|
-
const edge = `${vertex.name}_${output}_${nextIo.vertex.name}_${nextIo.name}`;
|
|
376
|
-
const tagOpName = `${edge}_test_tag`;
|
|
377
|
-
const tagOp = createTestTagOperation(tagOpName, testRunId, edge);
|
|
378
|
-
const tagVertex = new Vertex(tagOp);
|
|
379
|
-
// Rewire edges: vertex -> tagOp -> nextIo.vertex
|
|
380
|
-
vertex.removeNext(output, nextIo);
|
|
381
|
-
vertex.addNext(output, { name: DEFAULT_INPUT, vertex: tagVertex });
|
|
382
|
-
tagVertex.addNext(DEFAULT_OUTPUT, nextIo);
|
|
383
|
-
// Add the new tagging vertex to the graph
|
|
384
|
-
jobGraph.set(tagVertex.name, tagVertex);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Creates a log transform operation that adds test tags.
|
|
391
|
-
*
|
|
392
|
-
* The operation adds two tags:
|
|
393
|
-
* - grepr.test_run_id: Identifies which test run this data belongs to
|
|
394
|
-
* - grepr.edge: Identifies edge this data was going through
|
|
395
|
-
*
|
|
396
|
-
* These tags are added using the ADD modification, which appends to existing tag arrays.
|
|
397
|
-
*
|
|
398
|
-
* @param name - Name for the tagging operation
|
|
399
|
-
* @param testRunId - Unique test run identifier
|
|
400
|
-
* @param edge - Name of the edge
|
|
401
|
-
* @returns A LogTransformAction operation that adds the tags
|
|
402
|
-
*/
|
|
403
|
-
function createTestTagOperation(name, testRunId, edge) {
|
|
404
|
-
return {
|
|
405
|
-
type: LogTransformActionType.log_transform,
|
|
406
|
-
name,
|
|
407
|
-
transforms: [
|
|
408
|
-
{
|
|
409
|
-
order: 0,
|
|
410
|
-
type: TagActionType.tag_action,
|
|
411
|
-
modification: TagActionModification.ADD,
|
|
412
|
-
tagKey: 'grepr.test_run_id',
|
|
413
|
-
values: [testRunId]
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
order: 1,
|
|
417
|
-
type: TagActionType.tag_action,
|
|
418
|
-
modification: TagActionModification.ADD,
|
|
419
|
-
tagKey: 'grepr.edge',
|
|
420
|
-
values: [edge]
|
|
421
|
-
}
|
|
422
|
-
]
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
const isSource = (vertex) => vertex.prev.size === 0;
|
|
426
|
-
const isSink = (vertex) => vertex.next.size === 0;
|
|
427
|
-
/**
|
|
428
|
-
* Vertex types tapped by type (the ones `applyPatchToJobGraph` mutates), so
|
|
429
|
-
* legacy graphs with noncanonical names still tap the edited vertex. Pass-through
|
|
430
|
-
* stages are matched by name instead (see {@link TAP_STAGE_NAME_TYPES}) because
|
|
431
|
-
* linearizing parallel per-sink `logs-filter` vertices would change draft semantics.
|
|
432
|
-
* Tap-eligible vertices fork their output to the shared sync sink (tagged for demux);
|
|
433
|
-
* sources keep streaming live and sinks collapse into that one shared sync sink.
|
|
434
|
-
*/
|
|
435
|
-
const TAP_MUTABLE_TYPES = new Set([
|
|
436
|
-
GrokParserType.grok_parser,
|
|
437
|
-
LogAttributesRemapperType.log_attributes_remapper,
|
|
438
|
-
LogReducerType.log_reducer,
|
|
439
|
-
]);
|
|
440
|
-
const TAP_STAGE_NAME_TYPES = new Map([
|
|
441
|
-
[RAW_PRE_PARSER_FILTER, LogsFilterType.logs_filter],
|
|
442
|
-
[RAW_JSON_PROCESSOR, JsonLogProcessorType.json_log_processor],
|
|
443
|
-
[RAW_PRE_WAREHOUSE_FILTER, LogsFilterType.logs_filter],
|
|
444
|
-
[RAW_PRE_EXCEPTIONS_FILTER, LogsFilterType.logs_filter],
|
|
445
|
-
]);
|
|
446
|
-
function isTapEligible(vertex) {
|
|
447
|
-
if (TAP_MUTABLE_TYPES.has(vertex.type)) {
|
|
448
|
-
return true;
|
|
449
|
-
}
|
|
450
|
-
return TAP_STAGE_NAME_TYPES.get(vertex.name) === vertex.type;
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* Tap-eligible vertices in topological order (Kahn's algorithm). Uses edges,
|
|
454
|
-
* not vertex-array order, since the array order isn't guaranteed topological.
|
|
455
|
-
*/
|
|
456
|
-
function collectTapEligibleInOrder(jobGraph) {
|
|
457
|
-
const vertices = jobGraph.vertices ?? [];
|
|
458
|
-
const edges = jobGraph.edges ?? [];
|
|
459
|
-
const verticesByName = new Map();
|
|
460
|
-
for (const v of vertices) {
|
|
461
|
-
if (v.name)
|
|
462
|
-
verticesByName.set(v.name, v);
|
|
463
|
-
}
|
|
464
|
-
const downstream = new Map();
|
|
465
|
-
const inboundCount = new Map();
|
|
466
|
-
for (const v of vertices) {
|
|
467
|
-
if (v.name) {
|
|
468
|
-
downstream.set(v.name, []);
|
|
469
|
-
inboundCount.set(v.name, 0);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
for (const edge of edges) {
|
|
473
|
-
const { sourceVertex, targetVertex } = parseEdge(edge);
|
|
474
|
-
downstream.get(sourceVertex)?.push(targetVertex);
|
|
475
|
-
inboundCount.set(targetVertex, (inboundCount.get(targetVertex) ?? 0) + 1);
|
|
476
|
-
}
|
|
477
|
-
const queue = [];
|
|
478
|
-
for (const [name, count] of inboundCount) {
|
|
479
|
-
if (count === 0)
|
|
480
|
-
queue.push(name);
|
|
481
|
-
}
|
|
482
|
-
const ordered = [];
|
|
483
|
-
const processed = new Set();
|
|
484
|
-
while (queue.length > 0) {
|
|
485
|
-
const name = queue.shift();
|
|
486
|
-
processed.add(name);
|
|
487
|
-
const vertex = verticesByName.get(name);
|
|
488
|
-
if (vertex && isTapEligible(vertex)) {
|
|
489
|
-
ordered.push(vertex);
|
|
490
|
-
}
|
|
491
|
-
for (const child of downstream.get(name) ?? []) {
|
|
492
|
-
const remaining = (inboundCount.get(child) ?? 0) - 1;
|
|
493
|
-
inboundCount.set(child, remaining);
|
|
494
|
-
if (remaining === 0)
|
|
495
|
-
queue.push(child);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
if (processed.size < verticesByName.size) {
|
|
499
|
-
throw new Error(`Unsupported raw job graph shape: cycle detected while building tapped draft ` +
|
|
500
|
-
`(${processed.size}/${verticesByName.size} vertices were topologically reachable).`);
|
|
501
|
-
}
|
|
502
|
-
return ordered;
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Build a source-preserving draft from a job-graph pipeline: keep the proposed
|
|
506
|
-
* source vertices streaming live (so add/remove-source edits are exercised),
|
|
507
|
-
* interpose a logs-event-sampler after each live source to bound per-source
|
|
508
|
-
* volume, strip production sinks, and fan output into a shared logs-sync-sink.
|
|
509
|
-
* Mirrors the UI's template draft mode for raw (non-templated) jobs. Verifies
|
|
510
|
-
* the graph runs, not external sink delivery.
|
|
511
|
-
*/
|
|
512
|
-
export function transformJobGraphToSourcePreservingDraft(originalJob, options = {}) {
|
|
513
|
-
if (!originalJob.jobGraph) {
|
|
514
|
-
throw new Error('Job graph is required');
|
|
515
|
-
}
|
|
516
|
-
const graph = structuredClone(originalJob.jobGraph);
|
|
517
|
-
const existingNames = new Set();
|
|
518
|
-
for (const vertex of graph.vertices) {
|
|
519
|
-
if (vertex.name)
|
|
520
|
-
existingNames.add(vertex.name);
|
|
521
|
-
}
|
|
522
|
-
const targetProcessing = options.processing ?? originalJob.processing ?? JobProcessing.STREAMING;
|
|
523
|
-
const sinkNames = new Set(graph.vertices
|
|
524
|
-
.filter(isProductionSinkOperation)
|
|
525
|
-
.map(vertex => vertex.name)
|
|
526
|
-
.filter((name) => typeof name === 'string' && name.length > 0));
|
|
527
|
-
const originalParsedEdges = parseGraphEdges(graph.edges ?? []);
|
|
528
|
-
const sinkInboundEdges = originalParsedEdges.filter(edge => sinkNames.has(edge.targetVertex));
|
|
529
|
-
const retainedVertices = graph.vertices.filter(vertex => !sinkNames.has(vertex.name));
|
|
530
|
-
const retainedEdges = (graph.edges ?? []).filter(edge => !edgeTouchesAny(edge, sinkNames));
|
|
531
|
-
// Interpose a logs-event-sampler after each live source (mutates the retained
|
|
532
|
-
// vertices/edges in place) so the live stream is rate-limited, mirroring the
|
|
533
|
-
// UI's template draft mode. Done before the tap/sink wiring below so the
|
|
534
|
-
// sampler vertices are part of the graph the rest of this function reasons over.
|
|
535
|
-
insertSourceSamplers(retainedVertices, retainedEdges, existingNames, options.sampler);
|
|
536
|
-
const syncSinkName = uniqueDraftName(existingNames, 'draft_source_preserving_sink');
|
|
537
|
-
existingNames.add(syncSinkName);
|
|
538
|
-
const syncSink = makeOperation(LogsSynchronousSinkType.logs_sync_sink, syncSinkName);
|
|
539
|
-
const tapVertices = collectTapEligibleInOrder({ vertices: retainedVertices, edges: retainedEdges });
|
|
540
|
-
const tapEligibleNames = new Set(tapVertices
|
|
541
|
-
.map(vertex => vertex.name)
|
|
542
|
-
.filter((name) => typeof name === 'string' && name.length > 0));
|
|
543
|
-
const draftEdges = [...retainedEdges];
|
|
544
|
-
for (const inbound of sinkInboundEdges) {
|
|
545
|
-
// A sink chained to another sink was itself stripped, so it cannot source a
|
|
546
|
-
// draft edge. Its records are already observed one hop upstream.
|
|
547
|
-
if (sinkNames.has(inbound.sourceVertex))
|
|
548
|
-
continue;
|
|
549
|
-
// Tap-eligible sink-predecessors reach the sync sink through their tagger
|
|
550
|
-
// below; adding a direct edge here too would deliver their records twice
|
|
551
|
-
// (once untagged, once tagged), inflating counts the per-stage demux can't
|
|
552
|
-
// attribute. Route them only through the tagger.
|
|
553
|
-
if (tapEligibleNames.has(inbound.sourceVertex))
|
|
554
|
-
continue;
|
|
555
|
-
addDraftEdgeIfMissing(draftEdges, inbound.sourceVertex, inbound.sourcePort, syncSinkName, DEFAULT_INPUT);
|
|
556
|
-
}
|
|
557
|
-
// Fallback: no production sinks had recorded inbound edges; wire all terminal vertices to the sync sink.
|
|
558
|
-
if (sinkInboundEdges.length === 0) {
|
|
559
|
-
for (const terminal of terminalVertices(retainedVertices, retainedEdges)) {
|
|
560
|
-
if (tapEligibleNames.has(terminal.name))
|
|
561
|
-
continue; // same double-delivery reasoning as above
|
|
562
|
-
addDraftEdgeIfMissing(draftEdges, terminal.name, DEFAULT_OUTPUT, syncSinkName, DEFAULT_INPUT);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
const taggerVertices = [];
|
|
566
|
-
for (const tapVertex of tapVertices) {
|
|
567
|
-
if (!tapVertex.name)
|
|
568
|
-
continue;
|
|
569
|
-
const taggerName = uniqueDraftName(existingNames, `tap_${tapVertex.name}`);
|
|
570
|
-
existingNames.add(taggerName);
|
|
571
|
-
const tagger = makeSinkSourceTagger(taggerName, tapVertex.name);
|
|
572
|
-
taggerVertices.push(tagger);
|
|
573
|
-
addDraftEdgeIfMissing(draftEdges, tapVertex.name, DEFAULT_OUTPUT, taggerName, DEFAULT_INPUT);
|
|
574
|
-
addDraftEdgeIfMissing(draftEdges, taggerName, DEFAULT_OUTPUT, syncSinkName, DEFAULT_INPUT);
|
|
575
|
-
}
|
|
576
|
-
return {
|
|
577
|
-
...originalJob,
|
|
578
|
-
name: options.testName ?? `${originalJob.name}_draft`,
|
|
579
|
-
execution: JobExecution.SYNCHRONOUS,
|
|
580
|
-
processing: targetProcessing,
|
|
581
|
-
tags: {
|
|
582
|
-
...(originalJob.tags ?? {}),
|
|
583
|
-
},
|
|
584
|
-
jobGraph: {
|
|
585
|
-
vertices: [...retainedVertices, ...taggerVertices, syncSink],
|
|
586
|
-
edges: draftEdges,
|
|
587
|
-
},
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
function parseGraphEdges(edges) {
|
|
591
|
-
return edges.map(edge => parseEdge(edge));
|
|
592
|
-
}
|
|
593
|
-
function edgeTouchesAny(edge, names) {
|
|
594
|
-
const parsed = parseEdge(edge);
|
|
595
|
-
return names.has(parsed.sourceVertex) || names.has(parsed.targetVertex);
|
|
596
|
-
}
|
|
597
|
-
function isProductionSinkOperation(operation) {
|
|
598
|
-
return typeof operation.type === 'string' && isSinkType(operation.type);
|
|
599
|
-
}
|
|
600
|
-
function makeOperation(type, name, extra = {}) {
|
|
601
|
-
return { type, name, ...extra };
|
|
602
|
-
}
|
|
603
|
-
/** A `log-transform` vertex that tags records with `sink-source=<sourceName>` for per-stage demux. */
|
|
604
|
-
function makeSinkSourceTagger(taggerName, sourceName) {
|
|
605
|
-
return makeOperation(LogTransformActionType.log_transform, taggerName, {
|
|
606
|
-
transforms: [
|
|
607
|
-
{ type: TagActionType.tag_action, modification: TagActionModification.ADD, tagKey: 'sink-source', values: [sourceName], order: 0 },
|
|
608
|
-
],
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
/** A `logs-event-sampler` vertex tuned for a draft, mirroring the UI's template draft sampler. */
|
|
612
|
-
function makeEventSampler(name, sampler) {
|
|
613
|
-
const extra = {
|
|
614
|
-
maxBurstLimit: sampler?.maxBurstLimit ?? DEFAULT_DRAFT_SAMPLER_BURST,
|
|
615
|
-
};
|
|
616
|
-
// Omit maxAllowedRate unless set so the server's sampler default applies, exactly as the UI does.
|
|
617
|
-
if (sampler?.maxAllowedRate !== undefined) {
|
|
618
|
-
extra.maxAllowedRate = sampler.maxAllowedRate;
|
|
619
|
-
}
|
|
620
|
-
return makeOperation(LogsEventSamplerType.logs_event_sampler, name, extra);
|
|
621
|
-
}
|
|
622
|
-
/**
|
|
623
|
-
* Interpose a logs-event-sampler between each live source and its downstream
|
|
624
|
-
* targets, rewiring `source -> target` into `source -> sampler -> target`.
|
|
625
|
-
* Mutates `vertices` (appends samplers) and `edges` (rewires) in place.
|
|
626
|
-
*
|
|
627
|
-
* A source is any vertex with no inbound edge. Uploaded-log-file sources are
|
|
628
|
-
* skipped, matching the UI, which doesn't rate-limit uploaded-file drafts.
|
|
629
|
-
* Isolated sources (no outbound edge) get no sampler — there's nothing to bound.
|
|
630
|
-
*/
|
|
631
|
-
function insertSourceSamplers(vertices, edges, existingNames, sampler) {
|
|
632
|
-
const targets = new Set(parseGraphEdges(edges).map(edge => edge.targetVertex));
|
|
633
|
-
const sources = vertices.filter((vertex) => typeof vertex.name === 'string' &&
|
|
634
|
-
vertex.name.length > 0 &&
|
|
635
|
-
!targets.has(vertex.name) &&
|
|
636
|
-
vertex.type !== GreprUploadedLogFileSourceType.grepr_uploaded_log_file_source);
|
|
637
|
-
for (const source of sources) {
|
|
638
|
-
const outbound = parseGraphEdges(edges).filter(edge => edge.sourceVertex === source.name);
|
|
639
|
-
if (outbound.length === 0)
|
|
640
|
-
continue;
|
|
641
|
-
const samplerName = uniqueDraftName(existingNames, `${source.name}_draft_sampler`);
|
|
642
|
-
existingNames.add(samplerName);
|
|
643
|
-
vertices.push(makeEventSampler(samplerName, sampler));
|
|
644
|
-
// Drop the source's outbound edges, then relink them through the sampler.
|
|
645
|
-
for (let i = edges.length - 1; i >= 0; i--) {
|
|
646
|
-
if (parseEdge(edges[i]).sourceVertex === source.name)
|
|
647
|
-
edges.splice(i, 1);
|
|
648
|
-
}
|
|
649
|
-
edges.push(formatDraftEdge(source.name, outbound[0]?.sourcePort ?? DEFAULT_OUTPUT, samplerName, DEFAULT_INPUT));
|
|
650
|
-
for (const edge of outbound) {
|
|
651
|
-
edges.push(formatDraftEdge(samplerName, DEFAULT_OUTPUT, edge.targetVertex, edge.targetPort));
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
function terminalVertices(vertices, edges) {
|
|
656
|
-
const sources = new Set();
|
|
657
|
-
const targets = new Set();
|
|
658
|
-
for (const edge of parseGraphEdges(edges)) {
|
|
659
|
-
sources.add(edge.sourceVertex);
|
|
660
|
-
targets.add(edge.targetVertex);
|
|
661
|
-
}
|
|
662
|
-
return vertices.filter(vertex => typeof vertex.name === 'string' &&
|
|
663
|
-
vertex.name.length > 0 &&
|
|
664
|
-
!sources.has(vertex.name) &&
|
|
665
|
-
targets.has(vertex.name));
|
|
666
|
-
}
|
|
667
|
-
function uniqueDraftName(existing, base) {
|
|
668
|
-
if (!existing.has(base))
|
|
669
|
-
return base;
|
|
670
|
-
let i = 1;
|
|
671
|
-
while (existing.has(`${base}_${i}`)) {
|
|
672
|
-
i += 1;
|
|
673
|
-
}
|
|
674
|
-
return `${base}_${i}`;
|
|
675
|
-
}
|
|
676
|
-
function formatDraftEdge(sourceVertex, sourcePort, targetVertex, targetPort) {
|
|
677
|
-
const source = sourcePort === DEFAULT_OUTPUT ? sourceVertex : `${sourceVertex}:${sourcePort}`;
|
|
678
|
-
const target = targetPort === DEFAULT_INPUT ? targetVertex : `${targetVertex}:${targetPort}`;
|
|
679
|
-
return `${source} -> ${target}`;
|
|
680
|
-
}
|
|
681
|
-
function addDraftEdgeIfMissing(edges, sourceVertex, sourcePort, targetVertex, targetPort) {
|
|
682
|
-
const exists = parseGraphEdges(edges).some(edge => edge.sourceVertex === sourceVertex &&
|
|
683
|
-
edge.sourcePort === sourcePort &&
|
|
684
|
-
edge.targetVertex === targetVertex &&
|
|
685
|
-
edge.targetPort === targetPort);
|
|
686
|
-
if (!exists) {
|
|
687
|
-
edges.push(formatDraftEdge(sourceVertex, sourcePort, targetVertex, targetPort));
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Displays a summary of the transformations applied to a job.
|
|
692
|
-
*
|
|
693
|
-
* This shows a before/after comparison including:
|
|
694
|
-
* - Job name changes
|
|
695
|
-
* - Execution type changes
|
|
696
|
-
* - Processing type changes
|
|
697
|
-
* - operation and edge changes
|
|
698
|
-
* - Test run ID
|
|
699
|
-
*
|
|
700
|
-
* The output is formatted for console display.
|
|
701
|
-
*
|
|
702
|
-
* @param originalJob - The original production job
|
|
703
|
-
* @param transformedJob - The transformed test job
|
|
704
|
-
*/
|
|
705
|
-
export function showDiff(originalJob, transformedJob) {
|
|
706
|
-
console.log('\n=== TRANSFORMATION SUMMARY ===\n');
|
|
707
|
-
console.log(`Name: ${originalJob.name} → ${transformedJob.name}`);
|
|
708
|
-
console.log(`Execution: ${originalJob.execution} → ${transformedJob.execution}`);
|
|
709
|
-
console.log(`Processing: ${originalJob.processing} → ${transformedJob.processing}`);
|
|
710
|
-
if (originalJob.jobGraph && transformedJob.jobGraph) {
|
|
711
|
-
// Show added/removed operations and added/removed edges
|
|
712
|
-
// For each operation in original, check if in transformed, and if changed
|
|
713
|
-
const originalOps = new Set(originalJob.jobGraph.vertices.map(v => v.name));
|
|
714
|
-
const transformedOps = new Set(transformedJob.jobGraph.vertices.map(v => v.name));
|
|
715
|
-
const originalOpsMap = new Map(originalJob.jobGraph.vertices.map(v => [v.name, v]));
|
|
716
|
-
const transformedOpsMap = new Map(transformedJob.jobGraph.vertices.map(v => [v.name, v]));
|
|
717
|
-
const addedOps = Array.from(transformedOps).filter(op => !originalOps.has(op));
|
|
718
|
-
const removedOps = Array.from(originalOps).filter(op => !transformedOps.has(op));
|
|
719
|
-
const changedOps = Array.from(originalOps).filter(op => transformedOps.has(op) &&
|
|
720
|
-
JSON.stringify(originalOpsMap.get(op)) !== JSON.stringify(transformedOpsMap.get(op)));
|
|
721
|
-
if (addedOps.length > 0) {
|
|
722
|
-
console.log(`Added Operations:`);
|
|
723
|
-
for (const opName of addedOps) {
|
|
724
|
-
const op = transformedOpsMap.get(opName);
|
|
725
|
-
console.log(`${JSON.stringify(op, null, 2)}`);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
if (removedOps.length > 0) {
|
|
729
|
-
console.log(`Removed Operations:`);
|
|
730
|
-
for (const opName of removedOps) {
|
|
731
|
-
const op = originalOpsMap.get(opName);
|
|
732
|
-
console.log(`${JSON.stringify(op, null, 2)}`);
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
if (changedOps.length > 0) {
|
|
736
|
-
console.log(` Changed Operations:`);
|
|
737
|
-
for (const opName of changedOps) {
|
|
738
|
-
const originalOp = originalOpsMap.get(opName);
|
|
739
|
-
const transformedOp = transformedOpsMap.get(opName);
|
|
740
|
-
console.log(`- Original: ${JSON.stringify(originalOp, null, 2)}`);
|
|
741
|
-
console.log(`+ Transformed: ${JSON.stringify(transformedOp, null, 2)}`);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
// Show edge changes
|
|
745
|
-
const originalEdges = new Set(originalJob.jobGraph.edges);
|
|
746
|
-
const transformedEdges = new Set(transformedJob.jobGraph.edges);
|
|
747
|
-
const addedEdges = Array.from(transformedEdges).filter(edge => !originalEdges.has(edge));
|
|
748
|
-
const removedEdges = Array.from(originalEdges).filter(edge => !transformedEdges.has(edge));
|
|
749
|
-
if (addedEdges.length > 0) {
|
|
750
|
-
console.log(`\n Added Edges:`);
|
|
751
|
-
for (const edge of addedEdges) {
|
|
752
|
-
console.log(`+ ${edge}`);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
if (removedEdges.length > 0) {
|
|
756
|
-
console.log(`\n Removed Edges:`);
|
|
757
|
-
for (const edge of removedEdges) {
|
|
758
|
-
console.log(`- ${edge}`);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
console.log('\nOperations:');
|
|
762
|
-
}
|
|
763
|
-
console.log('\nTest Tag:');
|
|
764
|
-
console.log(` ${transformedJob.tags?.['grepr.test_run_id']}`);
|
|
765
|
-
console.log('\n=== TRANSFORMED JOB JSON ===\n');
|
|
766
|
-
}
|
|
767
|
-
//# sourceMappingURL=job-graph-transformer.js.map
|