@produtype/core 1.19.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -5,6 +5,7 @@ const detectContext_1 = require("./detectContext");
|
|
|
5
5
|
const textSearch_1 = require("../utils/textSearch");
|
|
6
6
|
const absenceEvidence_1 = require("./absenceEvidence");
|
|
7
7
|
const valuesFromPackage_1 = require("./structural/valuesFromPackage");
|
|
8
|
+
const readTextFileSafe_1 = require("../utils/readTextFileSafe");
|
|
8
9
|
/**
|
|
9
10
|
* Logging packages, which the ecosystem names and the author does not.
|
|
10
11
|
*
|
|
@@ -32,15 +33,43 @@ const LOGGING_PACKAGES = [
|
|
|
32
33
|
];
|
|
33
34
|
/** Elixir reaches for a backend rather than a logger: Logger itself is in OTP. */
|
|
34
35
|
const ELIXIR_LOGGING_PACKAGES = ['logger_json', 'logger_file_backend', 'sentry'];
|
|
36
|
+
/**
|
|
37
|
+
* The route, in every spelling somebody writes it.
|
|
38
|
+
*
|
|
39
|
+
* `\/(health|healthz|readyz|livez|alive)\b` does not match `/healthcheck`: after
|
|
40
|
+
* `health` comes a `c`, and the boundary fails. Netflix's dispatch declares
|
|
41
|
+
* `@api_router.get("/healthcheck")` and was told it has no health endpoint — the
|
|
42
|
+
* plainest spelling there is, missed by the pattern meant to find it.
|
|
43
|
+
*
|
|
44
|
+
* This does not disturb the decision recorded below about Dropwizard. That one is
|
|
45
|
+
* about an endpoint the *framework* publishes, which no Dropwizard application writes
|
|
46
|
+
* in its own source; a line like dispatch's is somebody declaring the route
|
|
47
|
+
* themselves.
|
|
48
|
+
*/
|
|
49
|
+
const HEALTH_ROUTE = /\/(health|healthz|healthcheck|health[-_]check|readyz|livez|alive)\b/i;
|
|
35
50
|
async function detectObservability(ctx) {
|
|
36
51
|
const evidence = [];
|
|
37
|
-
|
|
52
|
+
/**
|
|
53
|
+
* .NET names its logging in the project file and nothing here was reading it.
|
|
54
|
+
*
|
|
55
|
+
* Radarr declares `NLog`, `NLog.Extensions.Logging` and
|
|
56
|
+
* `NLog.Layouts.ClefJsonLayout` — CLEF being the compact JSON event format — and
|
|
57
|
+
* was reported as logging something but not structurally. The npm list has had
|
|
58
|
+
* winston and pino since the beginning and treats the dependency alone as enough;
|
|
59
|
+
* these are the same statement in another manifest.
|
|
60
|
+
*/
|
|
61
|
+
const DOTNET_LOGGING_PACKAGES = ['NLog', 'NLog.Extensions.Logging', 'Serilog', 'Serilog.AspNetCore', 'Microsoft.Extensions.Logging', 'log4net'];
|
|
62
|
+
const logDeps = [
|
|
63
|
+
...(0, detectContext_1.hasAnyDep)(ctx, LOGGING_PACKAGES),
|
|
64
|
+
...(0, detectContext_1.hasAnyElixirDep)(ctx, ELIXIR_LOGGING_PACKAGES),
|
|
65
|
+
...(0, detectContext_1.hasAnyDotnetDep)(ctx, DOTNET_LOGGING_PACKAGES),
|
|
66
|
+
];
|
|
38
67
|
const sentryDeps = (0, detectContext_1.hasAnyDep)(ctx, ['@sentry/node', 'sentry-sdk']);
|
|
39
68
|
for (const d of logDeps)
|
|
40
69
|
evidence.push({ type: 'dependency', value: d, claim: 'logging' });
|
|
41
70
|
for (const d of sentryDeps)
|
|
42
71
|
evidence.push({ type: 'dependency', value: d, claim: 'logging' });
|
|
43
|
-
const hits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [
|
|
72
|
+
const hits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [HEALTH_ROUTE, /x-request-id/i, /correlation-id/i, /error\s*handler/i, /RotatingFileHandler/i], 25);
|
|
44
73
|
/**
|
|
45
74
|
* One search, three answers, so the claim is decided per hit rather than per search.
|
|
46
75
|
*
|
|
@@ -49,7 +78,7 @@ async function detectObservability(ctx) {
|
|
|
49
78
|
* is what the line is evidence of.
|
|
50
79
|
*/
|
|
51
80
|
for (const m of hits) {
|
|
52
|
-
const claim =
|
|
81
|
+
const claim = HEALTH_ROUTE.test(m.snippet)
|
|
53
82
|
? 'health'
|
|
54
83
|
: /x-request-id|correlation-id/i.test(m.snippet)
|
|
55
84
|
? 'request-id'
|
|
@@ -78,9 +107,44 @@ async function detectObservability(ctx) {
|
|
|
78
107
|
const actuator = (0, detectContext_1.hasAnyGradleDep)(ctx, ['spring-boot-starter-actuator']);
|
|
79
108
|
for (const dep of actuator)
|
|
80
109
|
evidence.push({ type: 'dependency', value: dep, claim: 'health' });
|
|
81
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The probe a platform declares, which is the operator's half of the same answer.
|
|
112
|
+
*
|
|
113
|
+
* immich declares `healthcheck:` for its services in all four of its compose files
|
|
114
|
+
* and was told it has no health endpoint. Something is being probed — that is what
|
|
115
|
+
* the key means — and the endpoint it probes is inside the image, where no text
|
|
116
|
+
* search here will find it.
|
|
117
|
+
*
|
|
118
|
+
* `HEALTHCHECK` is Docker's instruction, `healthcheck:` is compose's key, and
|
|
119
|
+
* `livenessProbe:` and `readinessProbe:` are Kubernetes' field names. None is the
|
|
120
|
+
* author's word, and each one is a statement that this service answers a health
|
|
121
|
+
* question.
|
|
122
|
+
*
|
|
123
|
+
* `disable: true` is the one that says the opposite — compose's way of switching
|
|
124
|
+
* off an image's own check — so a block carrying it is not counted.
|
|
125
|
+
*/
|
|
126
|
+
const deploymentManifests = ctx.files.all.filter((file) => /(^|\/)(docker-compose[\w.-]*\.ya?ml|compose[\w.-]*\.ya?ml|Dockerfile[\w.-]*)$/.test(file)
|
|
127
|
+
|| /(^|\/)(k8s|kubernetes|helm|deploy|charts)\/.*\.ya?ml$/i.test(file)).slice(0, 12);
|
|
128
|
+
const declaredProbes = [];
|
|
129
|
+
for (const file of deploymentManifests) {
|
|
130
|
+
const text = (await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file)) ?? '';
|
|
131
|
+
const lines = text.split(/\r?\n/);
|
|
132
|
+
for (let i = 0; i < lines.length; i++) {
|
|
133
|
+
if (!/^\s*(?:HEALTHCHECK\s|healthcheck\s*:|livenessProbe\s*:|readinessProbe\s*:)/.test(lines[i]))
|
|
134
|
+
continue;
|
|
135
|
+
/** compose writes `healthcheck:` then `disable: true` to switch the image's own off. */
|
|
136
|
+
if (lines.slice(i + 1, i + 3).some((line) => /^\s*disable\s*:\s*true/.test(line)))
|
|
137
|
+
continue;
|
|
138
|
+
declaredProbes.push({ type: 'snippet', value: lines[i].trim().slice(0, 200), file, line: i + 1, claim: 'health' });
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const probe of declaredProbes.slice(0, 3))
|
|
143
|
+
evidence.push(probe);
|
|
144
|
+
const hasHealth = declaredProbes.length > 0
|
|
145
|
+
|| healthFiles.length > 0
|
|
82
146
|
|| actuator.length > 0
|
|
83
|
-
|| hits.some((h) =>
|
|
147
|
+
|| hits.some((h) => HEALTH_ROUTE.test(h.snippet));
|
|
84
148
|
const hasReqId = hits.some((h) => /x-request-id|correlation-id/i.test(h.snippet));
|
|
85
149
|
// Structured logging without a logging library is still structured logging. What
|
|
86
150
|
// matters is that entries are machine-readable and correlated, not which package
|
|
@@ -146,7 +210,7 @@ async function detectObservability(ctx) {
|
|
|
146
210
|
const hasStructuredLogging = logDeps.length > 0 || structuredHits.length > 0;
|
|
147
211
|
const hasAnyLogging = hasStructuredLogging || plainLogging.length > 0;
|
|
148
212
|
if (!hasHealth) {
|
|
149
|
-
evidence.push(...(0, absenceEvidence_1.searchedFor)('a health endpoint', ['/health', '/healthz', '/readyz', 'a health, healthz, readyz, liveness or readiness route file'], 'health'));
|
|
213
|
+
evidence.push(...(0, absenceEvidence_1.searchedFor)('a health endpoint', ['/health', '/healthz', '/readyz', 'a health, healthz, readyz, liveness or readiness route file', 'a HEALTHCHECK, a compose healthcheck or a Kubernetes liveness probe'], 'health'));
|
|
150
214
|
}
|
|
151
215
|
if (!hasAnyLogging) {
|
|
152
216
|
evidence.push(...(0, absenceEvidence_1.searchedFor)('logging', ['winston', 'pino', 'morgan', 'bunyan', 'Monolog', 'slog', 'zap', 'logrus', 'slf4j', 'Rails.logger', 'tracing::', 'logger.info/warn/error/debug', 'Logger.info', 'error_log(', 'JSON.stringify with a level field'], 'logging'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.0",
|
|
4
4
|
"description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|