Skip to content

Commit 4dede11

Browse files
committed
chore: add scripts/gen-semconv-ts.js to help generate semconv.ts files for unstable semconv consts
Refs: https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv
1 parent 4fb610d commit 4dede11

File tree

1 file changed

+217
-0
lines changed

1 file changed

+217
-0
lines changed

scripts/gen-semconv-ts.js

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
#!/usr/bin/env node
2+
/*
3+
* Copyright The OpenTelemetry Authors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* Generate a "src/semconv.ts" in the current workspace dir, which includes
20+
* copies of the unstable semconv definitions used in this package.
21+
*
22+
* This is to support the recommendation from
23+
* https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv
24+
* that any usage of unstable Semantic Conventions definitions should be done
25+
* by *copying* those definitions to a local file in this package.
26+
*
27+
* Usage:
28+
* node scripts/gen-semconv-ts.js [WORKSPACE-DIR]
29+
*/
30+
31+
const fs = require('fs');
32+
const { execSync } = require('child_process');
33+
const path = require('path');
34+
const { globSync } = require('glob');
35+
36+
const USE_COLOR = process.stdout.isTTY && !process.env.NO_COLOR?.length > 0;
37+
38+
let numWarns = 0;
39+
function warn(...args) {
40+
numWarns += 1;
41+
if (USE_COLOR) {
42+
process.stdout.write('\x1b[31m');
43+
}
44+
process.stdout.write('gen-semconv-ts warning: ');
45+
if (USE_COLOR) {
46+
process.stdout.write('\x1b[39m');
47+
}
48+
console.log(...args);
49+
}
50+
51+
function getAllWorkspaceDirs() {
52+
const pj = JSON.parse(
53+
fs.readFileSync(path.join(TOP, 'package.json'), 'utf8')
54+
);
55+
return pj.workspaces
56+
.map((wsGlob) => globSync(path.join(wsGlob, 'package.json')))
57+
.flat()
58+
.map(path.dirname);
59+
}
60+
61+
function lintSemconvDeps() {
62+
const wsDirs = getAllWorkspaceDirs();
63+
64+
for (let wsDir of wsDirs) {
65+
const pj = JSON.parse(
66+
fs.readFileSync(path.join(wsDir, 'package.json'), 'utf8')
67+
);
68+
const depRange = pj?.dependencies?.[SEMCONV];
69+
const devDepRange = pj?.devDependencies?.[SEMCONV];
70+
if (!(depRange || devDepRange)) {
71+
continue;
72+
}
73+
74+
// Rule: The semconv dep should *not* be pinned. Expect `^X.Y.Z`.
75+
const pinnedVerRe = /^\d+\.\d+\.\d+$/;
76+
if (depRange && pinnedVerRe.exec(depRange)) {
77+
problem(`${wsDir}/package.json: package ${pj.name} pins "${SEMCONV}" in dependencies, but should not (see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#why-not-pin-the-version)`);
78+
} else if (devDepRange && pinnedVerRe.exec(devDepRange)) {
79+
problem(`${wsDir}/package.json: package ${pj.name} pins "${SEMCONV}" in devDependencies, but should not (see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#why-not-pin-the-version)`);
80+
}
81+
82+
// Rule: The incubating entry-point should not be used.
83+
const srcFiles = globSync(path.join(wsDir, 'src', '**', '*.ts'));
84+
const usesIncubatingRe = /import\s+\{?[^{;]*\s+from\s+'@opentelemetry\/semantic-conventions\/incubating'/s;
85+
for (let srcFile of srcFiles) {
86+
const srcText = fs.readFileSync(srcFile, 'utf8');
87+
const match = usesIncubatingRe.exec(srcText);
88+
if (match) {
89+
problem(`${srcFile}: uses the 'incubating' entry-point from '@opentelemetry/semantic-conventions', but should not (see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv)`)
90+
}
91+
}
92+
}
93+
}
94+
95+
function genSemconvTs(wsDir) {
96+
const semconvPath = require.resolve('@opentelemetry/semantic-conventions',
97+
{paths: [path.join(wsDir, 'node_modules')]});
98+
const semconvStable = require(semconvPath);
99+
100+
// Gather unstable semconv imports. Consider any imports from
101+
// '@opentelemetry/semantic-conventions/incubating' or from an existing local
102+
// '.../semconv'.
103+
const srcFiles = globSync(path.join(wsDir, '{src,test}', '**', '*.ts'));
104+
const importRes = [
105+
/import\s+{([^}]*)}\s+from\s+'@opentelemetry\/semantic-conventions\/incubating'/s,
106+
/import\s+{([^}]*)}\s+from\s+'\.[^']*\/semconv'/s,
107+
];
108+
const names = new Set();
109+
for (const srcFile of srcFiles) {
110+
const srcText = fs.readFileSync(srcFile, 'utf8');
111+
for (const importRe of importRes) {
112+
const match = importRe.exec(srcText);
113+
if (match) {
114+
match[1].trim().split(/,/g).forEach(n => {
115+
n = n.trim();
116+
if (n) {
117+
if (semconvStable[n]) {
118+
warn(`${wsDir}/${srcFile}: '${n}' export is available on the stable "@opentelemetry/semantic-conventions" entry-point. This definition will not be included in the generated semconv.ts. Instead use:\n import { ${n} } from '@opentelemetry/semantic-conventions';`)
119+
} else {
120+
names.add(n);
121+
}
122+
}
123+
});
124+
}
125+
}
126+
}
127+
if (names.size === 0) {
128+
console.log(`Did not find any usage of unstable semconv exports in "${wsDir}/{src,test}/**/*.ts".`)
129+
} else {
130+
console.log(`Found import of ${names.size} unstable semconv definitions.`)
131+
}
132+
133+
// Load the source from the semconv package from which we'll copy.
134+
// We are cheating a bit in knowing the semconv package structure. We want
135+
// "build/esnext/experimental_*.js" files. Use the "esnext" build because it
136+
// is closed to the TypeScript we want.
137+
const semconvEsnextDir = path.resolve(
138+
semconvPath,
139+
'../../esnext', // .../build/src/index.js -> .../build/esnext
140+
);
141+
const srcPaths = globSync(path.join(semconvEsnextDir, 'experimental_*.js'));
142+
const src = srcPaths
143+
.map(f => fs.readFileSync(f))
144+
.join('\n\n');
145+
146+
const sortedNames = Array.from(names).sort();
147+
const chunks = [];
148+
for (let name of sortedNames) {
149+
const re = new RegExp(`^export const ${name} = .*;$`, 'm')
150+
const match = re.exec(src);
151+
if (!match) {
152+
throw new Error(`could not find "${name}" export in semconv build files: ${re} did not match in content from ${srcPaths.join(', ')}`);
153+
}
154+
155+
// Find a preceding block comment, if any.
156+
const WHITESPACE_CHARS = [' ', '\t', '\n', '\r'];
157+
let idx = match.index - 1;
158+
while (idx >=1 && WHITESPACE_CHARS.includes(src[idx])) {
159+
idx--;
160+
}
161+
if (src.slice(idx-1, idx+1) !== '*/') {
162+
// There is not a block comment preceding the export.
163+
chunks.push(match[0]);
164+
continue;
165+
}
166+
idx -= 2;
167+
while (idx >= 0) {
168+
if (src[idx] === '/' && src[idx+1] === '*') {
169+
// Found the start of the block comment.
170+
chunks.push(src.slice(idx, match.index) + match[0]);
171+
break;
172+
}
173+
idx--;
174+
}
175+
}
176+
177+
const semconvTsPath = path.join(wsDir, 'src', 'semconv.ts');
178+
fs.writeFileSync(
179+
semconvTsPath,
180+
`/*
181+
* Copyright The OpenTelemetry Authors
182+
*
183+
* Licensed under the Apache License, Version 2.0 (the "License");
184+
* you may not use this file except in compliance with the License.
185+
* You may obtain a copy of the License at
186+
*
187+
* https://www.apache.org/licenses/LICENSE-2.0
188+
*
189+
* Unless required by applicable law or agreed to in writing, software
190+
* distributed under the License is distributed on an "AS IS" BASIS,
191+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
192+
* See the License for the specific language governing permissions and
193+
* limitations under the License.
194+
*/
195+
196+
/*
197+
* This file contains a copy of unstable semantic convention definitions
198+
* used by this package.
199+
* @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv
200+
*/
201+
202+
${chunks.join('\n\n')}
203+
`,
204+
{ encoding: 'utf8' }
205+
);
206+
console.log(`Generated "${semconvTsPath}".`);
207+
208+
console.log(`Running 'npx eslint --fix src/semconv.ts' to fix formatting.`);
209+
execSync(`npx eslint --fix src/semconv.ts`, { cwd: wsDir });
210+
}
211+
212+
// mainline
213+
const wsDir = process.argv[2] || '.';
214+
genSemconvTs(wsDir);
215+
if (numWarns > 0) {
216+
process.exitCode = 1;
217+
}

0 commit comments

Comments
 (0)