0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-04 01:44:26 -05:00

feat(http): add otel metrics (#28034)

Signed-off-by: Leo Kettmeir <crowlkats@toaxl.com>
Co-authored-by: Luca Casonato <hello@lcas.dev>
This commit is contained in:
Leo Kettmeir 2025-02-18 23:19:52 +01:00 committed by GitHub
parent f62fc9e81f
commit bf79971c95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1579 additions and 245 deletions

1
Cargo.lock generated
View file

@ -2045,6 +2045,7 @@ dependencies = [
"deno_core",
"deno_error",
"deno_net",
"deno_telemetry",
"deno_websocket",
"flate2",
"http 0.2.12",

View file

@ -13,6 +13,7 @@ import {
op_http_close_after_finish,
op_http_get_request_headers,
op_http_get_request_method_and_url,
op_http_metric_handle_otel_error,
op_http_read_request_body,
op_http_request_on_cancel,
op_http_serve,
@ -89,6 +90,7 @@ import { SymbolAsyncDispose } from "ext:deno_web/00_infra.js";
import {
builtinTracer,
enterSpan,
METRICS_ENABLED,
TRACING_ENABLED,
} from "ext:deno_telemetry/telemetry.ts";
import {
@ -573,6 +575,9 @@ function mapToCallback(context, callback, onError) {
);
}
} catch (error) {
if (METRICS_ENABLED) {
op_http_metric_handle_otel_error(req);
}
// deno-lint-ignore no-console
console.error("Exception in onError while handling exception", error);
response = internalServerError();

View file

@ -30,6 +30,7 @@ cache_control.workspace = true
deno_core.workspace = true
deno_error.workspace = true
deno_net.workspace = true
deno_telemetry.workspace = true
deno_websocket.workspace = true
flate2.workspace = true
http.workspace = true

View file

@ -270,8 +270,12 @@ pub async fn op_http_upgrade_websocket_next(
// Stage 1: set the response to 101 Switching Protocols and send it
let upgrade = http.upgrade()?;
{
{
http.otel_info_set_status(StatusCode::SWITCHING_PROTOCOLS.as_u16());
}
let mut response_parts = http.response_parts();
response_parts.status = StatusCode::SWITCHING_PROTOCOLS;
for (name, value) in headers {
response_parts.headers.append(
HeaderName::from_bytes(&name).unwrap(),
@ -305,7 +309,10 @@ fn set_promise_complete(http: Rc<HttpRecord>, status: u16) {
// The Javascript code should never provide a status that is invalid here (see 23_response.js), so we
// will quietly ignore invalid values.
if let Ok(code) = StatusCode::from_u16(status) {
http.response_parts().status = code;
{
http.response_parts().status = code;
}
http.otel_info_set_status(status);
}
http.complete();
}
@ -713,7 +720,10 @@ fn set_response(
// The Javascript code should never provide a status that is invalid here (see 23_response.js), so we
// will quietly ignore invalid values.
if let Ok(code) = StatusCode::from_u16(status) {
http.response_parts().status = code;
{
http.response_parts().status = code;
}
http.otel_info_set_status(status);
}
} else if force_instantiate_body {
response_fn(Compression::None).abort();
@ -1404,3 +1414,12 @@ pub async fn op_raw_write_vectored(
let nwritten = resource.write_vectored(&buf1, &buf2).await?;
Ok(nwritten)
}
#[op2(fast)]
pub fn op_http_metric_handle_otel_error(external: *const c_void) {
let http =
// SAFETY: external is deleted before calling this op.
unsafe { take_external!(external, "op_http_metric_handle_otel_error") };
http.otel_info_set_error("user");
}

View file

@ -53,6 +53,10 @@ use deno_core::ResourceId;
use deno_core::StringOrBuffer;
use deno_error::JsErrorBox;
use deno_net::raw::NetworkStream;
use deno_telemetry::Histogram;
use deno_telemetry::MeterProvider;
use deno_telemetry::UpDownCounter;
use deno_telemetry::OTEL_GLOBALS;
use deno_websocket::ws_create_server_stream;
use flate2::write::GzEncoder;
use flate2::Compression;
@ -70,6 +74,7 @@ use hyper_v014::Body;
use hyper_v014::HeaderMap;
use hyper_v014::Request;
use hyper_v014::Response;
use once_cell::sync::OnceCell;
use serde::Serialize;
use tokio::io::AsyncRead;
use tokio::io::AsyncWrite;
@ -100,6 +105,15 @@ pub use request_properties::HttpRequestProperties;
pub use service::UpgradeUnavailableError;
pub use websocket_upgrade::WebSocketUpgradeError;
struct OtelCollectors {
duration: Histogram<f64>,
active_requests: UpDownCounter<i64>,
request_size: Histogram<u64>,
response_size: Histogram<u64>,
}
static OTEL_COLLECTORS: OnceCell<OtelCollectors> = OnceCell::new();
#[derive(Debug, Default, Clone, Copy)]
pub struct Options {
/// By passing a hook function, the caller can customize various configuration
@ -156,6 +170,7 @@ deno_core::extension!(
http_next::op_http_wait,
http_next::op_http_close,
http_next::op_http_cancel,
http_next::op_http_metric_handle_otel_error,
],
esm = ["00_serve.ts", "01_http.js", "02_websocket.ts"],
options = {
@ -231,6 +246,287 @@ impl From<tokio::net::unix::SocketAddr> for HttpSocketAddr {
}
}
struct OtelInfo {
attributes: OtelInfoAttributes,
duration: Option<std::time::Instant>,
request_size: Option<u64>,
response_size: Option<u64>,
}
struct OtelInfoAttributes {
http_request_method: Cow<'static, str>,
network_protocol_version: &'static str,
url_scheme: Cow<'static, str>,
server_address: Option<String>,
server_port: Option<i64>,
error_type: Option<&'static str>,
http_response_status_code: Option<i64>,
}
impl OtelInfoAttributes {
fn method(method: &http::method::Method) -> Cow<'static, str> {
use http::method::Method;
match *method {
Method::GET => Cow::Borrowed("GET"),
Method::POST => Cow::Borrowed("POST"),
Method::PUT => Cow::Borrowed("PUT"),
Method::DELETE => Cow::Borrowed("DELETE"),
Method::HEAD => Cow::Borrowed("HEAD"),
Method::OPTIONS => Cow::Borrowed("OPTIONS"),
Method::CONNECT => Cow::Borrowed("CONNECT"),
Method::PATCH => Cow::Borrowed("PATCH"),
Method::TRACE => Cow::Borrowed("TRACE"),
_ => Cow::Owned(method.to_string()),
}
}
fn method_v02(method: &http_v02::method::Method) -> Cow<'static, str> {
use http_v02::method::Method;
match *method {
Method::GET => Cow::Borrowed("GET"),
Method::POST => Cow::Borrowed("POST"),
Method::PUT => Cow::Borrowed("PUT"),
Method::DELETE => Cow::Borrowed("DELETE"),
Method::HEAD => Cow::Borrowed("HEAD"),
Method::OPTIONS => Cow::Borrowed("OPTIONS"),
Method::CONNECT => Cow::Borrowed("CONNECT"),
Method::PATCH => Cow::Borrowed("PATCH"),
Method::TRACE => Cow::Borrowed("TRACE"),
_ => Cow::Owned(method.to_string()),
}
}
fn version(version: http::Version) -> &'static str {
use http::Version;
match version {
Version::HTTP_09 => "0.9",
Version::HTTP_10 => "1.0",
Version::HTTP_11 => "1.1",
Version::HTTP_2 => "2",
Version::HTTP_3 => "3",
_ => unreachable!(),
}
}
fn version_v02(version: http_v02::Version) -> &'static str {
use http_v02::Version;
match version {
Version::HTTP_09 => "0.9",
Version::HTTP_10 => "1.0",
Version::HTTP_11 => "1.1",
Version::HTTP_2 => "2",
Version::HTTP_3 => "3",
_ => unreachable!(),
}
}
fn for_counter(&self) -> Vec<deno_telemetry::KeyValue> {
let mut attributes = vec![
deno_telemetry::KeyValue::new(
"http.request.method",
self.http_request_method.clone(),
),
deno_telemetry::KeyValue::new("url.scheme", self.url_scheme.clone()),
];
if let Some(address) = self.server_address.clone() {
attributes.push(deno_telemetry::KeyValue::new("server.address", address));
}
if let Some(port) = self.server_port {
attributes.push(deno_telemetry::KeyValue::new("server.port", port));
}
attributes
}
fn for_histogram(&self) -> Vec<deno_telemetry::KeyValue> {
let mut histogram_attributes = vec![
deno_telemetry::KeyValue::new(
"http.request.method",
self.http_request_method.clone(),
),
deno_telemetry::KeyValue::new("url.scheme", self.url_scheme.clone()),
deno_telemetry::KeyValue::new(
"network.protocol.version",
self.network_protocol_version,
),
];
if let Some(address) = self.server_address.clone() {
histogram_attributes
.push(deno_telemetry::KeyValue::new("server.address", address));
}
if let Some(port) = self.server_port {
histogram_attributes
.push(deno_telemetry::KeyValue::new("server.port", port));
}
if let Some(status_code) = self.http_response_status_code {
histogram_attributes.push(deno_telemetry::KeyValue::new(
"http.response.status_code",
status_code,
));
}
if let Some(error) = self.error_type {
histogram_attributes
.push(deno_telemetry::KeyValue::new("error.type", error));
}
histogram_attributes
}
}
impl OtelInfo {
fn new(
instant: std::time::Instant,
request_size: u64,
attributes: OtelInfoAttributes,
) -> Self {
let otel = OTEL_GLOBALS.get().unwrap();
let collectors = OTEL_COLLECTORS.get_or_init(|| {
let meter = otel
.meter_provider
.meter_with_scope(otel.builtin_instrumentation_scope.clone());
let duration = meter
.f64_histogram("http.server.request.duration")
.with_unit("s")
.with_description("Duration of HTTP server requests.")
.with_boundaries(vec![
0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0,
7.5, 10.0,
])
.build();
let active_requests = meter
.i64_up_down_counter("http.server.active_requests")
.with_unit("{request}")
.with_description("Number of active HTTP server requests.")
.build();
let request_size = meter
.u64_histogram("http.server.request.body.size")
.with_unit("By")
.with_description("Size of HTTP server request bodies.")
.with_boundaries(vec![
0.0,
100.0,
1000.0,
10000.0,
100000.0,
1000000.0,
10000000.0,
100000000.0,
1000000000.0,
])
.build();
let response_size = meter
.u64_histogram("http.server.response.body.size")
.with_unit("By")
.with_description("Size of HTTP server response bodies.")
.with_boundaries(vec![
0.0,
100.0,
1000.0,
10000.0,
100000.0,
1000000.0,
10000000.0,
100000000.0,
1000000000.0,
])
.build();
OtelCollectors {
duration,
active_requests,
request_size,
response_size,
}
});
collectors.active_requests.add(1, &attributes.for_counter());
Self {
attributes,
duration: Some(instant),
request_size: Some(request_size),
response_size: Some(0),
}
}
fn handle_duration_and_request_size(&mut self) {
let collectors = OTEL_COLLECTORS.get().unwrap();
let attributes = self.attributes.for_histogram();
if let Some(duration) = self.duration.take() {
let duration = duration.elapsed();
collectors
.duration
.record(duration.as_secs_f64(), &attributes);
}
if let Some(request_size) = self.request_size.take() {
let collectors = OTEL_COLLECTORS.get().unwrap();
collectors.request_size.record(request_size, &attributes);
}
}
}
impl Drop for OtelInfo {
fn drop(&mut self) {
let collectors = OTEL_COLLECTORS.get().unwrap();
self.handle_duration_and_request_size();
collectors
.active_requests
.add(-1, &self.attributes.for_counter());
if let Some(response_size) = self.response_size {
collectors
.response_size
.record(response_size, &self.attributes.for_histogram());
}
}
}
fn handle_error_otel(
otel: &Option<Rc<RefCell<Option<OtelInfo>>>>,
error: &HttpError,
) {
if let Some(otel) = otel.as_ref() {
let mut maybe_otel_info = otel.borrow_mut();
if let Some(otel_info) = maybe_otel_info.as_mut() {
otel_info.attributes.error_type = Some(match error {
HttpError::Resource(_) => "resource",
HttpError::Canceled(_) => "canceled",
HttpError::HyperV014(_) => "hyper",
HttpError::InvalidHeaderName(_) => "invalid header name",
HttpError::InvalidHeaderValue(_) => "invalid header value",
HttpError::Http(_) => "http",
HttpError::ResponseHeadersAlreadySent => {
"response headers already sent"
}
HttpError::ConnectionClosedWhileSendingResponse => {
"connection closed while sending response"
}
HttpError::AlreadyInUse => "already in use",
HttpError::Io(_) => "io",
HttpError::NoResponseHeaders => "no response headers",
HttpError::ResponseAlreadyCompleted => "response already completed",
HttpError::UpgradeBodyUsed => "upgrade body used",
HttpError::Other(_) => "unknown",
});
}
}
}
struct HttpConnResource {
addr: HttpSocketAddr,
scheme: &'static str,
@ -300,6 +596,8 @@ impl HttpConnResource {
let (request_tx, request_rx) = oneshot::channel();
let (response_tx, response_rx) = oneshot::channel();
let otel_instant = OTEL_GLOBALS.get().map(|_| std::time::Instant::now());
let acceptor = HttpAcceptor::new(request_tx, response_rx);
self.acceptors_tx.unbounded_send(acceptor).ok()?;
@ -317,11 +615,37 @@ impl HttpConnResource {
.unwrap_or(Encoding::Identity)
};
let otel_info = OTEL_GLOBALS.get().map(|_| {
let size_hint = request.size_hint();
Rc::new(RefCell::new(Some(OtelInfo::new(
otel_instant.unwrap(),
size_hint.upper().unwrap_or(size_hint.lower()),
OtelInfoAttributes {
http_request_method: OtelInfoAttributes::method_v02(
request.method(),
),
url_scheme: Cow::Borrowed(self.scheme),
network_protocol_version: OtelInfoAttributes::version_v02(
request.version(),
),
server_address: request.uri().host().map(|host| host.to_string()),
server_port: request.uri().port_u16().map(|port| port as i64),
error_type: Default::default(),
http_response_status_code: Default::default(),
},
))))
});
let method = request.method().to_string();
let url = req_url(&request, self.scheme, &self.addr);
let read_stream = HttpStreamReadResource::new(self, request);
let write_stream =
HttpStreamWriteResource::new(self, response_tx, accept_encoding);
let read_stream =
HttpStreamReadResource::new(self, request, otel_info.clone());
let write_stream = HttpStreamWriteResource::new(
self,
response_tx,
accept_encoding,
otel_info,
);
Some((read_stream, write_stream, method, url))
};
@ -438,22 +762,29 @@ pub struct HttpStreamReadResource {
pub rd: AsyncRefCell<HttpRequestReader>,
cancel_handle: CancelHandle,
size: SizeHint,
otel_info: Option<Rc<RefCell<Option<OtelInfo>>>>,
}
pub struct HttpStreamWriteResource {
conn: Rc<HttpConnResource>,
wr: AsyncRefCell<HttpResponseWriter>,
accept_encoding: Encoding,
otel_info: Option<Rc<RefCell<Option<OtelInfo>>>>,
}
impl HttpStreamReadResource {
fn new(conn: &Rc<HttpConnResource>, request: Request<Body>) -> Self {
fn new(
conn: &Rc<HttpConnResource>,
request: Request<Body>,
otel_info: Option<Rc<RefCell<Option<OtelInfo>>>>,
) -> Self {
let size = request.body().size_hint();
Self {
_conn: conn.clone(),
rd: HttpRequestReader::Headers(request).into(),
size,
cancel_handle: CancelHandle::new(),
otel_info,
}
}
}
@ -530,11 +861,13 @@ impl HttpStreamWriteResource {
conn: &Rc<HttpConnResource>,
response_tx: oneshot::Sender<Response<Body>>,
accept_encoding: Encoding,
otel_info: Option<Rc<RefCell<Option<OtelInfo>>>>,
) -> Self {
Self {
conn: conn.clone(),
wr: HttpResponseWriter::Headers(response_tx).into(),
accept_encoding,
otel_info,
}
}
}
@ -786,6 +1119,14 @@ async fn op_http_write_headers(
let (new_wr, body) = http_response(data, compressing, encoding)?;
let body = builder.status(status).body(body)?;
if let Some(otel) = stream.otel_info.as_ref() {
let mut otel = otel.borrow_mut();
if let Some(otel_info) = otel.as_mut() {
otel_info.attributes.http_response_status_code = Some(status as _);
otel_info.handle_duration_and_request_size();
}
}
let mut old_wr = RcRef::map(&stream, |r| &r.wr).borrow_mut().await;
let response_tx = match replace(&mut *old_wr, new_wr) {
HttpResponseWriter::Headers(response_tx) => response_tx,
@ -810,7 +1151,8 @@ fn op_http_headers(
let stream = state.resource_table.get::<HttpStreamReadResource>(rid)?;
let rd = RcRef::map(&stream, |r| &r.rd)
.try_borrow()
.ok_or(HttpError::AlreadyInUse)?;
.ok_or(HttpError::AlreadyInUse)
.inspect_err(|e| handle_error_otel(&stream.otel_info, e))?;
match &*rd {
HttpRequestReader::Headers(request) => Ok(req_headers(request.headers())),
HttpRequestReader::Body(headers, _) => Ok(req_headers(headers)),
@ -1025,6 +1367,15 @@ async fn op_http_write(
.get::<HttpStreamWriteResource>(rid)?;
let mut wr = RcRef::map(&stream, |r| &r.wr).borrow_mut().await;
if let Some(otel) = stream.otel_info.as_ref() {
let mut maybe_otel_info = otel.borrow_mut();
if let Some(otel_info) = maybe_otel_info.as_mut() {
if let Some(response_size) = otel_info.response_size.as_mut() {
*response_size += buf.len() as u64;
}
}
}
match &mut *wr {
HttpResponseWriter::Headers(_) => Err(HttpError::NoResponseHeaders),
HttpResponseWriter::Closed => Err(HttpError::ResponseAlreadyCompleted),
@ -1125,13 +1476,17 @@ async fn op_http_upgrade_websocket(
let request = match &mut *rd {
HttpRequestReader::Headers(request) => request,
_ => return Err(HttpError::UpgradeBodyUsed),
_ => {
return Err(HttpError::UpgradeBodyUsed)
.inspect_err(|e| handle_error_otel(&stream.otel_info, e))
}
};
let (transport, bytes) = extract_network_stream(
hyper_v014::upgrade::on(request)
.await
.map_err(|err| HttpError::HyperV014(Arc::new(err)))?,
.map_err(|err| HttpError::HyperV014(Arc::new(err)))
.inspect_err(|e| handle_error_otel(&stream.otel_info, e))?,
);
Ok(ws_create_server_stream(
&mut state.borrow_mut(),

View file

@ -1,4 +1,6 @@
// Copyright 2018-2025 the Deno authors. MIT license.
use std::borrow::Cow;
use std::cell::Cell;
use std::cell::Ref;
use std::cell::RefCell;
@ -30,6 +32,8 @@ use tokio::sync::oneshot;
use crate::request_properties::HttpConnectionProperties;
use crate::response_body::ResponseBytesInner;
use crate::response_body::ResponseStreamResult;
use crate::OtelInfo;
use crate::OtelInfoAttributes;
pub type Request = hyper::Request<Incoming>;
pub type Response = hyper::Response<HttpRecordResponse>;
@ -184,12 +188,38 @@ pub(crate) async fn handle_request(
server_state: SignallingRc<HttpServerState>, // Keep server alive for duration of this future.
tx: tokio::sync::mpsc::Sender<Rc<HttpRecord>>,
) -> Result<Response, hyper_v014::Error> {
let otel_info = if deno_telemetry::OTEL_GLOBALS.get().is_some() {
let instant = std::time::Instant::now();
let size_hint = request.size_hint();
Some(OtelInfo::new(
instant,
size_hint.upper().unwrap_or(size_hint.lower()),
OtelInfoAttributes {
http_request_method: OtelInfoAttributes::method(request.method()),
url_scheme: request
.uri()
.scheme_str()
.map(|s| Cow::Owned(s.to_string()))
.unwrap_or_else(|| Cow::Borrowed("http")),
network_protocol_version: OtelInfoAttributes::version(
request.version(),
),
server_address: request.uri().host().map(|host| host.to_string()),
server_port: request.uri().port_u16().map(|port| port as i64),
error_type: Default::default(),
http_response_status_code: Default::default(),
},
))
} else {
None
};
// If the underlying TCP connection is closed, this future will be dropped
// and execution could stop at any await point.
// The HttpRecord must live until JavaScript is done processing so is wrapped
// in an Rc. The guard ensures unneeded resources are freed at cancellation.
let guarded_record = guard(
HttpRecord::new(request, request_info, server_state),
HttpRecord::new(request, request_info, server_state, otel_info),
HttpRecord::cancel,
);
@ -228,6 +258,7 @@ struct HttpRecordInner {
been_dropped: bool,
finished: bool,
needs_close_after_finish: bool,
otel_info: Option<OtelInfo>,
}
pub struct HttpRecord(RefCell<Option<HttpRecordInner>>);
@ -248,6 +279,7 @@ impl HttpRecord {
request: Request,
request_info: HttpConnectionProperties,
server_state: SignallingRc<HttpServerState>,
otel_info: Option<OtelInfo>,
) -> Rc<Self> {
let (request_parts, request_body) = request.into_parts();
let request_body = Some(request_body.into());
@ -284,6 +316,7 @@ impl HttpRecord {
been_dropped: false,
finished: false,
needs_close_after_finish: false,
otel_info,
});
record
}
@ -487,6 +520,7 @@ impl HttpRecord {
) -> Poll<Self::Output> {
let mut mut_self = self.0.self_mut();
if mut_self.response_ready {
mut_self.otel_info.take();
return Poll::Ready(());
}
mut_self.response_waker = Some(cx.waker().clone());
@ -523,6 +557,22 @@ impl HttpRecord {
HttpRecordFinished(self)
}
pub fn otel_info_set_status(&self, status: u16) {
let mut inner = self.self_mut();
if let Some(info) = inner.otel_info.as_mut() {
info.attributes.http_response_status_code = Some(status as _);
info.handle_duration_and_request_size();
}
}
pub fn otel_info_set_error(&self, error: &'static str) {
let mut inner = self.self_mut();
if let Some(info) = inner.otel_info.as_mut() {
info.attributes.error_type = Some(error);
info.handle_duration_and_request_size();
}
}
}
#[repr(transparent)]
@ -579,6 +629,16 @@ impl Body for HttpRecordResponse {
}
record.take_response_body();
}
if let ResponseStreamResult::NonEmptyBuf(buf) = &res {
let mut http = self.0 .0.borrow_mut();
if let Some(otel_info) = &mut http.as_mut().unwrap().otel_info {
if let Some(response_size) = &mut otel_info.response_size {
*response_size += buf.len() as u64;
}
}
}
Poll::Ready(res.into())
}

View file

@ -38,8 +38,10 @@ use opentelemetry::logs::AnyValue;
use opentelemetry::logs::LogRecord as LogRecordTrait;
use opentelemetry::logs::Severity;
use opentelemetry::metrics::AsyncInstrumentBuilder;
pub use opentelemetry::metrics::Histogram;
use opentelemetry::metrics::InstrumentBuilder;
use opentelemetry::metrics::MeterProvider as _;
pub use opentelemetry::metrics::MeterProvider;
pub use opentelemetry::metrics::UpDownCounter;
use opentelemetry::otel_debug;
use opentelemetry::otel_error;
use opentelemetry::trace::Link;
@ -51,10 +53,10 @@ use opentelemetry::trace::TraceFlags;
use opentelemetry::trace::TraceId;
use opentelemetry::trace::TraceState;
use opentelemetry::InstrumentationScope;
use opentelemetry::Key;
use opentelemetry::KeyValue;
use opentelemetry::StringValue;
use opentelemetry::Value;
pub use opentelemetry::Key;
pub use opentelemetry::KeyValue;
pub use opentelemetry::StringValue;
pub use opentelemetry::Value;
use opentelemetry_otlp::HttpExporterBuilder;
use opentelemetry_otlp::Protocol;
use opentelemetry_otlp::WithExportConfig;
@ -196,7 +198,7 @@ fn otel_create_shared_runtime() -> UnboundedSender<BoxFuture<'static, ()>> {
}
#[derive(Clone, Copy)]
struct OtelSharedRuntime;
pub struct OtelSharedRuntime;
impl hyper::rt::Executor<BoxFuture<'static, ()>> for OtelSharedRuntime {
fn execute(&self, fut: BoxFuture<'static, ()>) {
@ -586,15 +588,16 @@ mod hyper_client {
}
}
struct OtelGlobals {
span_processor: BatchSpanProcessor<OtelSharedRuntime>,
log_processor: BatchLogProcessor<OtelSharedRuntime>,
id_generator: DenoIdGenerator,
meter_provider: SdkMeterProvider,
builtin_instrumentation_scope: InstrumentationScope,
#[derive(Debug)]
pub struct OtelGlobals {
pub span_processor: BatchSpanProcessor<OtelSharedRuntime>,
pub log_processor: BatchLogProcessor<OtelSharedRuntime>,
pub id_generator: DenoIdGenerator,
pub meter_provider: SdkMeterProvider,
pub builtin_instrumentation_scope: InstrumentationScope,
}
static OTEL_GLOBALS: OnceCell<OtelGlobals> = OnceCell::new();
pub static OTEL_GLOBALS: OnceCell<OtelGlobals> = OnceCell::new();
pub fn init(
rt_config: OtelRuntimeConfig,
@ -808,7 +811,7 @@ pub fn handle_log(record: &log::Record) {
}
#[derive(Debug)]
enum DenoIdGenerator {
pub enum DenoIdGenerator {
Random(RandomIdGenerator),
Deterministic {
next_trace_id: AtomicU64,
@ -1708,9 +1711,9 @@ impl OtelMeter {
enum Instrument {
Counter(opentelemetry::metrics::Counter<f64>),
UpDownCounter(opentelemetry::metrics::UpDownCounter<f64>),
UpDownCounter(UpDownCounter<f64>),
Gauge(opentelemetry::metrics::Gauge<f64>),
Histogram(opentelemetry::metrics::Histogram<f64>),
Histogram(Histogram<f64>),
Observable(Arc<Mutex<HashMap<Vec<KeyValue>, f64>>>),
}

View file

@ -23,6 +23,13 @@
"args": "run -A main.ts metric.ts",
"output": "metric.out"
},
"http_metric": {
"envs": {
"OTEL_METRIC_EXPORT_INTERVAL": "1000"
},
"args": "run -A main.ts http_metric.ts",
"output": "http_metric.out"
},
"links": {
"args": "run -A main.ts links.ts",
"output": "links.out"

View file

@ -189,5 +189,259 @@
"spanId": "0000000000000004"
}
],
"metrics": []
"metrics": [
{
"name": "http.server.active_requests",
"description": "Number of active HTTP server requests.",
"unit": "{request}",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"exemplars": [],
"flags": 0,
"asInt": 0
}
],
"aggregationTemporality": 2,
"isMonotonic": false
}
},
{
"name": "http.server.request.body.size",
"description": "Size of HTTP server request bodies.",
"unit": "By",
"metadata": [],
"histogram": {
"dataPoints": [
{
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "http.response.status_code",
"value": {
"intValue": "200"
}
},
{
"key": "network.protocol.version",
"value": {
"stringValue": "1.1"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"count": 1,
"sum": 0,
"bucketCounts": [
1,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"explicitBounds": [
0,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000
],
"exemplars": [],
"flags": 0,
"min": 0,
"max": 0
}
],
"aggregationTemporality": 2
}
},
{
"name": "http.server.request.duration",
"description": "Duration of HTTP server requests.",
"unit": "s",
"metadata": [],
"histogram": {
"dataPoints": [
{
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "http.response.status_code",
"value": {
"intValue": "200"
}
},
{
"key": "network.protocol.version",
"value": {
"stringValue": "1.1"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"count": 1,
"sum": [WILDCARD],
"bucketCounts": [
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"explicitBounds": [
0.005,
0.01,
0.025,
0.05,
0.075,
0.1,
0.25,
0.5,
0.75,
1,
2.5,
5,
7.5,
10
],
"exemplars": [],
"flags": 0,
"min": [WILDCARD],
"max": [WILDCARD]
}
],
"aggregationTemporality": 2
}
},
{
"name": "http.server.response.body.size",
"description": "Size of HTTP server response bodies.",
"unit": "By",
"metadata": [],
"histogram": {
"dataPoints": [
{
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "http.response.status_code",
"value": {
"intValue": "200"
}
},
{
"key": "network.protocol.version",
"value": {
"stringValue": "1.1"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"count": 1,
"sum": 0,
"bucketCounts": [
1,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"explicitBounds": [
0,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000
],
"exemplars": [],
"flags": 0,
"min": 0,
"max": 0
}
],
"aggregationTemporality": 2
}
}
]
}

View file

@ -0,0 +1,608 @@
{
"spans": [
{
"traceId": "00000000000000000000000000000001",
"spanId": "0000000000000001",
"traceState": "",
"parentSpanId": "",
"flags": 1,
"name": "GET",
"kind": 3,
"startTimeUnixNano": "[WILDCARD]",
"endTimeUnixNano": "[WILDCARD]",
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "url.full",
"value": {
"stringValue": "http://localhost:8080/"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
},
{
"key": "url.path",
"value": {
"stringValue": "/"
}
},
{
"key": "url.query",
"value": {
"stringValue": ""
}
},
{
"key": "http.response.status_code",
"value": {
"stringValue": "200"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"links": [],
"droppedLinksCount": 0,
"status": {
"message": "",
"code": 0
}
},
{
"traceId": "00000000000000000000000000000002",
"spanId": "0000000000000002",
"traceState": "",
"parentSpanId": "",
"flags": 1,
"name": "GET",
"kind": 2,
"startTimeUnixNano": "[WILDCARD]",
"endTimeUnixNano": "[WILDCARD]",
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "url.full",
"value": {
"stringValue": "http://localhost:8080/"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
},
{
"key": "url.path",
"value": {
"stringValue": "/"
}
},
{
"key": "url.query",
"value": {
"stringValue": ""
}
},
{
"key": "http.response.status_code",
"value": {
"stringValue": "200"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"links": [],
"droppedLinksCount": 0,
"status": {
"message": "",
"code": 0
}
},
{
"traceId": "00000000000000000000000000000001",
"spanId": "0000000000000003",
"traceState": "",
"parentSpanId": "0000000000000001",
"flags": 1,
"name": "GET",
"kind": 3,
"startTimeUnixNano": "[WILDCARD]",
"endTimeUnixNano": "[WILDCARD]",
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "url.full",
"value": {
"stringValue": "http://localhost:8080/"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
},
{
"key": "url.path",
"value": {
"stringValue": "/"
}
},
{
"key": "url.query",
"value": {
"stringValue": ""
}
},
{
"key": "http.response.status_code",
"value": {
"stringValue": "200"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"links": [],
"droppedLinksCount": 0,
"status": {
"message": "",
"code": 0
}
},
{
"traceId": "00000000000000000000000000000003",
"spanId": "0000000000000004",
"traceState": "",
"parentSpanId": "",
"flags": 1,
"name": "GET",
"kind": 2,
"startTimeUnixNano": "[WILDCARD]",
"endTimeUnixNano": "[WILDCARD]",
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "url.full",
"value": {
"stringValue": "http://localhost:8080/"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
},
{
"key": "url.path",
"value": {
"stringValue": "/"
}
},
{
"key": "url.query",
"value": {
"stringValue": ""
}
},
{
"key": "http.response.status_code",
"value": {
"stringValue": "200"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"links": [],
"droppedLinksCount": 0,
"status": {
"message": "",
"code": 0
}
},
{
"traceId": "00000000000000000000000000000001",
"spanId": "0000000000000005",
"traceState": "",
"parentSpanId": "0000000000000001",
"flags": 1,
"name": "GET",
"kind": 3,
"startTimeUnixNano": "[WILDCARD]",
"endTimeUnixNano": "[WILDCARD]",
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "url.full",
"value": {
"stringValue": "http://localhost:8080/"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
},
{
"key": "url.path",
"value": {
"stringValue": "/"
}
},
{
"key": "url.query",
"value": {
"stringValue": ""
}
},
{
"key": "http.response.status_code",
"value": {
"stringValue": "200"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"links": [],
"droppedLinksCount": 0,
"status": {
"message": "",
"code": 0
}
},
{
"traceId": "00000000000000000000000000000004",
"spanId": "0000000000000006",
"traceState": "",
"parentSpanId": "",
"flags": 1,
"name": "GET",
"kind": 2,
"startTimeUnixNano": "[WILDCARD]",
"endTimeUnixNano": "[WILDCARD]",
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "url.full",
"value": {
"stringValue": "http://localhost:8080/"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
},
{
"key": "url.path",
"value": {
"stringValue": "/"
}
},
{
"key": "url.query",
"value": {
"stringValue": ""
}
},
{
"key": "http.response.status_code",
"value": {
"stringValue": "200"
}
}
],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"links": [],
"droppedLinksCount": 0,
"status": {
"message": "",
"code": 0
}
}
],
"logs": [],
"metrics": [
{
"name": "http.server.active_requests",
"description": "Number of active HTTP server requests.",
"unit": "{request}",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"exemplars": [],
"flags": 0,
"asInt": 0
}
],
"aggregationTemporality": 2,
"isMonotonic": false
}
},
{
"name": "http.server.request.body.size",
"description": "Size of HTTP server request bodies.",
"unit": "By",
"metadata": [],
"histogram": {
"dataPoints": [
{
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "http.response.status_code",
"value": {
"intValue": "200"
}
},
{
"key": "network.protocol.version",
"value": {
"stringValue": "1.1"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"count": 3,
"sum": 0,
"bucketCounts": [
3,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"explicitBounds": [
0,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000
],
"exemplars": [],
"flags": 0,
"min": 0,
"max": 0
}
],
"aggregationTemporality": 2
}
},
{
"name": "http.server.request.duration",
"description": "Duration of HTTP server requests.",
"unit": "s",
"metadata": [],
"histogram": {
"dataPoints": [
{
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "http.response.status_code",
"value": {
"intValue": "200"
}
},
{
"key": "network.protocol.version",
"value": {
"stringValue": "1.1"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"count": 3,
"sum": [WILDCARD],
"bucketCounts": [
3,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"explicitBounds": [
0.005,
0.01,
0.025,
0.05,
0.075,
0.1,
0.25,
0.5,
0.75,
1,
2.5,
5,
7.5,
10
],
"exemplars": [],
"flags": 0,
"min": [WILDCARD],
"max": [WILDCARD]
}
],
"aggregationTemporality": 2
}
},
{
"name": "http.server.response.body.size",
"description": "Size of HTTP server response bodies.",
"unit": "By",
"metadata": [],
"histogram": {
"dataPoints": [
{
"attributes": [
{
"key": "http.request.method",
"value": {
"stringValue": "GET"
}
},
{
"key": "http.response.status_code",
"value": {
"intValue": "200"
}
},
{
"key": "network.protocol.version",
"value": {
"stringValue": "1.1"
}
},
{
"key": "url.scheme",
"value": {
"stringValue": "http"
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"count": 3,
"sum": 0,
"bucketCounts": [
3,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"explicitBounds": [
0,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000
],
"exemplars": [],
"flags": 0,
"min": 0,
"max": 0
}
],
"aggregationTemporality": 2
}
}
]
}

View file

@ -0,0 +1,10 @@
const server = Deno.serve(
{ port: 8080, onListen: () => {} },
() => new Response("foo"),
);
for (let i = 0; i < 3; i++) {
await fetch(`http://localhost:8080`);
}
await server.shutdown();

View file

@ -40,6 +40,17 @@ const server = Deno.serve(
data.spans.sort((a, b) =>
Number(BigInt(`0x${a.spanId}`) - BigInt(`0x${b.spanId}`))
);
data.metrics.sort((a, b) => a.name.localeCompare(b.name));
for (const metric of data.metrics) {
if ("histogram" in metric) {
for (const dataPoint of metric.histogram.dataPoints) {
dataPoint.attributes.sort((a, b) => {
return a.key.localeCompare(b.key);
});
}
}
}
console.log(JSON.stringify(data, null, 2));
});
},

View file

@ -18,8 +18,8 @@
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": 1
@ -29,180 +29,6 @@
"isMonotonic": true
}
},
{
"name": "up_down_counter",
"description": "Example of a UpDownCounter",
"unit": "",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [
{
"key": "attribute",
"value": {
"doubleValue": 1
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"exemplars": [],
"flags": 0,
"asDouble": -1
}
],
"aggregationTemporality": 2,
"isMonotonic": false
}
},
{
"name": "gauge",
"description": "Example of a Gauge",
"unit": "",
"metadata": [],
"gauge": {
"dataPoints": [
{
"attributes": [
{
"key": "attribute",
"value": {
"doubleValue": 1
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"exemplars": [],
"flags": 0,
"asDouble": 1
}
]
}
},
{
"name": "histogram",
"description": "Example of a Histogram",
"unit": "",
"metadata": [],
"histogram": {
"dataPoints": [
{
"attributes": [
{
"key": "attribute",
"value": {
"doubleValue": 1
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"count": 1,
"sum": 1,
"bucketCounts": [
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"explicitBounds": [
0,
5,
10,
25,
50,
75,
100,
250,
500,
750,
1000,
2500,
5000,
7500,
10000
],
"exemplars": [],
"flags": 0,
"min": 1,
"max": 1
}
],
"aggregationTemporality": 2
}
},
{
"name": "observable_counter",
"description": "Example of a ObservableCounter",
"unit": "",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"exemplars": [],
"flags": 0,
"asDouble": 1
}
],
"aggregationTemporality": 2,
"isMonotonic": true
}
},
{
"name": "observable_up_down_counter",
"description": "Example of a ObservableUpDownCounter",
"unit": "",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"exemplars": [],
"flags": 0,
"asDouble": 1
}
],
"aggregationTemporality": 2,
"isMonotonic": false
}
},
{
"name": "observable_gauge",
"description": "Example of a ObservableGauge",
"unit": "",
"metadata": [],
"gauge": {
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"exemplars": [],
"flags": 0,
"asDouble": 1
}
]
}
},
{
"name": "counter",
"description": "Example of a Counter",
@ -219,8 +45,8 @@
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": 1
@ -230,33 +56,6 @@
"isMonotonic": true
}
},
{
"name": "up_down_counter",
"description": "Example of a UpDownCounter",
"unit": "",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [
{
"key": "attribute",
"value": {
"doubleValue": 1
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"exemplars": [],
"flags": 0,
"asDouble": -1
}
],
"aggregationTemporality": 2,
"isMonotonic": false
}
},
{
"name": "gauge",
"description": "Example of a Gauge",
@ -273,8 +72,33 @@
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": 1
}
]
}
},
{
"name": "gauge",
"description": "Example of a Gauge",
"unit": "",
"metadata": [],
"gauge": {
"dataPoints": [
{
"attributes": [
{
"key": "attribute",
"value": {
"doubleValue": 1
}
}
],
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": 1
@ -298,8 +122,72 @@
}
}
],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"count": 1,
"sum": 1,
"bucketCounts": [
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"explicitBounds": [
0,
5,
10,
25,
50,
75,
100,
250,
500,
750,
1000,
2500,
5000,
7500,
10000
],
"exemplars": [],
"flags": 0,
"min": 1,
"max": 1
}
],
"aggregationTemporality": 2
}
},
{
"name": "histogram",
"description": "Example of a Histogram",
"unit": "",
"metadata": [],
"histogram": {
"dataPoints": [
{
"attributes": [
{
"key": "attribute",
"value": {
"doubleValue": 1
}
}
],
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"count": 1,
"sum": 1,
"bucketCounts": [
@ -355,8 +243,8 @@
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": 1
@ -367,23 +255,23 @@
}
},
{
"name": "observable_up_down_counter",
"description": "Example of a ObservableUpDownCounter",
"name": "observable_counter",
"description": "Example of a ObservableCounter",
"unit": "",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": 1
}
],
"aggregationTemporality": 2,
"isMonotonic": false
"isMonotonic": true
}
},
{
@ -395,14 +283,126 @@
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": "[WILDCARD]",
"timeUnixNano": "[WILDCARD]",
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": 1
}
]
}
},
{
"name": "observable_gauge",
"description": "Example of a ObservableGauge",
"unit": "",
"metadata": [],
"gauge": {
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": 1
}
]
}
},
{
"name": "observable_up_down_counter",
"description": "Example of a ObservableUpDownCounter",
"unit": "",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": 1
}
],
"aggregationTemporality": 2,
"isMonotonic": false
}
},
{
"name": "observable_up_down_counter",
"description": "Example of a ObservableUpDownCounter",
"unit": "",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [],
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": 1
}
],
"aggregationTemporality": 2,
"isMonotonic": false
}
},
{
"name": "up_down_counter",
"description": "Example of a UpDownCounter",
"unit": "",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [
{
"key": "attribute",
"value": {
"doubleValue": 1
}
}
],
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": -1
}
],
"aggregationTemporality": 2,
"isMonotonic": false
}
},
{
"name": "up_down_counter",
"description": "Example of a UpDownCounter",
"unit": "",
"metadata": [],
"sum": {
"dataPoints": [
{
"attributes": [
{
"key": "attribute",
"value": {
"doubleValue": 1
}
}
],
"startTimeUnixNano": [WILDCARD],
"timeUnixNano": [WILDCARD],
"exemplars": [],
"flags": 0,
"asDouble": -1
}
],
"aggregationTemporality": 2,
"isMonotonic": false
}
}
]
}