ChRIS plugin apps' logs streaming and replay architecture #151
Replies: 21 comments 19 replies
|
As a first proposition we should consider the pros and cons of simpler solutions that then could get incrementally more complex as required. An initial proposition coud be: sequenceDiagram
participant UI as ChRIS UI
participant CUBE as SSE (CUBE)
participant RQ as RabbitMQ
participant DB as MongoDB
participant FL as Fluentd
participant RT as Runtime (Docker / Podman / Swarm / Kubernetes / SLURM)
participant PF as pfcon
UI ->> CUBE: /plugins/instances/<id>/logs/
CUBE -->> UI: Stream logs (SSE)
CUBE ->> RQ: Celery queue subscription
RQ ->> CUBE: near-realtime job's logs updates
CUBE ->> DB: fetch a job's logs
DB->> CUBE: job's logs replay, recovery,<br/>offline analyses
FL->> RQ: publish jobs' logs from runtimes
FL->> DB: store jobs' logs from runtimes
RT -->> FL: runtime container/jobs' logs
PF -->> RT: schedule plugin apps,<br/>set logs to Fluentd
Notes:
|
|
a caveat for However, the log may last for hours/days and may be huge. pure-mem based a possible solution is the mix of persistent storage + streaming, as streaming is for the newest 1000 lines of data, and we get the full logs from persistent storage. |
|
given that |
|
For the record, the following is the previous design: |
|
@rudolphpienaar @jennydaman @Sandip117 The Event Forwarder async watcher process, Log Consumer / Streaming Worker and Status Consumer Service - worker process need to be implemented as custom long running consumer or producer Kafka worker processes. flowchart LR
%% =========================
%% SOURCES
%% =========================
subgraph S[Compute Runtime]
K8S[Kubernetes Jobs / Pods]
DCKR[Docker Containers]
end
%% =========================
%% EVENT PIPELINE
%% =========================
subgraph E[Event Pipeline]
EF[Event Forwarder Daemon - async watcher process]
K1[(Kafka: job-status-events)]
ST[Status Consumer Service - worker process]
Q[Task Queue - Celery / RQ]
end
%% =========================
%% LOG PIPELINE
%% =========================
subgraph L[Log Pipeline]
FB[Fluent Bit / Vector - log agent]
K2[(Kafka: job-logs)]
ES[(Elasticsearch / OpenSearch)]
end
%% =========================
%% REAL-TIME STREAMING LAYER
%% =========================
subgraph R[Real-time Log Streaming Layer]
LC[Log Consumer / Streaming Worker]
RD[(Redis Pub/Sub)]
DJ[Django SSE Service - auth + streaming]
UI[Web UI - log viewer]
end
%% =========================
%% EVENT FLOWS
%% =========================
K8S --> EF
DCKR --> EF
EF --> K1
K1 --> ST
ST --> Q
%% =========================
%% LOG FLOWS
%% =========================
K8S --> FB
DCKR --> FB
FB --> K2
K2 --> ES
%% =========================
%% STREAMING FLOWS
%% =========================
K2 --> LC
LC --> RD
RD --> DJ
DJ --> UI
|
|
hi @jbernal0019 ~
However, one caveat for I believe |
|
@chhsiao1981 Yes, we will keep the logs in Kafka for some time for replay. They will also be persisted in ES. Will need to implement the typical distributed system stuff in the custom consumers (retries + idempotency, logs batching, logs compression, event replay, etc...). |
|
Premature optimization is admittedly fun, but for the sake of productivity, before choosing the components and designing an architecture, we need to set a scope and identify requirements. Questions about Requirements
Thought Experiment: KISSKISS stands for "keep it simple, stupid." Kafka is not a secret sauce which magically adds scalability and event-drivenness. I can imagine that a system with fewer than 1,000 concurrent streams can work perfectly well, if not better, without Kafka. In more concrete terms, I mean that in a situation where there are fewer than 1,000 human users trying to observe in real-time the logs of plugin instances running at any given time (with anywhere from 1,000–10,000 plugin instances running in the background, logs which are not being actively observed by human end users are less burdensome on the system because there is no [real-time] streaming requirement). Component ChoicesI strongly believe in doing more research upfront to choose the right tool for the job. I also strongly prefer adopting current and next-gen technologies over using well-established solutions, which tend to be much more resource-intensive, steeper learning curve, and legacy patterns in both API design and deployment architecture. Moreover, from a business perspective, these choices can make it easier for us to answer the question: "what makes us different from others?" |
Log Storage Options
Decision Tree for Log Storage
|
|
@rudolphpienaar @jennydaman @Sandip117 @chhsiao1981 Considering the idea that (at the moment at least) we don't need to scale really large for the realtime logs and events as there won't be many concurrent users watching them in realtime from the UI I've implemented a new POC that uses simpler Readis Streams instead of Kafka. This is keeping in mind that there will still be many jobs running at any time (thousands) though. This significantly reduces setup complexity, operational costs and maintenance.. Think of it as getting ~80% of Kafka’s benefits with ~20% of the operational cost. Take a look at the POC repo. Here is the new compute logs and events architecture proposition. A new custom component was needed in place of FluentBit: Log Forwarder. This was to be able to implement missing resilience properties, custom logs transformations to fit Redis Streams and our particular use case that watches two parallel streaming pipelines from the same job with end-of-stream (EOS) signal.. The new custom service was necessary once Kafka was out of the picture. flowchart LR
%% =========================
%% SOURCES
%% =========================
subgraph S[Compute Runtime]
K8S[Kubernetes Jobs / Pods]
DCKR[Docker Containers]
end
%% =========================
%% EVENT PIPELINE
%% =========================
subgraph E[Event Pipeline]
EF[Event Forwarder Daemon - async watcher process]
K1[(Redis Streams: job-status-events)]
ST[Status Consumer Service - async worker process]
Q[Task Queue - Celery / RQ]
end
%% =========================
%% LOG PIPELINE
%% =========================
subgraph L[Log Pipeline]
LF[Log Forwarder Daemon - async watcher process]
K2[(Redis Streams: job-logs)]
ES[(OpenSearch)]
end
%% =========================
%% REAL-TIME STREAMING LAYER
%% =========================
subgraph R[Real-time Log Streaming Layer]
LC[Log Consumer Service - async worker process]
RD[(Redis Pub/Sub)]
DJ[SSE Service - auth + streaming]
UI[Web UI - log viewer]
end
%% =========================
%% EVENT FLOWS
%% =========================
K8S --> EF
DCKR --> EF
EF --> K1
K1 --> ST
ST --> Q
%% =========================
%% LOG FLOWS
%% =========================
K8S --> LF
DCKR --> LF
LF --> K2
K2 --> LC
LC --> ES
%% =========================
%% STREAMING FLOWS
%% =========================
LC --> RD
RD --> DJ
DJ --> UI
Resilience properties with Redis Streams:
Durability and HA:
|
|
It would be rude of me to provide a blanket statement of LLM hate without offering anything else of substance. Well, here is a proposal for what I have in mind in terms of something as simple as possible while being reasonably scalable/performant: flowchart LR
%% =========================
%% SOURCES
%% =========================
subgraph S[Compute Runtime]
K8S[Kubernetes Jobs / Pods]
DCKR[Docker Containers]
end
%% =========================
%% EVENT PIPELINE
%% =========================
subgraph E[Event Pipeline]
EF[Event Forwarder Daemon - async watcher process]
PG[PostgreSQL]
end
%% =========================
%% LOG PIPELINE
%% =========================
subgraph L[Log Pipeline]
FB[Fluent Bit / Vector - log agent]
ES[(Loki)]
end
%% =========================
%% REAL-TIME STREAMING LAYER
%% =========================
subgraph R[Real-time Log Streaming Layer]
DJ[Django SSE Service - auth + streaming]
UI[Web UI - log viewer]
end
%% =========================
%% EVENT FLOWS
%% =========================
K8S --> EF
DCKR --> EF
EF --> PG
PG --> DJ
%% =========================
%% LOG FLOWS
%% =========================
K8S --> FB
DCKR --> FB
FB --> ES
%% =========================
%% STREAMING FLOWS
%% =========================
ES --> DJ
DJ --> UI
Footnotes
|
Log and Event CollectorFluentBit v.s. Vector v.s. OpenTelemetry I strongly recommend the OpenTelemetry Collector (otel-collector) over FluentBit. Vector is a great piece of software but its community seems to be shrinking as people are standardizing around otel-collector and migrating to it. In fact, both Vector and FluentBit support OpenTelemetry protocols in order to keep up with the industry's standardization around OpenTelemetry. This is useful for existing systems with legacy architectures that depend on Vector or FluentBit, but we are trying to design and implement a new architecture so... Might as well choose the OpenTelemetry and forget about what are being replaced by it. To my knowledge, FluentD (not FluentBit) played an important role in recent history circa 2014–2020 because it was the only good option at the time, hence it has built-in support with docker. FluentBit came after and it does not have the same special treatment from docker because nowadays the ecosystem has expanded so shipping built-in drivers for any one component makes less sense. It seems like copy-pasting from a chatbot is what's cool, so here is what GPT-5 mini with reasoning + web search has to say:PROMPT: Please compare the OpenTelemetry Collector, FluentBit, and Vector. Make a suggestion for which one to use in a new software architecture for log and event streaming on a Kubernetes cluster. Use the most up-to-date recommendations and advice from the years 2025 and 2026.
My response: this demonstrates the capabilities and lack thereof of using ChatGPT for research and technical decision-making. The LLM performed a web search and found benchmarks showing that FluentBit and Vector are more resource-efficient than otel-collector, and it does not recommend using only otel-collector unless "you can accept higher per‑node resource use." This all makes sense since FluentBit is written in C, Vector in Rust, and otel-collector in Go. However, ChatGPT has never had to deploy on Kubernetes clusters, to be held accountable for its actions, and to endure all-nighters when things go wrong and what it thought was easy actually turns out to be hard. I have. Experienced human senior developers all agree:
ChatGPT's "recommended" "hybrid" solution is a terrible idea. Using two components is twice the amount of work compared to having one. Its recommendations to use FluentBit or Vector over otel-collector make sense for resource-constrained environments, but the general trend in industry is that developer time costs way more than CPU time. (I could keep beating this dead horse... a common log data transformation is to filter out unimportant lines, so performing OTTL transformations on the gateway instead of locally within DaemonSet containers means wasteful network I/O, which is even slower and more expensive than CPU time. ChatGPT speaks of this in saying vector can do "per‑event processing at the edge" yet it's unable to approximate nor judge how important edge-processing actually is.) |
|
@jennydaman We need the "middle man", Redis Streams in this case. There are many reasons for that (eg. resilience properties above) but here are three very important: 1- We can tolerate loosing some logs but can not tolerate losing events. Writing directly to Postgres from Event Forwarder is a terrible solution. It will also put more pressure on Postgres DB and doesn't fit our CUBE implementation. All the events processing and pfcon orchestration is done through Celery workers. Also I think the system above makes a good compromise between simplicity and easier to swap later for a higher performant system based on Kafka. I would remove Redis Pub/Sub from the picture but the SSE server then becomes more complex. It would no longer be stateless (easier to scale horizontally) and need to handle:
|
|
@jennydaman Great that you support the idea of using Redis. I don't think anybody needs to be convinced about why it's so important to decouple producers from consumers with an appropriate message queue system (helping with resilient features) in an event-driven architecture. Your comment "k8s API-server is comprised of a database (etcd) and we're essentially trying to copy data out of one database (k8s API-server's etcd) to another (CUBE's PostgreSQL)". This is an oversimplification of the whole ChRIS system. The whole plugin lifecycle, related event processing and PostgreSQL database schema and interactions are orchestrated from the client side (CUBE's Celery worker). We don't want to couple producers and consumers through data stores. Also PostgreSQL and Loki doing all the fan out work to consumers, filtering per client, distributing messages and pushing to the SSE event service in your proposed architecture looks really strange :-) The network partitioning was the first and only thing @rudolphpienaar asked and cared about when I presented the Kafka-based architecture. And I agree we should keep that in mind and also strive to minimize the communication surface and auth setup between the services running on the remote compute cluster and CUBE's consumers. Each arch has different trade offs and put complexity in different places and again I think the system I'm proposing not only fits better with current ChRIS implementation but also makes a good compromise between simplicity and easier to swap later for a higher performant system based on Kafka when that moment comes if ever. The weakest point in my arch above is the Log Consumer. Needs to be scalable. Kafka was able to write directly to OpenSearch but Redis Streams is not. |
|
|
@jennydaman I have a doubt about Loki. Everywhere I search about it seems to imply that it's designed to work better with object storage (S3). I asked AI to compare Loki and OpenSearch for our use case and POC code. This is what it said: What OpenSearch actually does todayThe write and read paths use OpenSearch for:
No How Loki's data model fits thatLoki indexes logs by labels only; content sits in compressed chunks on object storage. Two consequences matter:
Pros / cons for this use casePros of Loki
Cons of Loki for this access pattern
RecommendationNot a clear win unless log volume is your cost driver. The access pattern here is "point-read by job_id within a bounded time window" — OpenSearch's inverted index on |
|
@jennydaman @chhsiao1981 @rudolphpienaar @Sandip117 I've slimmed down the POC by now using Quickwit instead of OpenSearch and dropping Redis pub/sub. The SSE server now directly pulls events and logs for the near-realtime path from Redis Streams. It's a bit more complex now as it does the fan out work that was previously done by Redis pub/sub. This is supposed to be enough for hundreds of concurrent viewers. I think this is a minimalistic architecture that's easy to reason about, supports a moderate scale of UI streams and fits well with current ChRIS system. Here is the new architecture diagram. It's a bit simplified because the SSE service actually replays historical logs and events to late UI users from Quickwit and PostgreSQL This is in addition to the live stream if it's still happening when user connects. flowchart LR
%% =========================
%% SOURCES
%% =========================
subgraph S[Compute Runtime]
K8S[Kubernetes Jobs / Pods]
DCKR[Docker Containers]
end
%% =========================
%% EVENT PIPELINE
%% =========================
subgraph E[Event Pipeline]
EF[Event Forwarder Daemon - async watcher process]
K1[(Redis Streams: job-status-events)]
ST[Status Consumer Service - async worker process]
Q[Task Queue - Celery / Redis]
P[(PostgresSQL)]
end
%% =========================
%% REAL-TIME STREAMING LAYER
%% =========================
subgraph R[Real-time Streaming Layer]
DJ[SSE Service - auth + streaming]
UI[Web UI - log/event viewer]
end
%% =========================
%% LOG PIPELINE
%% =========================
subgraph L[Log Pipeline]
LF[Log Forwarder Daemon - async watcher process]
K2[(Redis Streams: job-logs)]
LC[Log Consumer Service - async worker process]
ES[(Quickwit)]
end
%% =========================
%% EVENT FLOWS
%% =========================
K8S --> EF
DCKR --> EF
EF --> K1
K1 --> ST
ST --> Q
Q --> P
%% =========================
%% LOG FLOWS
%% =========================
K8S --> LF
DCKR --> LF
LF --> K2
K2 --> LC
LC --> ES
%% =========================
%% STREAMING FLOWS
%% =========================
K1 --> DJ
K2 --> DJ
DJ --> UI
|
|
The caveat with pure-redis based streaming is that redis is in-memory only storage. |
|
The caveat of Openshift's direct-streaming-log on-demand approach:
|
|
https://iggy.apache.org/ is an interesting upcoming Kafka competitor. |

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Problem:
Currently container logs associated to a ChRIS plugin app running on a remote compute environment (Docker, Podman, Swarm, Kubernetes, SLURM) are obtained by CUBE by continuously polling the compute environment API through
pfconat very small time intervals to achieve near-realtime updates. The container logs are stored and updated directly in the relational Postgres DB as a long text field of a plugin instance table. When a user in the ChRIS UI wants to look at the logs panel for a running plugin instance the UI also continuously polls CUBE at very small time intervals to get the log updates.This architecture has many obvious flaws. Among them:
A new architecture should be devised to fix or mitigate the above issues.
Solution requirements:
All reactions