Skip to content

Commit 786d28e

Browse files
committed
Updating logging.
1 parent 184a4f8 commit 786d28e

5 files changed

Lines changed: 63 additions & 14 deletions

File tree

‎CLAUDE.md‎

Whitespace-only changes.

‎arbiter-platform/src/main/java/ai/philterd/arbiter/service/ElasticsearchIngestJobService.java‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,8 +355,11 @@ private JsonNode sendJson(final String url, final String body, final String auth
355355
final HttpResponse<String> resp = httpClient.send(reqBuilder.build(),
356356
HttpResponse.BodyHandlers.ofString());
357357
if (resp.statusCode() / 100 != 2) {
358+
// Do not include resp.body() — it carries partial search hits with raw
359+
// originalText for the documents this job was about to ingest, and the
360+
// exception bubbles up to a log site. Status + length is the safe surrogate.
358361
throw new IllegalStateException("Elasticsearch returned HTTP " + resp.statusCode()
359-
+ (resp.body() == null || resp.body().isEmpty() ? "" : ": " + resp.body()));
362+
+ " (body length " + (resp.body() == null ? 0 : resp.body().length()) + ")");
360363
}
361364
return objectMapper.readTree(resp.body());
362365
}

‎arbiter-platform/src/main/java/ai/philterd/arbiter/service/OpenSearchIndexService.java‎

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,13 @@ public void indexDocument(final Document document) {
8080
// (auth and configurable index name applied via applyAuth() and indexName())
8181
final HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
8282
if (resp.statusCode() / 100 != 2) {
83-
log.warn("OpenSearch indexing returned HTTP {} for document {} ({}): {}",
84-
resp.statusCode(), document.getId(), url, truncate(resp.body()));
83+
// Do not log resp.body() — a non-2xx index response from OpenSearch echoes
84+
// parts of the request payload, which carries the document's PII-bearing
85+
// originalText. Body length is enough for an operator to tell a parse error
86+
// (a few bytes) from a quota / mapping error (a longer JSON envelope).
87+
log.warn("OpenSearch indexing returned HTTP {} for document {} ({}); body length {}",
88+
resp.statusCode(), document.getId(), url,
89+
resp.body() == null ? 0 : resp.body().length());
8590
}
8691
} catch (Exception e) {
8792
log.warn("OpenSearch indexing failed for document {} at {}: {}",
@@ -184,8 +189,14 @@ public SearchResults search(final String query, final int from, final int size,
184189
return new SearchResults(0, safeFrom, safeSize, List.of());
185190
}
186191
if (resp.statusCode() / 100 != 2) {
187-
log.warn("OpenSearch search returned HTTP {} for query '{}': {}",
188-
resp.statusCode(), query, truncate(resp.body()));
192+
// Never log the raw query — users routinely search for PII (an email
193+
// address, a phone number, a partial name) and that string would land
194+
// in the log file. Never log the response body either — it contains
195+
// matched document originalText snippets. A length + status is enough
196+
// for triage; the OpenSearch cluster's own logs carry the full payload.
197+
log.warn("OpenSearch search returned HTTP {} (query length {}, response length {})",
198+
resp.statusCode(), query.length(),
199+
resp.body() == null ? 0 : resp.body().length());
189200
return empty;
190201
}
191202
final JsonNode root = objectMapper.readTree(resp.body());
@@ -210,7 +221,9 @@ public SearchResults search(final String query, final int from, final int size,
210221
}
211222
return new SearchResults(total, safeFrom, safeSize, List.copyOf(hits));
212223
} catch (Exception e) {
213-
log.warn("OpenSearch search failed for query '{}' at {}: {}", query, url, e.getMessage());
224+
// Same reasoning as the non-2xx branch above — log only metadata.
225+
log.warn("OpenSearch search failed at {} (query length {}): {}",
226+
url, query.length(), e.getMessage());
214227
return empty;
215228
}
216229
}
@@ -264,8 +277,11 @@ public SearchResults findSimilar(final String documentId, final String batchId,
264277
final HttpResponse<String> resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
265278
if (resp.statusCode() == 404) return empty;
266279
if (resp.statusCode() / 100 != 2) {
267-
log.warn("OpenSearch findSimilar returned HTTP {} for document {}: {}",
268-
resp.statusCode(), documentId, truncate(resp.body()));
280+
// The findSimilar response body contains matched documents' originalText
281+
// snippets — never log it. Length is enough for triage.
282+
log.warn("OpenSearch findSimilar returned HTTP {} for document {}; body length {}",
283+
resp.statusCode(), documentId,
284+
resp.body() == null ? 0 : resp.body().length());
269285
return empty;
270286
}
271287
final JsonNode root = objectMapper.readTree(resp.body());
@@ -354,11 +370,6 @@ private HttpRequest.Builder applyAuth(final HttpRequest.Builder b) {
354370
return b;
355371
}
356372

357-
private static String truncate(final String s) {
358-
if (s == null) return "";
359-
return s.length() <= 200 ? s : s.substring(0, 200) + "…";
360-
}
361-
362373
/**
363374
* Render a highlight snippet as safe HTML. The raw snippet contains the surrounding
364375
* document text (which may include user-typed {@code <script>} payloads) bracketed by

‎arbiter-platform/src/main/java/ai/philterd/arbiter/service/OpenSearchIngestJobService.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,8 +389,13 @@ private JsonNode sendJson(final String url, final String body, final String auth
389389
final HttpResponse<String> resp = httpClient.send(reqBuilder.build(),
390390
HttpResponse.BodyHandlers.ofString());
391391
if (resp.statusCode() / 100 != 2) {
392+
// Do not include resp.body() in the exception message: it can carry partial
393+
// search hits (with raw originalText) for the documents this job was about
394+
// to ingest, and the exception is then logged by the caller's failure path.
395+
// Status code + body length is the safe surrogate; an operator who needs the
396+
// full body can read it from the OpenSearch cluster's own logs.
392397
throw new IllegalStateException("OpenSearch returned HTTP " + resp.statusCode()
393-
+ (resp.body() == null || resp.body().isEmpty() ? "" : ": " + resp.body()));
398+
+ " (body length " + (resp.body() == null ? 0 : resp.body().length()) + ")");
394399
}
395400
return objectMapper.readTree(resp.body());
396401
}

‎docs/docs/security.md‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,36 @@ backups. Rotating the key requires re-encrypting every existing row; this is
278278
intentionally out-of-band today (no UI affordance) so that a sloppy rotation
279279
does not leave half the corpus unreadable.
280280

281+
## PII never appears in application logs
282+
283+
Arbiter's logging policy is that **document content is never written to a log
284+
file or to standard out**, regardless of log level. The fields covered are
285+
the same ones encrypted at rest in MongoDB:
286+
287+
- Document `originalText` (the source text being redacted) and
288+
`redactedText` (the rendered output).
289+
- Document `failureMessage` text.
290+
- Span `text` (the literal PII string detected).
291+
- Comment `text`.
292+
- User-supplied search queries (a reviewer searching for an email or phone
293+
number must not pin that string in the log file).
294+
295+
Network call sites that interact with stores carrying this content
296+
(OpenSearch indexing, OpenSearch search, OpenSearch `more_like_this`,
297+
OpenSearch and Elasticsearch ingest) deliberately log only metadata —
298+
status code, body length, document id, batch id, error class — when they
299+
fail. Response bodies and query strings are summarised by length, never
300+
echoed verbatim. Exceptions thrown from those layers carry the same
301+
metadata-only message so a downstream `log.warn(..., e.getMessage())` does
302+
not regress this contract.
303+
304+
If a richer dump is needed for diagnosis, read the **OpenSearch /
305+
Elasticsearch cluster's own logs** rather than Arbiter's — those stores
306+
have their own access controls and are part of the same security boundary
307+
documented above. The bootstrap admin notice on first start is the only
308+
deliberate write to standard out, and it carries no document PII — just
309+
the configured admin email and a generated initial password.
310+
281311
## Document content integrity
282312

283313
Every document Arbiter ingests — whether through the web upload form or

0 commit comments

Comments
 (0)