Skip to content

Commit b32d763

Browse files
feat(semantic-conventions): update semantic conventions to v1.29.0 (#5356)
Co-authored-by: Jamie Danielson <[email protected]>
1 parent d803022 commit b32d763

File tree

8 files changed

+1485
-177
lines changed

8 files changed

+1485
-177
lines changed

scripts/semconv/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
opentelemetry-specification/
22
semantic-conventions/
3+
tmp-changelog-gen/

scripts/semconv/changelog-gen.js

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
#!/usr/bin/env node
2+
/**
3+
* A script to generate a meaningful changelog entry for an update in
4+
* semantic conventions version.
5+
*
6+
* Usage:
7+
* vi scripts/semconv/generate.sh # Typically update SPEC_VERSION to latest.
8+
* ./scripts/semconv/generate.sh # Re-generate the semconv package exports.
9+
* ./scripts/semconv/changelog-gen.js [aVer [bVer]]
10+
*
11+
* Include the text from this script in "semantic-conventions/CHANGELOG.md",
12+
* and perhaps also in the PR description.
13+
*
14+
* Arguments to the script:
15+
* - `aVer` is the base version of `@opentelemetry/semantic-conventions`
16+
* published to npm to which to compare, e.g. "1.27.0". This defaults to the
17+
* latest version published to npm.
18+
* - `bVer` is the version being compared against `aVer`. This defaults to
19+
* "local", which uses the local build in "../../semantic-conventions" in this
20+
* repo.
21+
*
22+
* Examples:
23+
* ./scripts/semconv/changelog-gen.js # compare the local build to the latest in npm
24+
* ./scripts/semconv/changelog-gen.js 1.27.0 1.28.0 # compare two versions in npm
25+
*/
26+
27+
const fs = require('fs');
28+
const path = require('path');
29+
const globSync = require('glob').sync;
30+
const {execSync} = require('child_process');
31+
const rimraf = require('rimraf');
32+
33+
const TOP = path.resolve(__dirname, '..', '..');
34+
const TMP_DIR = path.join(__dirname, 'tmp-changelog-gen');
35+
36+
/**
37+
* Convert a string to an HTML anchor string, as Markdown does for headers.
38+
*/
39+
function slugify(s) {
40+
const slug = s.trim().replace(/ /g, '-').replace(/[^\w-]/g, '')
41+
return slug;
42+
}
43+
44+
/**
45+
* Given some JS `src` (typically from OTel build/esnext/... output), return
46+
* whether the given export name `k` is marked `@deprecated`.
47+
*
48+
* Some of this parsing is shared with "contrib/scripts/gen-semconv-ts.js".
49+
*
50+
* @returns {boolean|string} `false` if not deprecated, a string deprecated
51+
* message if deprecated and the message could be determined, otherwise
52+
* `true` if marked deprecated.
53+
*/
54+
function isDeprecated(src, k) {
55+
const re = new RegExp(`^export const ${k} = .*;$`, 'm')
56+
const match = re.exec(src);
57+
if (!match) {
58+
throw new Error(`could not find the "${k}" export in semconv build/esnext/ source files`);
59+
}
60+
61+
// Find a preceding block comment, if any.
62+
const WHITESPACE_CHARS = [' ', '\t', '\n', '\r'];
63+
let idx = match.index - 1;
64+
while (idx >=1 && WHITESPACE_CHARS.includes(src[idx])) {
65+
idx--;
66+
}
67+
if (src.slice(idx-1, idx+1) !== '*/') {
68+
// There is not a block comment preceding the export.
69+
return false;
70+
}
71+
idx -= 2;
72+
while (idx >= 0) {
73+
if (src[idx] === '/' && src[idx+1] === '*') {
74+
// Found the start of the block comment.
75+
const blockComment = src.slice(idx, match.index);
76+
if (!blockComment.includes('@deprecated')) {
77+
return false;
78+
}
79+
const deprecatedMsgMatch = /^\s*\*\s*@deprecated\s+(.*)$/m.exec(blockComment);
80+
if (deprecatedMsgMatch) {
81+
return deprecatedMsgMatch[1];
82+
} else {
83+
return true;
84+
}
85+
}
86+
idx--;
87+
}
88+
return false;
89+
}
90+
91+
function summarizeChanges({prev, curr, prevSrc, currSrc}) {
92+
const prevNames = new Set(Object.keys(prev));
93+
const currNames = new Set(Object.keys(curr));
94+
const valChanged = (a, b) => {
95+
if (typeof a !== typeof b) {
96+
return true;
97+
} else if (typeof a === 'function') {
98+
return a.toString() !== b.toString();
99+
} else {
100+
return a !== b;
101+
}
102+
};
103+
const isNewlyDeprecated = (k) => {
104+
const isPrevDeprecated = prevNames.has(k) && isDeprecated(prevSrc, k);
105+
const isCurrDeprecated = currNames.has(k) && isDeprecated(currSrc, k);
106+
if (isPrevDeprecated && !isCurrDeprecated) {
107+
throw new Error(`semconv export '${k}' was *un*-deprecated in this release!? Wassup?`);
108+
}
109+
return (!isPrevDeprecated && isCurrDeprecated);
110+
};
111+
112+
// Determine changes.
113+
const changes = [];
114+
for (let k of Object.keys(curr)) {
115+
if (!prevNames.has(k)) {
116+
// 'ns' is the "namespace". The value here is wrong for "FEATURE_FLAG",
117+
// "GEN_AI", etc. But good enough for the usage below.
118+
const ns = /^(ATTR_|METRIC_|)?([^_]+)_/.exec(k)[2];
119+
changes.push({type: 'added', k, v: curr[k], ns});
120+
} else if (valChanged(curr[k], prev[k])) {
121+
changes.push({type: 'changed', k, v: curr[k], prevV: prev[k]});
122+
} else {
123+
const deprecatedResult = isNewlyDeprecated(k);
124+
if (deprecatedResult) {
125+
changes.push({type: 'deprecated', k, v: curr[k], deprecatedResult});
126+
}
127+
}
128+
}
129+
for (let k of Object.keys(prev)) {
130+
if (!currNames.has(k)) {
131+
changes.push({change: 'removed', k, prevV: prev[k]});
132+
}
133+
}
134+
135+
// Create a set of summaries, one for each change type.
136+
let haveChanges = changes.length > 0;
137+
const summaryFromChangeType = {
138+
removed: [],
139+
changed: [],
140+
deprecated: [],
141+
added: [],
142+
}
143+
const execSummaryFromChangeType = {
144+
removed: null,
145+
changed: null,
146+
deprecated: null,
147+
added: null,
148+
};
149+
150+
const removed = changes.filter(ch => ch.type === 'removed');
151+
let summary = summaryFromChangeType.removed;
152+
if (removed.length) {
153+
execSummaryFromChangeType.removed = `${removed.length} removed exports`;
154+
if (summary.length) { summary.push(''); }
155+
let last;
156+
const longest = removed.reduce((acc, ch) => Math.max(acc, ch.k.length), 0);
157+
removed.forEach(ch => {
158+
if (last && ch.ns !== last.ns) { summary.push(''); }
159+
const cindent = ' '.repeat(longest - ch.k.length + 1);
160+
161+
const prevVRepr = ch.prevV.includes('_VALUE_') ? JSON.stringify(ch.prevV) : ch.prevV;
162+
summary.push(`${ch.k}${cindent}// ${prevVRepr}`);
163+
164+
last = ch;
165+
});
166+
}
167+
168+
const changed = changes.filter(ch => ch.type === 'changed');
169+
summary = summaryFromChangeType.changed;
170+
if (changed.length) {
171+
execSummaryFromChangeType.changed = `${changed.length} exported values changed`;
172+
if (summary.length) { summary.push(''); }
173+
let last;
174+
const longest = changed.reduce((acc, ch) => Math.max(acc, ch.k.length), 0);
175+
changed.forEach(ch => {
176+
if (last && ch.ns !== last.ns) { summary.push(''); }
177+
const cindent = ' '.repeat(longest - ch.k.length + 1);
178+
179+
const prevVRepr = ch.k.includes('_VALUE_') ? JSON.stringify(ch.prevV) : ch.prevV;
180+
const vRepr = ch.k.includes('_VALUE_') ? JSON.stringify(ch.v) : ch.v;
181+
summary.push(`${ch.k}${cindent}// ${prevVRepr} -> ${vRepr}`);
182+
183+
last = ch;
184+
});
185+
}
186+
187+
const deprecated = changes.filter(ch => ch.type === 'deprecated');
188+
summary = summaryFromChangeType.deprecated;
189+
if (deprecated.length) {
190+
execSummaryFromChangeType.deprecated = `${deprecated.length} newly deprecated exports`;
191+
if (summary.length) { summary.push(''); }
192+
let last;
193+
const longest = deprecated.reduce((acc, ch) => Math.max(acc, ch.k.length), 0);
194+
deprecated.forEach(ch => {
195+
if (last && ch.ns !== last.ns) { summary.push(''); }
196+
const cindent = ' '.repeat(longest - ch.k.length + 1);
197+
198+
if (typeof ch.deprecatedResult === 'string') {
199+
summary.push(`${ch.k}${cindent}// ${ch.v}: ${ch.deprecatedResult}`);
200+
} else {
201+
summary.push(ch.k)
202+
}
203+
204+
last = ch;
205+
});
206+
}
207+
208+
const added = changes.filter(ch => ch.type === 'added');
209+
summary = summaryFromChangeType.added;
210+
if (added.length) {
211+
execSummaryFromChangeType.added = `${added.length} added exports`;
212+
let last, lastAttr;
213+
const longest = added.reduce((acc, ch) => Math.max(acc, ch.k.length), 0);
214+
added.forEach(ch => {
215+
if (last && ch.ns !== last.ns) { summary.push(''); }
216+
let indent = '';
217+
if (lastAttr && ch.k.startsWith(lastAttr.k.slice('ATTR_'.length))) {
218+
indent = ' ';
219+
}
220+
const cindent = ' '.repeat(longest - ch.k.length + 1);
221+
222+
const vRepr = ch.k.includes('_VALUE_') ? JSON.stringify(ch.v) : ch.v
223+
summary.push(`${indent}${ch.k}${cindent}// ${vRepr}`);
224+
225+
last = ch;
226+
if (ch.k.startsWith('ATTR_')) {
227+
lastAttr = ch;
228+
}
229+
});
230+
}
231+
232+
return {
233+
haveChanges,
234+
execSummaryFromChangeType,
235+
summaryFromChangeType
236+
};
237+
}
238+
239+
240+
function semconvChangelogGen(aVer=undefined, bVer=undefined) {
241+
242+
console.log(`Creating tmp working dir "${TMP_DIR}"`);
243+
rimraf.sync(TMP_DIR);
244+
fs.mkdirSync(TMP_DIR);
245+
246+
const localDir = path.join(TOP, 'semantic-conventions');
247+
const pj = JSON.parse(fs.readFileSync(path.join(localDir, 'package.json')));
248+
const pkgInfo = JSON.parse(execSync(`npm info -j ${pj.name}`))
249+
250+
let aDir;
251+
if (!aVer) {
252+
aVer = pkgInfo.version; // By default compare to latest published version.
253+
}
254+
aDir = path.join(TMP_DIR, aVer, 'package');
255+
if (!fs.existsSync(aDir)) {
256+
console.log(`Downloading and extracting @opentelemetry/semantic-conventions@${aVer}`)
257+
const tarballUrl = `https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-${aVer}.tgz`;
258+
fs.mkdirSync(path.dirname(aDir));
259+
const cwd = path.dirname(aDir);
260+
execSync(`curl -sf -o package.tgz ${tarballUrl}`, { cwd });
261+
execSync(`tar xzf package.tgz`, { cwd });
262+
}
263+
264+
let bDir, bSemconvVer;
265+
if (!bVer) {
266+
bVer = 'local' // By default comparison target is the local build.
267+
bDir = localDir;
268+
269+
// Determine target spec ver.
270+
const generateShPath = path.join(__dirname, 'generate.sh');
271+
const specVerRe = /^SPEC_VERSION=(.*)$/m;
272+
const specVerMatch = specVerRe.exec(fs.readFileSync(generateShPath));
273+
if (!specVerMatch) {
274+
throw new Error(`could not determine current semconv SPEC_VERSION: ${specVerRe} did not match in ${generateShPath}`);
275+
}
276+
bSemconvVer = specVerMatch[1].trim();
277+
console.log('Target Semantic Conventions ver is:', bSemconvVer);
278+
} else {
279+
bSemconvVer = 'v' + bVer;
280+
bDir = path.join(TMP_DIR, bVer, 'package');
281+
console.log(`Downloading and extracting @opentelemetry/semantic-conventions@${bVer}`)
282+
const tarballUrl = `https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-${bVer}.tgz`;
283+
fs.mkdirSync(path.dirname(bDir));
284+
const cwd = path.dirname(bDir);
285+
execSync(`curl -sf -o package.tgz ${tarballUrl}`, { cwd });
286+
execSync(`tar xzf package.tgz`, { cwd });
287+
}
288+
289+
console.log(`Comparing exports between versions ${aVer} and ${bVer}`)
290+
const stableChInfo = summarizeChanges({
291+
// require('.../build/src/stable_*.js') from previous and current.
292+
prev: Object.assign(...globSync(path.join(aDir, 'build/src/stable_*.js')).map(require)),
293+
curr: Object.assign(...globSync(path.join(bDir, 'build/src/stable_*.js')).map(require)),
294+
// Load '.../build/esnext/stable_*.js' sources to use for parsing jsdoc comments.
295+
prevSrc: globSync(path.join(aDir, 'build/esnext/stable_*.js'))
296+
.map(f => fs.readFileSync(f, 'utf8'))
297+
.join('\n\n'),
298+
currSrc: globSync(path.join(bDir, 'build/esnext/stable_*.js'))
299+
.map(f => fs.readFileSync(f, 'utf8'))
300+
.join('\n\n'),
301+
});
302+
const unstableChInfo = summarizeChanges({
303+
prev: Object.assign(...globSync(path.join(aDir, 'build/src/experimental_*.js')).map(require)),
304+
curr: Object.assign(...globSync(path.join(bDir, 'build/src/experimental_*.js')).map(require)),
305+
prevSrc: globSync(path.join(aDir, 'build/esnext/experimental_*.js'))
306+
.map(f => fs.readFileSync(f, 'utf8'))
307+
.join('\n\n'),
308+
currSrc: globSync(path.join(bDir, 'build/esnext/experimental_*.js'))
309+
.map(f => fs.readFileSync(f, 'utf8'))
310+
.join('\n\n'),
311+
});
312+
313+
// Render the "change info" into a Markdown summary for the changelog.
314+
const changeTypes = ['removed', 'changed', 'deprecated', 'added'];
315+
let execSummaryFromChInfo = (chInfo) => {
316+
const parts = changeTypes
317+
.map(chType => chInfo.execSummaryFromChangeType[chType])
318+
.filter(s => typeof(s) === 'string');
319+
if (parts.length) {
320+
return parts.join(', ');
321+
} else {
322+
return 'none';
323+
}
324+
}
325+
const changelogEntry = [`
326+
* feat: update semantic conventions to ${bSemconvVer} [#NNNN]
327+
* Semantic Conventions ${bSemconvVer}:
328+
[changelog](https://github.com/open-telemetry/semantic-conventions/blob/main/CHANGELOG.md#${slugify(bSemconvVer)}) |
329+
[latest docs](https://opentelemetry.io/docs/specs/semconv/)
330+
* \`@opentelemetry/semantic-conventions\` (stable) changes: *${execSummaryFromChInfo(stableChInfo)}*
331+
* \`@opentelemetry/semantic-conventions/incubating\` (unstable) changes: *${execSummaryFromChInfo(unstableChInfo)}*
332+
`];
333+
334+
if (stableChInfo.haveChanges) {
335+
changelogEntry.push(`#### Stable changes in ${bSemconvVer}\n`);
336+
for (let changeType of changeTypes) {
337+
const summary = stableChInfo.summaryFromChangeType[changeType];
338+
if (summary.length) {
339+
changelogEntry.push(`<details open>
340+
<summary>${stableChInfo.execSummaryFromChangeType[changeType]}</summary>
341+
342+
\`\`\`js
343+
${summary.join('\n')}
344+
\`\`\`
345+
346+
</details>
347+
`);
348+
}
349+
}
350+
}
351+
352+
if (unstableChInfo.haveChanges) {
353+
changelogEntry.push(`#### Unstable changes in ${bSemconvVer}\n`);
354+
for (let changeType of changeTypes) {
355+
const summary = unstableChInfo.summaryFromChangeType[changeType];
356+
if (summary.length) {
357+
changelogEntry.push(`<details>
358+
<summary>${unstableChInfo.execSummaryFromChangeType[changeType]}</summary>
359+
360+
\`\`\`js
361+
${summary.join('\n')}
362+
\`\`\`
363+
364+
</details>
365+
`);
366+
}
367+
}
368+
}
369+
370+
return changelogEntry.join('\n');
371+
}
372+
373+
function main() {
374+
const [aVer, bVer] = process.argv.slice(2);
375+
const s = semconvChangelogGen(aVer, bVer);
376+
console.log('The following could be added to the top "Enhancement" section of "semantic-conventions/CHANGELOG.md":');
377+
console.log('\n- - -');
378+
console.log(s)
379+
console.log('- - -');
380+
}
381+
382+
main();

0 commit comments

Comments
 (0)