@@ -12,13 +12,21 @@ import (
1212 log "github.com/sirupsen/logrus"
1313 "golang.org/x/net/context"
1414
15+ "go.opentelemetry.io/otel"
16+ "go.opentelemetry.io/otel/attribute"
17+ "go.opentelemetry.io/otel/codes"
18+ "go.opentelemetry.io/otel/propagation"
19+ "go.opentelemetry.io/otel/trace"
20+
1521 "github.com/numkem/msgscript"
1622 "github.com/numkem/msgscript/executor"
1723)
1824
1925const DEFAULT_HTTP_PORT = 7643
2026const DEFAULT_HTTP_TIMEOUT = 5 * time .Second
2127
28+ var tracer = otel .Tracer ("http-nats-proxy" )
29+
2230type httpNatsProxy struct {
2331 port string
2432 nc * nats.Conn
@@ -40,34 +48,56 @@ func NewHttpNatsProxy(port int, natsURL string) (*httpNatsProxy, error) {
4048}
4149
4250func (p * httpNatsProxy ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
51+ // Extract context from incoming request headers
52+ ctx := otel .GetTextMapPropagator ().Extract (r .Context (), propagation .HeaderCarrier (r .Header ))
53+
54+ // Start a new span for the HTTP request
55+ ctx , span := tracer .Start (ctx , "http.request" ,
56+ trace .WithSpanKind (trace .SpanKindServer ),
57+ trace .WithAttributes (
58+ attribute .String ("http.method" , r .Method ),
59+ attribute .String ("http.url" , r .URL .String ()),
60+ attribute .String ("http.remote_addr" , r .RemoteAddr ),
61+ ),
62+ )
63+ defer span .End ()
64+
4365 defer r .Body .Close ()
4466
4567 // URL should look like /funcs.foobar
4668 // Where funcs.foobar is the subject for NATS
4769 ss := strings .Split (r .URL .Path , "/" )
4870 // Validate URL structure
4971 if len (ss ) < 2 {
72+ span .SetStatus (codes .Error , "Invalid URL structure" )
73+ span .SetAttributes (attribute .Int ("http.status_code" , http .StatusBadRequest ))
5074 w .WriteHeader (http .StatusBadRequest )
5175 w .Write ([]byte ("URL should be in the pattern of /<subject>" ))
5276 return
5377 }
5478 subject := ss [1 ]
79+ span .SetAttributes (attribute .String ("nats.subject" , subject ))
5580
5681 fields := log.Fields {
5782 "subject" : subject ,
5883 "client" : r .RemoteAddr ,
5984 }
6085 log .WithFields (fields ).Info ("Received HTTP request" )
6186
87+ // Read request body with tracing
6288 payload , err := io .ReadAll (r .Body )
6389 if err != nil {
90+ span .RecordError (err )
91+ span .SetStatus (codes .Error , "Failed to read request body" )
92+ span .SetAttributes (attribute .Int ("http.status_code" , http .StatusInternalServerError ))
6493 w .WriteHeader (http .StatusInternalServerError )
6594 _ , err = fmt .Fprintf (w , "failed to read request body: %s" , err )
6695 if err != nil {
6796 log .WithFields (fields ).Errorf ("failed to write payload: %v" , err )
6897 }
6998 return
7099 }
100+ span .SetAttributes (attribute .Int ("http.request.body_size" , len (payload )))
71101
72102 // We can override the HTTP timeout by passing the `_timeout` query string
73103 timeout := DEFAULT_HTTP_TIMEOUT
@@ -77,8 +107,9 @@ func (p *httpNatsProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
77107 timeout = DEFAULT_HTTP_TIMEOUT
78108 }
79109 }
110+ span .SetAttributes (attribute .String ("http.timeout" , timeout .String ()))
80111
81- ctx , cancel := context .WithTimeout (r . Context () , timeout )
112+ ctx , cancel := context .WithTimeout (ctx , timeout )
82113 defer cancel ()
83114
84115 url := strings .ReplaceAll (r .URL .String (), "/" + subject , "" )
@@ -90,29 +121,61 @@ func (p *httpNatsProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
90121 URL : url ,
91122 })
92123 if err != nil {
124+ span .RecordError (err )
125+ span .SetStatus (codes .Error , "Failed to encode message" )
93126 log .WithFields (fields ).Errorf ("failed to encode message: %v" , err )
94127 return
95128 }
96129
97- msg , err := p .nc .RequestWithContext (ctx , subject , body )
130+ // Start a child span for the NATS request
131+ ctx , natsSpan := tracer .Start (ctx , "nats.request" ,
132+ trace .WithSpanKind (trace .SpanKindClient ),
133+ trace .WithAttributes (
134+ attribute .String ("nats.subject" , subject ),
135+ attribute .Int ("nats.message_size" , len (body )),
136+ ),
137+ )
138+
139+ // Inject trace context into NATS message headers
140+ msg := nats .NewMsg (subject )
141+ msg .Data = body
142+ otel .GetTextMapPropagator ().Inject (ctx , natsHeaderCarrier (msg .Header ))
143+
144+ response , err := p .nc .RequestMsgWithContext (ctx , msg )
98145 if err != nil {
146+ natsSpan .RecordError (err )
147+ natsSpan .SetStatus (codes .Error , "NATS request failed" )
148+ natsSpan .End ()
149+ span .SetStatus (codes .Error , "Service unavailable" )
150+ span .SetAttributes (attribute .Int ("http.status_code" , http .StatusServiceUnavailable ))
99151 w .WriteHeader (http .StatusServiceUnavailable )
100152 w .Write ([]byte (err .Error ()))
101153 return
102154 }
155+ natsSpan .SetAttributes (attribute .Int ("nats.response_size" , len (msg .Data )))
156+ natsSpan .SetStatus (codes .Ok , "" )
157+ natsSpan .End ()
103158
104159 rep := new (executor.Reply )
105- err = json .Unmarshal (msg .Data , rep )
160+ err = json .Unmarshal (response .Data , rep )
106161 if err != nil {
162+ span .RecordError (err )
163+ span .SetStatus (codes .Error , "Failed to unmarshal response" )
164+ span .SetAttributes (attribute .Int ("http.status_code" , http .StatusFailedDependency ))
107165 w .WriteHeader (http .StatusFailedDependency )
108166 fmt .Fprintf (w , "Error: %v" , err )
109167 return
110168 }
111169
112170 if rep .Error != "" {
171+ span .SetAttributes (attribute .String ("executor.error" , rep .Error ))
113172 if rep .Error == (& executor.NoScriptFoundError {}).Error () {
173+ span .SetStatus (codes .Error , "Script not found" )
174+ span .SetAttributes (attribute .Int ("http.status_code" , http .StatusNotFound ))
114175 w .WriteHeader (http .StatusNotFound )
115176 } else {
177+ span .SetStatus (codes .Error , rep .Error )
178+ span .SetAttributes (attribute .Int ("http.status_code" , http .StatusInternalServerError ))
116179 w .WriteHeader (http .StatusInternalServerError )
117180 }
118181
@@ -126,6 +189,7 @@ func (p *httpNatsProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
126189
127190 // Go through all the scripts to see if one is HTML
128191 if t , sr := hasHTMLResult (rep .AllResults ); t {
192+ span .SetAttributes (attribute .Bool ("response.is_html" , true ))
129193 var hasContentType bool
130194 for k , v := range sr .Headers {
131195 if k == "Content-Type" {
@@ -136,27 +200,43 @@ func (p *httpNatsProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
136200 if ! hasContentType {
137201 w .Header ().Add ("Content-Type" , "text/html" )
138202 }
203+ span .SetAttributes (
204+ attribute .Int ("http.status_code" , sr .Code ),
205+ attribute .Int ("http.response.body_size" , len (sr .Payload )),
206+ )
139207 w .WriteHeader (sr .Code )
140208
141209 _ , err = w .Write (sr .Payload )
142210 if err != nil {
211+ span .RecordError (err )
143212 log .WithFields (fields ).Errorf ("failed to write reply back to HTTP response: %v" , err )
144213 }
145214
215+ span .SetStatus (codes .Ok , "" )
146216 // Since only the HTML page reply can "win" we ignore the rest
147217 return
148218 }
149219
150220 // Convert the results to bytes
221+ span .SetAttributes (attribute .Bool ("response.is_html" , false ))
151222 rr , err := json .Marshal (rep .AllResults )
152223 if err != nil {
224+ span .RecordError (err )
153225 log .WithFields (fields ).Errorf ("failed to serialize all results to JSON: %v" , err )
154226 }
155227
228+ span .SetAttributes (
229+ attribute .Int ("http.status_code" , http .StatusOK ),
230+ attribute .Int ("http.response.body_size" , len (rr )),
231+ )
232+
156233 _ , err = w .Write (rr )
157234 if err != nil {
235+ span .RecordError (err )
158236 log .WithFields (fields ).Errorf ("failed to write reply back to HTTP response: %v" , err )
159237 }
238+
239+ span .SetStatus (codes .Ok , "" )
160240}
161241
162242func hasHTMLResult (results map [string ]* executor.ScriptResult ) (bool , * executor.ScriptResult ) {
0 commit comments