@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
- const logDeps = [...(0, detectContext_1.hasAnyDep)(ctx, LOGGING_PACKAGES), ...(0, detectContext_1.hasAnyElixirDep)(ctx, ELIXIR_LOGGING_PACKAGES)];
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, [/\/(health|healthz|readyz|livez|alive)\b/i, /x-request-id/i, /correlation-id/i, /error\s*handler/i, /RotatingFileHandler/i], 25);
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 = /\/(health|healthz|readyz|livez|alive)\b/i.test(m.snippet)
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
- const hasHealth = healthFiles.length > 0
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) => /\/(health|healthz|readyz|livez|alive)\b/i.test(h.snippet));
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.19.0",
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": {