Repository navigation
Conversation
606817f to
08fb796
Compare
4e11f4c to
b0ce71f
Compare
400d808 to
9105a92
Compare
9105a92 to
06acac7
Compare
Signed-off-by: I539231 <pascal.zimmermann01@sap.com>
Kehrlann
left a comment
There was a problem hiding this comment.
The API introduced in #1265 was specifically designed to transition seamlessly from Doppler to Logcache.
The idea was to:
- Deprecate the old
log(...)which used Doppler types and the Doppler API - Introduce a new API that is cache-system independent, put Doppler in the beginning but silently swap Logcache later on
We are fine doing this, because if users care about the underlying logger implementation, they should use DopplerClient or LogCacheClient directly ; "applications" is an implementation-agnostic wrapper.
#1348 was specifically built on top of this.
This PR should continue the work, and evolve logs to use LogCacheClient's new tailing API to stream logs.
We should not introduce new methods in Applications, nor expose LogCache specific APIs in Applications.
| return logs(LogsRequest.builder().name(request.getName()).recent(false).build()) | ||
| .map(DefaultApplications::toApplicationLog); | ||
| } | ||
| return logs(LogsRequest.builder() |
There was a problem hiding this comment.
This is a regression: this method's logs(... recent=true) worked for users that didn't have Doppler (the extreme majority), and now it doesn't work anymore.
| cursor.set( | ||
| (last.getTimestamp() != null | ||
| ? last.getTimestamp() | ||
| : cursor.get()) | ||
| + 1); |
There was a problem hiding this comment.
[LLM-assisted review]
Opus 5.5 flagged an issue here: LogCache is eventually consistent, meaning that logs may arrive in the cache out of order. So reading from T to now() (say T+5), and setting the next cursor to T+6 will miss any logs emitted between T and T+5 that arrive after T+5.
Golang has a trick, WalkWithDelay: it drops envelopes it considers "too new" (< 2s). It introduces some delay (2s by default), but mechanically reads those dropped envelopes in the next poll loop.
| t -> { | ||
| if (t instanceof AbstractCloudFoundryException) { | ||
| return Mono.error(t); | ||
| } |
There was a problem hiding this comment.
I believe we're swallowing every every 4xx and 5xx through ErrorPayloadMappers#fallback and ending the stream.
Including rate limits (429) or indeed transient 503 as the logger warns below.
| LOGGER.warn( | ||
| "Transient error polling Log Cache, retrying", | ||
| t); | ||
| return Mono.just(ReadResponse.builder().build()); |
There was a problem hiding this comment.
Here we're piggybacking on fake empty responses to trigger a retry and introduce a delayed backoff. We should probably use Reactor's .retryWhen(), which gives us retry-spec, with fine grained controls over backoff.
The current interval is fine for backing off, but it lacks retry limit cap (maxCount in Go).
| new AtomicLong( | ||
| request.getStartTime() != null | ||
| ? request.getStartTime() | ||
| : defaultStartNanos); |
There was a problem hiding this comment.
I think this introduces a race condition should you have multiple subscribers. The problem is that the returned Flux has a closure over cursor.
var x = lcc.logsTail(request); // creates cursor1
x.doOnNext(...).subscribe(); // uses cursor1 to advance and mutates it
// some delays
x.doOnNext(...).subscribe(); // also uses cursor1 and competes with the previous subYou probably want to rework your reactive pipeline to create the cursor on subscription.
Claude suggests something like:
Flux<Envelope> logsTail(TailLogsRequest request) {
return Mono.fromSupplier(() -> initialCursor(request)) // evaluated per subscription
.flatMap(cursor -> poll(request, cursor))
.expand(page -> page.isEmpty()
? Mono.delay(request.getPollInterval()).then(poll(request, page.cursor))
: poll(request, page.nextCursor()))
.concatMapIterable(page -> page.envelopes);
}
private Mono<Page> poll(TailLogsRequest request, long cursor) {
return read(toReadRequest(request, cursor))
.map(response -> new Page(cursor, envelopes(response)));
}
private static final class Page {
final long cursor;
final List<Envelope> envelopes;
Page(long cursor, List<Envelope> envelopes) { ... }
boolean isEmpty() { return envelopes.isEmpty(); }
long nextCursor() { return envelopes.get(envelopes.size() - 1).getTimestamp() + 1; }
}
Summary
Adds live log streaming via Log Cache — the Java equivalent of
cf tail --follow.Previously the only Log Cache read path was
logsRecent, a single snapshot request. There was no way to continuously receive new log envelopes without polling manually. This change introduces alogsTailAPI across all three relevant modules.Motivation
Cloud Foundry dropped the legacy Loggregator Doppler streaming endpoint (
DopplerClient.stream()) in Loggregator ≥ 107.0 (CFD ≥ 24.3 / TAS ≥ 4.0). The CF CLI replaced it with a polling loop over the Log Cache/api/v1/readendpoint — that is whatcf tail --followdoes today. This PR brings the same capability to Java consumers.Changes
Changes in detail
cloudfoundry-client— newTailLogsRequestandLogCacheClient.logsTail()New
_TailLogsRequest.java(Immutables@Value.Immutable):sourceIdStringstartTimeLong(nullable)now − 5 s(ns)envelopeTypesList<EnvelopeType>(nullable)nameFilterString(nullable)pollIntervalDuration250 msNew method on
LogCacheClient:cloudfoundry-client-reactor— non-blocking polling implementationReactorLogCacheEndpoints.logsTail()mirrors the Go logcache.Walk() algorithm, fully non-blocking:AtomicLongcursor starts atstartTime(ornow − 5 s).Flux.deferbuilds a freshReadRequestfrom the current cursor on every repetition and callsGET /api/v1/read/{sourceId}?start_time=cursor.lastTimestamp + 1; each envelope is emitted individually downstream.repeatWheninserts aMono.delay(pollInterval)before the next poll. When envelopes arrive, the next poll starts immediately.Fluxis infinite; the caller cancels the subscription to stop.Transient network errors are logged and retried. HTTP errors (4xx/5xx) are propagated to the subscriber.
cloudfoundry-operations—Applications.logsTail()DefaultApplicationsdelegates toLogCacheClient:Tests
logsTailLogCacheLOG/OUTenvelope is forwarded correctlylogsTailLogCacheMultipleEnvelopesOUT → ERR → OUTare emitted in orderlogsTailLogCacheErrorRuntimeExceptionfrom the client propagates unchanged to the subscriberlogsTailLogCacheOutAndErrEnvelopesUsage
Or via the Operations API:
Relation to existing API
DopplerClient.stream()LogCacheClient.recentLogs()LogCacheClient.logsTail()✅ newChecklist
_TailLogsRequestImmutables value objectLogCacheClient.logsTail()interface methodReactorLogCacheEndpoints.logsTail()— non-blocking Reactor implementation_ReactorLogCacheClient.logsTail()— delegate overrideApplications.logsTail()— Operations API methodDefaultApplications.logsTail()— implementationDefaultApplicationsTest(120 passing)