66 "fmt"
77 "net/http"
88 "os"
9+ "time"
910
1011 "github.com/krateoplatformops/eventsse/internal/labels"
1112 "github.com/krateoplatformops/eventsse/internal/store"
@@ -14,82 +15,102 @@ import (
1415 corev1 "k8s.io/api/core/v1"
1516)
1617
18+ const (
19+ defaultHeartbeatInterval = 25 * time .Second
20+ defaultEventQueueSize = 256
21+ )
22+
1723func SSE (cli clientv3.Watcher ) http.Handler {
18- return & handler {
19- cli : cli ,
20- }
24+ return & handler {cli : cli }
2125}
2226
23- var _ http.Handler = (* handler )(nil )
24-
2527type handler struct {
2628 cli clientv3.Watcher
2729}
2830
29- // @title EventSSE API
30- // @version 1.0
31- // @description This the Krateo EventSSE server.
32- // @BasePath /
33-
34- // Health godoc
35- // @Summary SSE Endpoint
36- // @Description Get available events notifications
37- // @ID notifications
38- // @Produce json
39- // @Success 200 {array} types.Event
40- // @Router /pub [get]
41- func (r * handler ) ServeHTTP (wri http.ResponseWriter , req * http.Request ) {
31+ func (r * handler ) ServeHTTP (w http.ResponseWriter , req * http.Request ) {
4232 ctx , cancel := context .WithCancel (req .Context ())
4333 defer cancel ()
4434
45- // === CORS Preflight ===
35+ // CORS
4636 if req .Method == http .MethodOptions {
47- r .setCORSHeaders (wri )
48- wri .WriteHeader (http .StatusNoContent )
37+ r .setCORSHeaders (w )
38+ w .WriteHeader (http .StatusNoContent )
4939 return
5040 }
41+ r .setCORSHeaders (w )
5142
52- r .setCORSHeaders (wri )
53-
54- // === SSE Headers ===
55- wri .Header ().Set ("Content-Type" , "text/event-stream" )
56- wri .Header ().Set ("Cache-Control" , "no-cache" )
57- wri .Header ().Set ("Connection" , "keep-alive" )
58- wri .Header ().Set ("X-Accel-Buffering" , "no" )
43+ // SSE headers
44+ w .Header ().Set ("Content-Type" , "text/event-stream" )
45+ w .Header ().Set ("Cache-Control" , "no-cache" )
46+ w .Header ().Set ("Connection" , "keep-alive" )
47+ w .Header ().Set ("X-Accel-Buffering" , "no" )
5948
6049 log := zerolog .New (os .Stdout ).With ().
6150 Str ("service" , "eventsse" ).
6251 Timestamp ().
6352 Logger ()
6453
65- f , ok := wri .(http.Flusher )
54+ flusher , ok := w .(http.Flusher )
6655 if ! ok {
6756 log .Error ().Msg ("http.ResponseWriter does not implement http.Flusher" )
68- http .Error (wri , "Streaming not supported" , http .StatusInternalServerError )
57+ http .Error (w , "Streaming not supported" , http .StatusInternalServerError )
58+ return
59+ }
60+
61+ eventCh := make (chan string , defaultEventQueueSize )
62+ defer close (eventCh )
63+
64+ go func () {
65+ for evt := range eventCh {
66+ _ , err := w .Write ([]byte (evt ))
67+ if err != nil {
68+ log .Info ().Msg ("Client disconnected (write error)" )
69+ return
70+ }
71+ flusher .Flush ()
72+ }
73+ }()
74+
75+ // Evento iniziale
76+ initial := fmt .Sprintf ("event: connection-established\n id: 88888888\n data: %s\n \n " , `{"info": "Ready to watch events"}` )
77+ select {
78+ case eventCh <- initial :
79+ case <- ctx .Done ():
6980 return
7081 }
7182
72- fmt .Fprintln (wri , "event: connection-established" )
73- fmt .Fprintln (wri , "id: 88888888" )
74- fmt .Fprintf (wri , "data: %s\n \n " , `{"info": "Ready to watch events"}` )
75- f .Flush ()
83+ heartbeat := time .NewTicker (defaultHeartbeatInterval )
84+ defer heartbeat .Stop ()
7685
7786 watchChan := r .cli .Watch (ctx , store .RootKey , clientv3 .WithPrefix ())
87+
7888 for {
7989 select {
8090 case <- ctx .Done ():
8191 log .Info ().Msg ("SSE client disconnected" )
8292 return
8393
94+ case <- heartbeat .C :
95+ select {
96+ case eventCh <- ": ping\n \n " :
97+ default :
98+ log .Debug ().Msg ("Heartbeat skipped, client lento" )
99+ }
100+
84101 case watchResp , ok := <- watchChan :
85102 if ! ok {
86103 log .Warn ().Msg ("Etcd watch channel closed" )
87104 return
88105 }
106+ if err := watchResp .Err (); err != nil {
107+ log .Error ().Err (err ).Msg ("Error from ETCD watch" )
108+ continue
109+ }
89110
90111 for _ , ev := range watchResp .Events {
91- key := string (ev .Kv .Key )
92112 val := ev .Kv .Value
113+ key := string (ev .Kv .Key )
93114 if len (val ) == 0 {
94115 continue
95116 }
@@ -101,48 +122,39 @@ func (r *handler) ServeHTTP(wri http.ResponseWriter, req *http.Request) {
101122 }
102123
103124 cid := labels .CompositionID (& obj )
104- belongsToComposition := len (cid ) > 0
105-
106125 eventName := "krateo"
107- if len ( cid ) > 0 {
126+ if cid != "" {
108127 eventName = cid
109128 }
110129
111- zle := log .Debug ().
130+ log .Debug ().
112131 Str ("id" , key ).
132+ Str ("event" , eventName ).
113133 Str ("reason" , obj .Reason ).
114134 Str ("message" , obj .Message ).
115135 Str ("involvedObject.Name" , obj .InvolvedObject .Name ).
116- Str ("involvedObject.Namespace" , obj .InvolvedObject .Namespace )
117-
118- if belongsToComposition {
119- zle .Str ("event" , cid )
120- } else {
121- zle .Str ("event" , "krateo" )
122- }
123- zle .Msg ("Sending SSE" )
124- zle = nil
136+ Str ("involvedObject.Namespace" , obj .InvolvedObject .Namespace ).
137+ Msg ("Queueing SSE event" )
125138
126- fmt .Fprintf (wri , "event: %s\n " , eventName )
127- fmt .Fprintf (wri , "id: %s\n " , key )
128- fmt .Fprintf (wri , "data: %s\n \n " , string (val ))
129- f .Flush ()
139+ payload := fmt .Sprintf ("event: %s\n id: %s\n data: %s\n \n " , eventName , key , string (val ))
130140
131- log .Debug ().
132- Str ("event" , eventName ).
133- Str ("key" , key ).
134- Msg ("SSE sent" )
141+ // invio non bloccante
142+ select {
143+ case eventCh <- payload :
144+ default :
145+ log .Warn ().Str ("event" , eventName ).Str ("key" , key ).Msg ("Dropping SSE event, client troppo lento" )
146+ }
135147 }
136148 }
137149 }
138150}
139151
140- // setCORSHeaders aggiunge header CORS generali
141152func (r * handler ) setCORSHeaders (w http.ResponseWriter ) {
142153 w .Header ().Set ("Access-Control-Allow-Origin" , "*" )
143- w .Header ().Set ("Access-Control-Allow-Methods" , "GET, POST, OPTIONS" )
144- w .Header ().Set ("Access-Control-Allow-Headers" , "Accept, Authorization, Content-Type, X-Auth-Code, X-Krateo-TraceId" )
145- w .Header ().Set ("Access-Control-Expose-Headers" , "Link,Authorization,Content-Type" )
146- w .Header ().Set ("Access-Control-Allow-Headers" , "Authorization,Content-Type" )
154+ w .Header ().Set ("Access-Control-Allow-Methods" , "GET, OPTIONS" )
155+ w .Header ().Set ("Access-Control-Allow-Headers" ,
156+ "Accept, Authorization, Content-Type, X-Auth-Code, X-Krateo-TraceId" )
157+ w .Header ().Set ("Access-Control-Expose-Headers" ,
158+ "Link, Authorization, Content-Type" )
147159 w .Header ().Set ("Access-Control-Allow-Credentials" , "true" )
148160}
0 commit comments