|
| 1 | +use http::HeaderMap; |
| 2 | + |
| 3 | +/// [`gRPC` status codes](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md#status-codes-and-their-use-in-grpc) |
| 4 | +/// copied from tonic |
| 5 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] |
| 6 | +#[allow(dead_code)] |
| 7 | +pub enum GrpcCode { |
| 8 | + /// The operation completed successfully. |
| 9 | + Ok = 0, |
| 10 | + |
| 11 | + /// The operation was cancelled. |
| 12 | + Cancelled = 1, |
| 13 | + |
| 14 | + /// Unknown error. |
| 15 | + Unknown = 2, |
| 16 | + |
| 17 | + /// Client specified an invalid argument. |
| 18 | + InvalidArgument = 3, |
| 19 | + |
| 20 | + /// Deadline expired before operation could complete. |
| 21 | + DeadlineExceeded = 4, |
| 22 | + |
| 23 | + /// Some requested entity was not found. |
| 24 | + NotFound = 5, |
| 25 | + |
| 26 | + /// Some entity that we attempted to create already exists. |
| 27 | + AlreadyExists = 6, |
| 28 | + |
| 29 | + /// The caller does not have permission to execute the specified operation. |
| 30 | + PermissionDenied = 7, |
| 31 | + |
| 32 | + /// Some resource has been exhausted. |
| 33 | + ResourceExhausted = 8, |
| 34 | + |
| 35 | + /// The system is not in a state required for the operation's execution. |
| 36 | + FailedPrecondition = 9, |
| 37 | + |
| 38 | + /// The operation was aborted. |
| 39 | + Aborted = 10, |
| 40 | + |
| 41 | + /// Operation was attempted past the valid range. |
| 42 | + OutOfRange = 11, |
| 43 | + |
| 44 | + /// Operation is not implemented or not supported. |
| 45 | + Unimplemented = 12, |
| 46 | + |
| 47 | + /// Internal error. |
| 48 | + Internal = 13, |
| 49 | + |
| 50 | + /// The service is currently unavailable. |
| 51 | + Unavailable = 14, |
| 52 | + |
| 53 | + /// Unrecoverable data loss or corruption. |
| 54 | + DataLoss = 15, |
| 55 | + |
| 56 | + /// The request does not have valid authentication credentials |
| 57 | + Unauthenticated = 16, |
| 58 | +} |
| 59 | + |
| 60 | +/// If "grpc-status" can not be extracted from http response, the status "0" (Ok) is defined |
| 61 | +//TODO create similar but with tonic::Response<B> ? and use of [Status in tonic](https://docs.rs/tonic/latest/tonic/struct.Status.html) (more complete) |
| 62 | +pub fn update_span_from_response<B>( |
| 63 | + span: &tracing::Span, |
| 64 | + response: &http::Response<B>, |
| 65 | + is_spankind_server: bool, |
| 66 | +) { |
| 67 | + let status = status_from_http_header(response.headers()) |
| 68 | + .or_else(|| status_from_http_status(response.status())) |
| 69 | + .unwrap_or(GrpcCode::Ok as u16); |
| 70 | + span.record("rpc.grpc.status_code", status); |
| 71 | + |
| 72 | + if status_is_error(status, is_spankind_server) { |
| 73 | + span.record("otel.status_code", "ERROR"); |
| 74 | + } else { |
| 75 | + span.record("otel.status_code", "OK"); |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +/// based on [Status in tonic](https://docs.rs/tonic/latest/tonic/struct.Status.html#method.from_header_map) |
| 80 | +fn status_from_http_header(headers: &HeaderMap) -> Option<u16> { |
| 81 | + headers |
| 82 | + .get("grpc-status") |
| 83 | + .and_then(|v| v.to_str().ok()) |
| 84 | + .and_then(|v| v.parse::<u16>().ok()) |
| 85 | +} |
| 86 | + |
| 87 | +fn status_from_http_status(status_code: http::StatusCode) -> Option<u16> { |
| 88 | + match status_code { |
| 89 | + // Borrowed from https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md |
| 90 | + http::StatusCode::BAD_REQUEST => Some(GrpcCode::Internal as u16), |
| 91 | + http::StatusCode::UNAUTHORIZED => Some(GrpcCode::Unauthenticated as u16), |
| 92 | + http::StatusCode::FORBIDDEN => Some(GrpcCode::PermissionDenied as u16), |
| 93 | + http::StatusCode::NOT_FOUND => Some(GrpcCode::Unimplemented as u16), |
| 94 | + http::StatusCode::TOO_MANY_REQUESTS |
| 95 | + | http::StatusCode::BAD_GATEWAY |
| 96 | + | http::StatusCode::SERVICE_UNAVAILABLE |
| 97 | + | http::StatusCode::GATEWAY_TIMEOUT => Some(GrpcCode::Unavailable as u16), |
| 98 | + // We got a 200 but no trailers, we can infer that this request is finished. |
| 99 | + // |
| 100 | + // This can happen when a streaming response sends two Status but |
| 101 | + // gRPC requires that we end the stream after the first status. |
| 102 | + // |
| 103 | + // https://github.com/hyperium/tonic/issues/681 |
| 104 | + http::StatusCode::OK => None, |
| 105 | + _ => Some(GrpcCode::Unknown as u16), |
| 106 | + } |
| 107 | +} |
| 108 | + |
| 109 | +#[inline] |
| 110 | +#[must_use] |
| 111 | +/// see [Semantic Conventions for gRPC | OpenTelemetry](https://opentelemetry.io/docs/specs/semconv/rpc/grpc/) |
| 112 | +/// see [GRPC Core: Status codes and their use in gRPC](https://grpc.github.io/grpc/core/md_doc_statuscodes.html) |
| 113 | +pub fn status_is_error(status: u16, is_spankind_server: bool) -> bool { |
| 114 | + if is_spankind_server { |
| 115 | + status == 2 || status == 4 || status == 12 || status == 13 || status == 14 || status == 15 |
| 116 | + } else { |
| 117 | + status != 0 |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +fn update_span_from_error<E>(span: &tracing::Span, error: &E) |
| 122 | +where |
| 123 | + E: std::error::Error, |
| 124 | +{ |
| 125 | + span.record("otel.status_code", "ERROR"); |
| 126 | + span.record("rpc.grpc.status_code", 2); |
| 127 | + span.record("exception.message", error.to_string()); |
| 128 | + error |
| 129 | + .source() |
| 130 | + .map(|s| span.record("exception.message", s.to_string())); |
| 131 | +} |
| 132 | + |
| 133 | +pub fn update_span_from_response_or_error<B, E>( |
| 134 | + span: &tracing::Span, |
| 135 | + response: &Result<http::Response<B>, E>, |
| 136 | +) where |
| 137 | + E: std::error::Error, |
| 138 | +{ |
| 139 | + match response { |
| 140 | + Ok(response) => { |
| 141 | + update_span_from_response(span, response, true); |
| 142 | + } |
| 143 | + Err(err) => { |
| 144 | + update_span_from_error(span, err); |
| 145 | + } |
| 146 | + } |
| 147 | +} |
| 148 | + |
| 149 | +// [opentelemetry-specification/.../rpc.md](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/rpc.md) |
| 150 | +//TODO create similar but with tonic::Request<B> ? |
| 151 | +#[allow(clippy::needless_pass_by_value)] |
| 152 | +pub(crate) fn make_span_from_request<B>( |
| 153 | + req: &http::Request<B>, |
| 154 | + kind: opentelemetry::trace::SpanKind, |
| 155 | +) -> tracing::Span { |
| 156 | + use crate::http::{extract_service_method, http_host, user_agent}; |
| 157 | + use crate::otel_trace_span; |
| 158 | + use tracing::field::Empty; |
| 159 | + |
| 160 | + let (service, method) = extract_service_method(req.uri()); |
| 161 | + otel_trace_span!( |
| 162 | + "GRPC request", |
| 163 | + http.user_agent = %user_agent(req), |
| 164 | + otel.name = format!("{service}/{method}"), |
| 165 | + otel.kind = ?kind, |
| 166 | + otel.status_code = Empty, |
| 167 | + rpc.system ="grpc", |
| 168 | + rpc.service = %service, |
| 169 | + rpc.method = %method, |
| 170 | + rpc.grpc.status_code = Empty, // to set on response |
| 171 | + server.address = %http_host(req), |
| 172 | + exception.message = Empty, // to set on response |
| 173 | + exception.details = Empty, // to set on response |
| 174 | + ) |
| 175 | +} |
| 176 | + |
| 177 | +// if let Some(host_name) = SYSTEM.host_name() { |
| 178 | +// attributes.push(NET_HOST_NAME.string(host_name)); |
| 179 | +// } |
| 180 | + |
| 181 | +#[cfg(test)] |
| 182 | +mod tests { |
| 183 | + use super::*; |
| 184 | + use rstest::rstest; |
| 185 | + |
| 186 | + #[rstest] |
| 187 | + #[case(0)] |
| 188 | + #[case(16)] |
| 189 | + #[case(-1)] |
| 190 | + fn test_status_from_http_header(#[case] input: i32) { |
| 191 | + let mut headers = http::HeaderMap::new(); |
| 192 | + headers.insert("grpc-status", input.to_string().parse().unwrap()); |
| 193 | + if input > -1 { |
| 194 | + assert_eq!( |
| 195 | + status_from_http_header(&headers), |
| 196 | + Some(u16::try_from(input).unwrap()) |
| 197 | + ); |
| 198 | + } else { |
| 199 | + assert_eq!(status_from_http_header(&headers), None); |
| 200 | + } |
| 201 | + } |
| 202 | +} |
0 commit comments