use http::HeaderMap;
use opentelemetry_semantic_conventions::attribute::{
EXCEPTION_MESSAGE, OTEL_STATUS_CODE, RPC_GRPC_STATUS_CODE,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[allow(dead_code)]
pub enum GrpcCode {
Ok = 0,
Cancelled = 1,
Unknown = 2,
InvalidArgument = 3,
DeadlineExceeded = 4,
NotFound = 5,
AlreadyExists = 6,
PermissionDenied = 7,
ResourceExhausted = 8,
FailedPrecondition = 9,
Aborted = 10,
OutOfRange = 11,
Unimplemented = 12,
Internal = 13,
Unavailable = 14,
DataLoss = 15,
Unauthenticated = 16,
}
pub fn update_span_from_response<B>(
span: &tracing::Span,
response: &http::Response<B>,
is_spankind_server: bool,
) {
let status = status_from_http_header(response.headers())
.or_else(|| status_from_http_status(response.status()))
.unwrap_or(GrpcCode::Ok as u16);
span.record(RPC_GRPC_STATUS_CODE, status);
if status_is_error(status, is_spankind_server) {
span.record(OTEL_STATUS_CODE, "ERROR");
} else {
span.record(OTEL_STATUS_CODE, "OK");
}
}
fn status_from_http_header(headers: &HeaderMap) -> Option<u16> {
headers
.get("grpc-status")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u16>().ok())
}
fn status_from_http_status(status_code: http::StatusCode) -> Option<u16> {
match status_code {
http::StatusCode::BAD_REQUEST => Some(GrpcCode::Internal as u16),
http::StatusCode::UNAUTHORIZED => Some(GrpcCode::Unauthenticated as u16),
http::StatusCode::FORBIDDEN => Some(GrpcCode::PermissionDenied as u16),
http::StatusCode::NOT_FOUND => Some(GrpcCode::Unimplemented as u16),
http::StatusCode::TOO_MANY_REQUESTS
| http::StatusCode::BAD_GATEWAY
| http::StatusCode::SERVICE_UNAVAILABLE
| http::StatusCode::GATEWAY_TIMEOUT => Some(GrpcCode::Unavailable as u16),
http::StatusCode::OK => None,
_ => Some(GrpcCode::Unknown as u16),
}
}
#[inline]
#[must_use]
pub fn status_is_error(status: u16, is_spankind_server: bool) -> bool {
if is_spankind_server {
status == 2 || status == 4 || status == 12 || status == 13 || status == 14 || status == 15
} else {
status != 0
}
}
fn update_span_from_error<E>(span: &tracing::Span, error: &E)
where
E: std::error::Error,
{
span.record(OTEL_STATUS_CODE, "ERROR");
span.record(RPC_GRPC_STATUS_CODE, 2);
span.record(EXCEPTION_MESSAGE, error.to_string());
error
.source()
.map(|s| span.record(EXCEPTION_MESSAGE, s.to_string()));
}
pub fn update_span_from_response_or_error<B, E>(
span: &tracing::Span,
response: &Result<http::Response<B>, E>,
) where
E: std::error::Error,
{
match response {
Ok(response) => {
update_span_from_response(span, response, true);
}
Err(err) => {
update_span_from_error(span, err);
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn make_span_from_request<B>(
req: &http::Request<B>,
kind: opentelemetry::trace::SpanKind,
) -> tracing::Span {
use crate::http::{extract_service_method, http_host, user_agent};
use crate::otel_trace_span;
use tracing::field::Empty;
let (service, method) = extract_service_method(req.uri());
otel_trace_span!(
"GRPC request",
http.user_agent = %user_agent(req),
otel.name = format!("{service}/{method}"),
otel.kind = ?kind,
otel.status_code = Empty,
rpc.system ="grpc",
rpc.service = %service,
rpc.method = %method,
rpc.grpc.status_code = Empty, server.address = %http_host(req),
exception.message = Empty, exception.details = Empty, )
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(0)]
#[case(16)]
#[case(-1)]
fn test_status_from_http_header(#[case] input: i32) {
let mut headers = http::HeaderMap::new();
headers.insert("grpc-status", input.to_string().parse().unwrap());
if input > -1 {
assert_eq!(
status_from_http_header(&headers),
Some(u16::try_from(input).unwrap())
);
} else {
assert_eq!(status_from_http_header(&headers), None);
}
}
}