77-include_lib (" opentelemetry_api/include/opentelemetry.hrl" ).
88-include_lib (" opentelemetry_api_experimental/include/otel_meter.hrl" ).
99
10+ % % OTel macros expand to pattern matches that dialyzer flags as unreachable.
11+ -dialyzer ({no_match , [init / 3 , terminate / 3 , record_if_positive / 3 ]}).
12+
1013-record (state , {
1114 next :: any (),
1215 span_ctx :: opentelemetry :span_ctx () | undefined ,
1821 metric_attrs :: map ()
1922}).
2023
24+ -define (OTEL_NOVA_HTTP_ROUTE , otel_nova_http_route ).
25+
2126-spec init (cowboy_stream :streamid (), cowboy_req :req (), cowboy :opts ()) ->
2227 {cowboy_stream :commands (), # state {}}.
2328init (StreamID , Req , Opts ) ->
@@ -84,7 +89,8 @@ terminate(StreamID, Reason, #state{next = Next, span_ctx = SpanCtx, otel_ctx = O
8489 ok ;
8590 Code ->
8691 ? set_attribute ('http.response.status_code' , Code ),
87- maybe_set_error_status (Code )
92+ maybe_set_error_status (Code ),
93+ maybe_set_error_type (Code )
8894 end ,
8995
9096 ? end_span (),
@@ -93,9 +99,19 @@ terminate(StreamID, Reason, #state{next = Next, span_ctx = SpanCtx, otel_ctx = O
9399 % % Record metrics
94100 EndTime = erlang :monotonic_time (),
95101 Duration = erlang :convert_time_unit (EndTime - ReqStart , native , millisecond ) / 1000 ,
102+ RouteAttrs = case erlang :erase (? OTEL_NOVA_HTTP_ROUTE ) of
103+ undefined -> #{};
104+ Route -> #{'http.route' => Route }
105+ end ,
106+ FinalAttrs0 = maps :merge (MetricAttrs , RouteAttrs ),
96107 FinalAttrs = case StatusCode of
97- undefined -> MetricAttrs ;
98- SC -> MetricAttrs #{'http.response.status_code' => SC }
108+ undefined -> FinalAttrs0 ;
109+ SC ->
110+ ErrorAttrs = case SC >= 500 of
111+ true -> #{'error.type' => integer_to_binary (SC )};
112+ false -> #{}
113+ end ,
114+ maps :merge (FinalAttrs0 #{'http.response.status_code' => SC }, ErrorAttrs )
99115 end ,
100116 ? histogram_record ('http.server.request.duration' , Duration , FinalAttrs ),
101117 record_if_positive ('http.server.request.body.size' , ReqBodyLen , FinalAttrs ),
@@ -119,21 +135,25 @@ request_attributes(Req) ->
119135 Host = cowboy_req :host (Req ),
120136 Port = cowboy_req :port (Req ),
121137 {PeerAddr , PeerPort } = cowboy_req :peer (Req ),
138+ PeerBin = peer_to_binary (PeerAddr ),
122139
123- Attrs = #{
140+ Attrs0 = #{
124141 'http.request.method' => Method ,
125142 'url.path' => Path ,
126143 'url.scheme' => Scheme ,
127144 'server.address' => Host ,
128145 'server.port' => Port ,
129- 'network.peer.address' => peer_to_binary (PeerAddr ),
130- 'network.peer.port' => PeerPort
146+ 'network.peer.address' => PeerBin ,
147+ 'network.peer.port' => PeerPort ,
148+ 'network.protocol.version' => protocol_version (Req ),
149+ 'client.address' => client_address (Req , PeerBin )
131150 },
132151
133- case cowboy_req :header (<<" user-agent" >>, Req ) of
134- undefined -> Attrs ;
135- UA -> Attrs #{'user_agent.original' => UA }
136- end .
152+ Attrs1 = case cowboy_req :header (<<" user-agent" >>, Req ) of
153+ undefined -> Attrs0 ;
154+ UA -> Attrs0 #{'user_agent.original' => UA }
155+ end ,
156+ Attrs1 .
137157
138158metric_attributes (Req ) ->
139159 #{
@@ -143,16 +163,44 @@ metric_attributes(Req) ->
143163 'server.port' => cowboy_req :port (Req )
144164 }.
145165
146- peer_to_binary (Addr ) when is_tuple (Addr ) ->
147- list_to_binary (inet :ntoa (Addr ));
148166peer_to_binary (Addr ) ->
149- Addr .
167+ list_to_binary ( inet : ntoa ( Addr )) .
150168
151169maybe_set_error_status (Code ) when Code >= 500 ->
152170 ? set_status (? OTEL_STATUS_ERROR , <<" Server error" >>);
153171maybe_set_error_status (_ ) ->
154172 ok .
155173
174+ maybe_set_error_type (Code ) when Code >= 500 ->
175+ ? set_attribute ('error.type' , integer_to_binary (Code ));
176+ maybe_set_error_type (_ ) ->
177+ ok .
178+
179+ protocol_version (#{version := 'HTTP/1.0' }) -> <<" 1.0" >>;
180+ protocol_version (#{version := 'HTTP/1.1' }) -> <<" 1.1" >>;
181+ protocol_version (#{version := 'HTTP/2' }) -> <<" 2" >>;
182+ protocol_version (#{version := 'HTTP/3' }) -> <<" 3" >>;
183+ protocol_version (_ ) -> <<" 1.1" >>.
184+
185+ client_address (Req , PeerBin ) ->
186+ case cowboy_req :header (<<" x-forwarded-for" >>, Req ) of
187+ undefined ->
188+ case cowboy_req :header (<<" forwarded" >>, Req ) of
189+ undefined -> PeerBin ;
190+ Forwarded -> parse_forwarded_for (Forwarded , PeerBin )
191+ end ;
192+ XFF ->
193+ [First | _ ] = binary :split (XFF , <<" ," >>),
194+ string :trim (First )
195+ end .
196+
197+ parse_forwarded_for (Forwarded , Default ) ->
198+ case re :run (Forwarded , <<" for=\" ?([^;,\" ]+)\" ?" >>,
199+ [{capture , [1 ], binary }, caseless ]) of
200+ {match , [Addr ]} -> string :trim (Addr );
201+ nomatch -> Default
202+ end .
203+
156204record_if_positive (Name , Value , Attrs ) when Value > 0 ->
157205 ? histogram_record (Name , Value , Attrs );
158206record_if_positive (_ , _ , _ ) ->
0 commit comments