Skip to content

Commit c8eba31

Browse files
authored
chore: bitrot script to show some maintenance todos (#163)
1 parent c38b2e3 commit c8eba31

File tree

1 file changed

+243
-0
lines changed

1 file changed

+243
-0
lines changed

scripts/bitrot.js

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
#!/usr/bin/env node
2+
3+
/*
4+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
5+
* or more contributor license agreements. See the NOTICE file distributed with
6+
* this work for additional information regarding copyright
7+
* ownership. Elasticsearch B.V. licenses this file to you under
8+
* the Apache License, Version 2.0 (the "License"); you may
9+
* not use this file except in compliance with the License.
10+
* You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing,
15+
* software distributed under the License is distributed on an
16+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
* KIND, either express or implied. See the License for the
18+
* specific language governing permissions and limitations
19+
* under the License.
20+
*/
21+
22+
const {execSync} = require('child_process');
23+
const fs = require('fs');
24+
const path = require('path');
25+
const semver = require('semver');
26+
27+
const ETEL_PJ_PATH = path.resolve(
28+
__dirname,
29+
'..',
30+
'packages',
31+
'opentelemetry-node',
32+
'package.json'
33+
);
34+
const SKIP_INSTR_NAMES = [
35+
'@opentelemetry/instrumentation-aws-lambda', // supported versions isn't meaningful
36+
'@opentelemetry/instrumentation-redis', // the separate 'instrumentation-redis-4' handles the latest versions
37+
];
38+
const QUIET = true;
39+
40+
let gRotCount = 0;
41+
42+
// ---- caching
43+
44+
const gCachePath = '/tmp/eon-bitrot.cache.json';
45+
let gCache = null;
46+
47+
function ensureCacheLoaded(ns) {
48+
if (gCache === null) {
49+
try {
50+
gCache = JSON.parse(fs.readFileSync(gCachePath));
51+
} catch (loadErr) {
52+
gCache = {};
53+
}
54+
}
55+
if (!(ns in gCache)) {
56+
gCache[ns] = {};
57+
}
58+
return gCache[ns];
59+
}
60+
61+
function saveCache() {
62+
if (gCache !== null) {
63+
fs.writeFileSync(gCachePath, JSON.stringify(gCache, null, 2));
64+
}
65+
}
66+
67+
// ---- minimal ANSI styling support (from bunyan)
68+
69+
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
70+
// Suggested colors (some are unreadable in common cases):
71+
// - Good: cyan, yellow (limited use, poor visibility on white background),
72+
// bold, green, magenta, red
73+
// - Bad: blue (not visible on cmd.exe), grey (same color as background on
74+
// Solarized Dark theme from <https://github.com/altercation/solarized>, see
75+
// issue #160)
76+
var colors = {
77+
bold: [1, 22],
78+
italic: [3, 23],
79+
underline: [4, 24],
80+
inverse: [7, 27],
81+
white: [37, 39],
82+
grey: [90, 39],
83+
black: [30, 39],
84+
blue: [34, 39],
85+
cyan: [36, 39],
86+
green: [32, 39],
87+
magenta: [35, 39],
88+
red: [31, 39],
89+
yellow: [33, 39],
90+
};
91+
92+
function stylizeWithColor(str, color) {
93+
if (!str) {
94+
return '';
95+
}
96+
var codes = colors[color];
97+
if (codes) {
98+
return '\x1B[' + codes[0] + 'm' + str + '\x1B[' + codes[1] + 'm';
99+
} else {
100+
return str;
101+
}
102+
}
103+
104+
let stylize = stylizeWithColor;
105+
106+
// ---- support functions
107+
108+
function rot(moduleName, s) {
109+
gRotCount++;
110+
console.log(`${stylize(moduleName, 'bold')} bitrot: ${s}`);
111+
}
112+
113+
function getNpmInfo(name) {
114+
const CACHE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
115+
const cache = ensureCacheLoaded('npmInfo');
116+
const cacheEntry = cache[name];
117+
if (cacheEntry) {
118+
if (cacheEntry.timestamp + CACHE_TIMEOUT_MS > Date.now()) {
119+
return cacheEntry.value;
120+
} else {
121+
delete cache[name];
122+
}
123+
}
124+
125+
// Limited security guard on exec'ing given `name`.
126+
const PKG_NAME_RE = /^(@[\w_.-]+\/)?([\w_.-]+)$/;
127+
if (!PKG_NAME_RE.test(name)) {
128+
throw new Error(
129+
`${JSON.stringify(
130+
name
131+
)} does not look like a valid npm package name`
132+
);
133+
}
134+
135+
const stdout = execSync(`npm info -j "${name}"`);
136+
const npmInfo = JSON.parse(stdout);
137+
138+
cache[name] = {
139+
timestamp: Date.now(),
140+
value: npmInfo,
141+
};
142+
saveCache();
143+
return npmInfo;
144+
}
145+
146+
function bitrot() {
147+
const pj = JSON.parse(fs.readFileSync(ETEL_PJ_PATH, 'utf8'));
148+
const instrNames = Object.keys(pj.dependencies).filter((d) =>
149+
d.startsWith('@opentelemetry/instrumentation-')
150+
);
151+
152+
const ainPj = getNpmInfo('@opentelemetry/auto-instrumentations-node');
153+
const ainInstrNames = Object.keys(ainPj.dependencies).filter((d) =>
154+
d.startsWith('@opentelemetry/instrumentation-')
155+
);
156+
157+
for (let instrName of ainInstrNames) {
158+
if (SKIP_INSTR_NAMES.includes(instrName)) continue;
159+
if (!instrNames.includes(instrName)) {
160+
rot(instrName, 'missing instr that auto-instrumentations-node has');
161+
}
162+
}
163+
164+
for (let instrName of instrNames) {
165+
if (SKIP_INSTR_NAMES.includes(instrName)) continue;
166+
167+
if (!QUIET) console.log(`${instrName}:`);
168+
const mod = require(instrName);
169+
const instrClass = Object.keys(mod).filter((n) =>
170+
n.endsWith('Instrumentation')
171+
)[0];
172+
const instr = new mod[instrClass]();
173+
const initVal = instr.init(); // grpc is weird here
174+
if (initVal === undefined) {
175+
if (!QUIET) console.log(` (instr.init() returned undefined!)`);
176+
continue;
177+
}
178+
const instrNodeModuleFiles = Array.isArray(initVal)
179+
? initVal
180+
: [initVal];
181+
182+
const supVersFromModName = {};
183+
for (let inmf of instrNodeModuleFiles) {
184+
// TODO: warn if supportedVersions range is open-ended. E.g. if it satisfies 9999.9999.9999 or something.
185+
if (!QUIET)
186+
console.log(
187+
` ${inmf.name}: ${JSON.stringify(
188+
inmf.supportedVersions
189+
)}`
190+
);
191+
// TODO: keep printing these? Do they ever matter?
192+
for (let file of inmf.files) {
193+
if (!QUIET)
194+
console.log(
195+
` ${file.name}: ${JSON.stringify(
196+
file.supportedVersions
197+
)}`
198+
);
199+
}
200+
if (!supVersFromModName[inmf.name]) {
201+
supVersFromModName[inmf.name] = [];
202+
}
203+
supVersFromModName[inmf.name].push(inmf.supportedVersions);
204+
}
205+
for (let modName of Object.keys(supVersFromModName)) {
206+
const supVers = supVersFromModName[modName].flat();
207+
if (supVers.toString() === '*') {
208+
// This is code for "node core module".
209+
continue;
210+
}
211+
const info = getNpmInfo(modName);
212+
const latest = info['dist-tags'].latest;
213+
if (!QUIET)
214+
console.log(` latest published: ${modName}@${latest}`);
215+
let supsLatest = false;
216+
for (let range of supVers) {
217+
if (semver.satisfies(latest, range)) {
218+
supsLatest = true;
219+
}
220+
}
221+
if (!supsLatest) {
222+
rot(
223+
instrName,
224+
`supportedVersions of module "${modName}" (${JSON.stringify(
225+
supVers
226+
)}) do not support the latest published ${modName}@${latest}`
227+
);
228+
}
229+
}
230+
}
231+
}
232+
233+
function main(argv) {
234+
bitrot();
235+
236+
if (gRotCount > 0) {
237+
process.exit(3);
238+
}
239+
}
240+
241+
if (require.main === module) {
242+
main(process.argv);
243+
}

0 commit comments

Comments
 (0)