Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(semantic-conventions): update semantic conventions to v1.29.0 #5356

Merged
merged 11 commits into from
Feb 6, 2025
1 change: 1 addition & 0 deletions scripts/semconv/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
opentelemetry-specification/
semantic-conventions/
tmp-changelog-gen/
377 changes: 377 additions & 0 deletions scripts/semconv/changelog-gen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
#!/usr/bin/env node
/**
* A script to generate a meaningful changelog entry for an update in
* semantic conventions version.
*
* Usage:
* vi scripts/semconv/generate.sh # Typically update SPEC_VERSION to latest.
* ./scripts/semconv/generate.sh # Re-generate the semconv package exports.
* ./scripts/semconv/changelog-gen.sh [aVer [bVer]]
trentm marked this conversation as resolved.
Show resolved Hide resolved
*
* where:
* - `aVer` is the base version of `@opentelemetry/semantic-conventions` to
* which to compare, e.g. "1.28.0". This defaults to the latest version
* published to npm.
* - `bVer` is the version being compared against `aVer`. This defaults to
JamieDanielson marked this conversation as resolved.
Show resolved Hide resolved
* "local", which uses the local build in ../../semantic-conventions in this
* repo.
*
* The last command (this script) will output a text block that can be used
* in "semantic-conventions/CHANGELOG.md" (and also perhaps in the PR
* desciption).
trentm marked this conversation as resolved.
Show resolved Hide resolved
*/

const fs = require('fs');
const path = require('path');
const globSync = require('glob').sync;
const {execSync} = require('child_process');
const rimraf = require('rimraf');

const TOP = path.resolve(__dirname, '..', '..');
const TMP_DIR = path.join(__dirname, 'tmp-changelog-gen');

/**
* Convert a string to an HTML anchor string, as Markdown does for headers.
*/
function slugify(s) {
const slug = s.trim().replace(/ /g, '-').replace(/[^\w-]/g, '')
return slug;
}

/**
* Given some JS `src` (typically from OTel build/esnext/... output), return
* whether the given export name `k` is marked `@deprecated`.
*
* Some of this parsing is shared with "contrib/scripts/gen-semconv-ts.js".
*
* @returns {boolean|string} `false` if not deprecated, a string deprecated
* message if deprecated and the message could be determined, otherwise
* `true` if marked deprecated.
*/
function isDeprecated(src, k) {
const re = new RegExp(`^export const ${k} = .*;$`, 'm')
const match = re.exec(src);
if (!match) {
throw new Error(`could not find the "${k}" export in semconv build/esnext/ source files`);
}

// Find a preceding block comment, if any.
const WHITESPACE_CHARS = [' ', '\t', '\n', '\r'];
let idx = match.index - 1;
while (idx >=1 && WHITESPACE_CHARS.includes(src[idx])) {
idx--;
}
if (src.slice(idx-1, idx+1) !== '*/') {
// There is not a block comment preceding the export.
return false;
}
idx -= 2;
while (idx >= 0) {
if (src[idx] === '/' && src[idx+1] === '*') {
// Found the start of the block comment.
const blockComment = src.slice(idx, match.index);
if (!blockComment.includes('@deprecated')) {
return false;
}
const deprecatedMsgMatch = /^\s*\*\s*@deprecated\s+(.*)$/m.exec(blockComment);
if (deprecatedMsgMatch) {
return deprecatedMsgMatch[1];
} else {
return true;
}
}
idx--;
}
return false;
}

function summarizeChanges({prev, curr, prevSrc, currSrc}) {
const prevNames = new Set(Object.keys(prev));
const currNames = new Set(Object.keys(curr));
const valChanged = (a, b) => {
if (typeof a !== typeof b) {
return true;
} else if (typeof a === 'function') {
return a.toString() !== b.toString();
} else {
return a !== b;
}
};
const isNewlyDeprecated = (k) => {
const isPrevDeprecated = prevNames.has(k) && isDeprecated(prevSrc, k);
const isCurrDeprecated = currNames.has(k) && isDeprecated(currSrc, k);
if (isPrevDeprecated && !isCurrDeprecated) {
throw new Error(`semconv export '${k}' was *un*-deprecated in this release!? Wassup?`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

semconv export '${k}' was *un*-deprecated in this release!? Wassup?

😄

}
return (!isPrevDeprecated && isCurrDeprecated);
};

// Determine changes.
const changes = [];
for (let k of Object.keys(curr)) {
if (!prevNames.has(k)) {
// 'ns' is the "namespace". The value here is wrong for "FEATURE_FLAG",
// "GEN_AI", etc. But good enough for the usage below.
const ns = /^(ATTR_|METRIC_|)?([^_]+)_/.exec(k)[2];
changes.push({type: 'added', k, v: curr[k], ns});
} else if (valChanged(curr[k], prev[k])) {
changes.push({type: 'changed', k, v: curr[k], prevV: prev[k]});
} else {
const deprecatedResult = isNewlyDeprecated(k);
if (deprecatedResult) {
changes.push({type: 'deprecated', k, v: curr[k], deprecatedResult});
}
}
}
for (let k of Object.keys(prev)) {
if (!currNames.has(k)) {
changes.push({change: 'removed', k, prevV: prev[k]});
}
}

// Create a set of summaries, one for each change type.
let haveChanges = changes.length > 0;
const summaryFromChangeType = {
removed: [],
changed: [],
deprecated: [],
added: [],
}
const execSummaryFromChangeType = {
removed: null,
changed: null,
deprecated: null,
added: null,
};

const removed = changes.filter(ch => ch.type === 'removed');
let summary = summaryFromChangeType.removed;
if (removed.length) {
execSummaryFromChangeType.removed = `${removed.length} removed exports`;
if (summary.length) { summary.push(''); }
let last;
const longest = removed.reduce((acc, ch) => Math.max(acc, ch.k.length), 0);
removed.forEach(ch => {
if (last && ch.ns !== last.ns) { summary.push(''); }
const cindent = ' '.repeat(longest - ch.k.length + 1);

const prevVRepr = ch.prevV.includes('_VALUE_') ? JSON.stringify(ch.prevV) : ch.prevV;
summary.push(`${ch.k}${cindent}// ${prevVRepr}`);

last = ch;
});
}

const changed = changes.filter(ch => ch.type === 'changed');
summary = summaryFromChangeType.changed;
if (changed.length) {
execSummaryFromChangeType.changed = `${changed.length} exported values changed`;
if (summary.length) { summary.push(''); }
let last;
const longest = changed.reduce((acc, ch) => Math.max(acc, ch.k.length), 0);
changed.forEach(ch => {
if (last && ch.ns !== last.ns) { summary.push(''); }
const cindent = ' '.repeat(longest - ch.k.length + 1);

const prevVRepr = ch.k.includes('_VALUE_') ? JSON.stringify(ch.prevV) : ch.prevV;
const vRepr = ch.k.includes('_VALUE_') ? JSON.stringify(ch.v) : ch.v;
summary.push(`${ch.k}${cindent}// ${prevVRepr} -> ${vRepr}`);

last = ch;
});
}

const deprecated = changes.filter(ch => ch.type === 'deprecated');
summary = summaryFromChangeType.deprecated;
if (deprecated.length) {
execSummaryFromChangeType.deprecated = `${deprecated.length} newly deprecated exports`;
if (summary.length) { summary.push(''); }
let last;
const longest = deprecated.reduce((acc, ch) => Math.max(acc, ch.k.length), 0);
deprecated.forEach(ch => {
if (last && ch.ns !== last.ns) { summary.push(''); }
const cindent = ' '.repeat(longest - ch.k.length + 1);

if (typeof ch.deprecatedResult === 'string') {
summary.push(`${ch.k}${cindent}// ${ch.v}: ${ch.deprecatedResult}`);
} else {
summary.push(ch.k)
}

last = ch;
});
}

const added = changes.filter(ch => ch.type === 'added');
summary = summaryFromChangeType.added;
if (added.length) {
execSummaryFromChangeType.added = `${added.length} added exports`;
let last, lastAttr;
const longest = added.reduce((acc, ch) => Math.max(acc, ch.k.length), 0);
added.forEach(ch => {
if (last && ch.ns !== last.ns) { summary.push(''); }
let indent = '';
if (lastAttr && ch.k.startsWith(lastAttr.k.slice('ATTR_'.length))) {
indent = ' ';
}
const cindent = ' '.repeat(longest - ch.k.length + 1);

const vRepr = ch.k.includes('_VALUE_') ? JSON.stringify(ch.v) : ch.v
summary.push(`${indent}${ch.k}${cindent}// ${vRepr}`);

last = ch;
if (ch.k.startsWith('ATTR_')) {
lastAttr = ch;
}
});
}

return {
haveChanges,
execSummaryFromChangeType,
summaryFromChangeType
};
}


function semconvChangelogGen(aVer=undefined, bVer=undefined) {

console.log(`Creating tmp working dir "${TMP_DIR}"`);
rimraf.sync(TMP_DIR);
fs.mkdirSync(TMP_DIR);

const localDir = path.join(TOP, 'semantic-conventions');
const pj = JSON.parse(fs.readFileSync(path.join(localDir, 'package.json')));
const pkgInfo = JSON.parse(execSync(`npm info -j ${pj.name}`))

let aDir;
if (!aVer) {
aVer = pkgInfo.version; // By default compare to latest published version.
}
aDir = path.join(TMP_DIR, aVer, 'package');
if (!fs.existsSync(aDir)) {
console.log(`Downloading and extracting @opentelemetry/semantic-conventions@${aVer}`)
const tarballUrl = `https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-${aVer}.tgz`;
fs.mkdirSync(path.dirname(aDir));
execSync(`curl -sf -o - ${tarballUrl} | tar xzf -`,
{ cwd: path.dirname(aDir) });
}

let bDir, bSemconvVer;
if (!bVer) {
bVer = 'local' // By default comparison target is the local build.
bDir = localDir;

// Determine target spec ver.
const generateShPath = path.join(__dirname, 'generate.sh');
const specVerRe = /^SPEC_VERSION=(.*)$/m;
const specVerMatch = specVerRe.exec(fs.readFileSync(generateShPath));
if (!specVerMatch) {
throw new Error(`could not determine current semconv SPEC_VERSION: ${specVerRe} did not match in ${generateShPath}`);
}
bSemconvVer = specVerMatch[1].trim();
console.log('Target Semantic Conventions ver is:', bSemconvVer);
} else {
bSemconvVer = 'v' + bVer;
bDir = path.join(TMP_DIR, bVer, 'package');
console.log(`Downloading and extracting @opentelemetry/semantic-conventions@${bVer}`)
const tarballUrl = `https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-${bVer}.tgz`;
fs.mkdirSync(path.dirname(bDir));
execSync(`curl -sf -o - ${tarballUrl} | tar xzf -`,
{ cwd: path.dirname(bDir) });
}

console.log(`Comparing exports between versions ${aVer} and ${bVer}`)
const stableChInfo = summarizeChanges({
// require('.../build/src/stable_*.js') from previous and current.
prev: Object.assign(...globSync(path.join(aDir, 'build/src/stable_*.js')).map(require)),
curr: Object.assign(...globSync(path.join(bDir, 'build/src/stable_*.js')).map(require)),
// Load '.../build/esnext/stable_*.js' sources to use for parsing jsdoc comments.
prevSrc: globSync(path.join(aDir, 'build/esnext/stable_*.js'))
.map(f => fs.readFileSync(f, 'utf8'))
.join('\n\n'),
currSrc: globSync(path.join(bDir, 'build/esnext/stable_*.js'))
.map(f => fs.readFileSync(f, 'utf8'))
.join('\n\n'),
});
const unstableChInfo = summarizeChanges({
prev: Object.assign(...globSync(path.join(aDir, 'build/src/experimental_*.js')).map(require)),
curr: Object.assign(...globSync(path.join(bDir, 'build/src/experimental_*.js')).map(require)),
prevSrc: globSync(path.join(aDir, 'build/esnext/experimental_*.js'))
.map(f => fs.readFileSync(f, 'utf8'))
.join('\n\n'),
currSrc: globSync(path.join(bDir, 'build/esnext/experimental_*.js'))
.map(f => fs.readFileSync(f, 'utf8'))
.join('\n\n'),
});

// Render the "change info" into a Markdown summary for the changelog.
const changeTypes = ['removed', 'changed', 'deprecated', 'added'];
let execSummaryFromChInfo = (chInfo) => {
const parts = changeTypes
.map(chType => chInfo.execSummaryFromChangeType[chType])
.filter(s => typeof(s) === 'string');
if (parts.length) {
return parts.join(', ');
} else {
return 'none';
}
}
const changelogEntry = [`
* feat: update semantic conventions to ${bSemconvVer} [#NNNN]
* Semantic Conventions ${bSemconvVer}:
[changelog](https://github.com/open-telemetry/semantic-conventions/blob/main/CHANGELOG.md#${slugify(bSemconvVer)}) |
[latest docs](https://opentelemetry.io/docs/specs/semconv/)
* \`@opentelemetry/semantic-conventions\` (stable) changes: *${execSummaryFromChInfo(stableChInfo)}*
* \`@opentelemetry/semantic-conventions/incubating\` (unstable) changes: *${execSummaryFromChInfo(unstableChInfo)}*
`];

if (stableChInfo.haveChanges) {
changelogEntry.push(`#### Stable changes in ${bSemconvVer}\n`);
for (let changeType of changeTypes) {
const summary = stableChInfo.summaryFromChangeType[changeType];
if (summary.length) {
changelogEntry.push(`<details open>
<summary>${stableChInfo.execSummaryFromChangeType[changeType]}</summary>

\`\`\`js
${summary.join('\n')}
\`\`\`

</details>
`);
}
}
}

if (unstableChInfo.haveChanges) {
changelogEntry.push(`#### Unstable changes in ${bSemconvVer}\n`);
for (let changeType of changeTypes) {
const summary = unstableChInfo.summaryFromChangeType[changeType];
if (summary.length) {
changelogEntry.push(`<details>
<summary>${unstableChInfo.execSummaryFromChangeType[changeType]}</summary>

\`\`\`js
${summary.join('\n')}
\`\`\`

</details>
`);
}
}
}

return changelogEntry.join('\n');
}

function main() {
const [aVer, bVer] = process.argv.slice(2);
const s = semconvChangelogGen(aVer, bVer);
console.log('The following could be added to the top "Enhancement" section of "semantic-conventions/CHANGELOG.md":');
console.log('\n- - -');
console.log(s)
console.log('- - -');
}

main();
4 changes: 2 additions & 2 deletions scripts/semconv/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ ROOT_DIR="${SCRIPT_DIR}/../../"

# Get latest version by running `git tag -l --sort=version:refname | tail -1`
# ... in [email protected]:open-telemetry/semantic-conventions.git
SPEC_VERSION=v1.28.0
SPEC_VERSION=v1.29.0
# ... in [email protected]:open-telemetry/weaver.git
GENERATOR_VERSION=v0.10.0

# When running on windows and your are getting references to ";C" (like Telemetry;C)
# When running on windows and you are getting references to ";C" (like Telemetry;C)
# then this is an issue with the bash shell, so first run the following in your shell:
# export MSYS_NO_PATHCONV=1

Expand Down
Loading