1
- import { fileURLToPath } from 'url'
1
+ import { createRequire } from 'module'
2
+ import { fileURLToPath , pathToFileURL } from 'url'
2
3
3
- import { execaNode } from 'execa'
4
+ import { trace } from '@opentelemetry/api'
5
+ import { ExecaChildProcess , execaNode } from 'execa'
6
+ import { gte } from 'semver'
4
7
5
8
import { addErrorInfo } from '../error/info.js'
9
+ import { NetlifyConfig } from '../index.js'
10
+ import { BufferedLogs } from '../log/logger.js'
6
11
import {
7
- logRuntime ,
8
- logLoadingPlugins ,
9
- logOutdatedPlugins ,
10
12
logIncompatiblePlugins ,
11
13
logLoadingIntegration ,
14
+ logLoadingPlugins ,
15
+ logOutdatedPlugins ,
16
+ logRuntime ,
12
17
} from '../log/messages/compatibility.js'
13
18
import { isTrustedPlugin } from '../steps/plugin.js'
14
19
import { measureDuration } from '../time/main.js'
15
20
16
- import { getEventFromChild } from './ipc.js'
21
+ import { callChild , getEventFromChild } from './ipc.js'
22
+ import { PluginsOptions } from './node_version.js'
17
23
import { getSpawnInfo } from './options.js'
18
24
19
25
const CHILD_MAIN_FILE = fileURLToPath ( new URL ( 'child/main.js' , import . meta. url ) )
20
26
27
+ const require = createRequire ( import . meta. url )
28
+
21
29
// Start child processes used by all plugins
22
30
// We fire plugins through child processes so that:
23
31
// - each plugin is sandboxed, e.g. cannot access/modify its parent `process`
@@ -35,25 +43,72 @@ const tStartPlugins = async function ({ pluginsOptions, buildDir, childEnv, logs
35
43
logIncompatiblePlugins ( logs , pluginsOptions )
36
44
37
45
const childProcesses = await Promise . all (
38
- pluginsOptions . map ( ( { pluginDir, nodePath, pluginPackageJson } ) =>
39
- startPlugin ( { pluginDir, nodePath, buildDir, childEnv, systemLogFile, pluginPackageJson } ) ,
46
+ pluginsOptions . map ( ( { pluginDir, nodePath, nodeVersion , pluginPackageJson } ) =>
47
+ startPlugin ( { pluginDir, nodePath, nodeVersion , buildDir, childEnv, systemLogFile, pluginPackageJson } ) ,
40
48
) ,
41
49
)
42
50
return { childProcesses }
43
51
}
44
52
45
53
export const startPlugins = measureDuration ( tStartPlugins , 'start_plugins' )
46
54
47
- const startPlugin = async function ( { pluginDir, nodePath, buildDir, childEnv, systemLogFile, pluginPackageJson } ) {
48
- const childProcess = execaNode ( CHILD_MAIN_FILE , [ ] , {
55
+ const startPlugin = async function ( {
56
+ pluginDir,
57
+ nodeVersion,
58
+ nodePath,
59
+ buildDir,
60
+ childEnv,
61
+ systemLogFile,
62
+ pluginPackageJson,
63
+ } : {
64
+ nodeVersion : string
65
+ nodePath : string
66
+ pluginDir : string
67
+ /** The process cwd that is used to spawn the child process */
68
+ buildDir : string
69
+ childEnv : Record < string , string >
70
+ pluginPackageJson : Record < string , string >
71
+ systemLogFile : number
72
+ } ) {
73
+ const ctx = trace . getActiveSpan ( ) ?. spanContext ( )
74
+
75
+ // the baggage will be passed to the child process when sending the run event
76
+ const args = [
77
+ ...process . argv . filter ( ( arg ) => arg . startsWith ( '--tracing' ) ) ,
78
+ `--tracing.traceId=${ ctx ?. traceId } ` ,
79
+ `--tracing.parentSpanId=${ ctx ?. spanId } ` ,
80
+ `--tracing.traceFlags=${ ctx ?. traceFlags } ` ,
81
+ `--tracing.enabled=${ ! ! isTrustedPlugin ( pluginPackageJson ?. name ) } ` ,
82
+ ]
83
+
84
+ const nodeOptions : string [ ] = [ ]
85
+
86
+ // the sdk setup is a optional dependency that might not exist
87
+ // only use it if it exists
88
+ try {
89
+ // the --import preloading is only available in node 18.18.0 and above
90
+ // plugins that run on a lower node version will not be able to be instrumented with opentelemetry
91
+ if ( gte ( nodeVersion , '18.18.0' ) ) {
92
+ const entry = require . resolve ( '@netlify/opentelemetry-sdk-setup/bin.js' )
93
+ // on windows only file:// urls are allowed
94
+ nodeOptions . push ( '--import' , pathToFileURL ( entry ) . toString ( ) )
95
+ }
96
+ } catch {
97
+ // noop
98
+ }
99
+
100
+ const childProcess = execaNode ( CHILD_MAIN_FILE , args , {
49
101
cwd : buildDir ,
50
102
preferLocal : true ,
51
103
localDir : pluginDir ,
52
104
nodePath,
53
- // make sure we don't pass build's node cli properties for now (e.g. --import)
54
- nodeOptions : [ ] ,
105
+ nodeOptions,
55
106
execPath : nodePath ,
56
- env : childEnv ,
107
+ env : {
108
+ ...childEnv ,
109
+ OTEL_SERVICE_NAME : pluginPackageJson ?. name ,
110
+ OTEL_SERVICE_VERSION : pluginPackageJson ?. version ,
111
+ } ,
57
112
extendEnv : false ,
58
113
stdio :
59
114
isTrustedPlugin ( pluginPackageJson ?. name ) && systemLogFile
@@ -72,14 +127,56 @@ const startPlugin = async function ({ pluginDir, nodePath, buildDir, childEnv, s
72
127
}
73
128
74
129
// Stop all plugins child processes
75
- export const stopPlugins = function ( childProcesses ) {
76
- childProcesses . forEach ( stopPlugin )
130
+ export const stopPlugins = async function ( {
131
+ childProcesses,
132
+ logs,
133
+ verbose,
134
+ pluginOptions,
135
+ netlifyConfig,
136
+ } : {
137
+ logs : BufferedLogs
138
+ verbose : boolean
139
+ childProcesses : { childProcess : ExecaChildProcess } [ ]
140
+ pluginOptions : PluginsOptions [ ]
141
+ netlifyConfig : NetlifyConfig
142
+ } ) {
143
+ await Promise . all (
144
+ childProcesses . map ( ( { childProcess } , index ) => {
145
+ return stopPlugin ( { childProcess, verbose, logs, pluginOptions : pluginOptions [ index ] , netlifyConfig } )
146
+ } ) ,
147
+ )
77
148
}
78
149
79
- const stopPlugin = function ( { childProcess } ) {
150
+ const stopPlugin = async function ( {
151
+ childProcess,
152
+ logs,
153
+ pluginOptions : { packageName, inputs, pluginPath, pluginPackageJson : packageJson = { } } ,
154
+ netlifyConfig,
155
+ verbose,
156
+ } : {
157
+ childProcess : ExecaChildProcess
158
+ pluginOptions : PluginsOptions
159
+ netlifyConfig : NetlifyConfig
160
+ verbose : boolean
161
+ logs : BufferedLogs
162
+ } ) {
80
163
if ( childProcess . connected ) {
164
+ // reliable stop tracing inside child processes
165
+ await callChild ( {
166
+ childProcess,
167
+ eventName : 'shutdown' ,
168
+ payload : {
169
+ packageName,
170
+ pluginPath,
171
+ inputs,
172
+ packageJson,
173
+ verbose,
174
+ netlifyConfig,
175
+ } ,
176
+ logs,
177
+ verbose,
178
+ } )
81
179
childProcess . disconnect ( )
82
180
}
83
-
84
181
childProcess . kill ( )
85
182
}
0 commit comments