Repository: cloudflare/foundations (language: rust)

Issue:
Rust 1.79 clippy failing due to possible code simplification in darling's proc macro usage
foundations' CI is failing the clippy check with:
```
error: if let can be simplified with `.unwrap_or_default()`
  --> foundations-macros/src/info_metric/mod.rs:18:15
   |
18 |     #[darling(default)]
   |               ^^^^^^^
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#manual_unwrap_or_default
   = note: `-D clippy::manual-unwrap-or-default` implied by `-D warnings`
   = help: to override `-D warnings` add `#[allow(clippy::manual_unwrap_or_default)]`
```

This has been reported in darling's repo https://github.com/TedDriggs/darling/issues/293 but is still not fixed there. So right now foundations development is stuck.



Discussion / hints:
Temporary mitigation until this is fixed properly: https://github.com/cloudflare/foundations/pull/51
This has been fixed in darling 0.20.10
@TedDriggs thank you for the heads up
Ah, we can't easily update - we're still on `syn` 1.x
Ok, wasn't that hard after all: https://github.com/cloudflare/foundations/pull/56

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fb48438..8e46d5f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,9 +4,11 @@ on:
   pull_request:
     branches:
       - main
+      - "v[0-9]+.*"
   push:
     branches:
       - main
+      - "v[0-9]+.*"
 
 env:
   RUSTFLAGS: -Dwarnings
@@ -23,7 +25,6 @@ jobs:
       - fmt
       - check-features
       - test
-      - minimal-versions
     steps:
       - run: exit 0
 
@@ -46,43 +47,6 @@ jobs:
           RUSTDOCFLAGS: --cfg docsrs -Dwarnings --cfg tokio_unstable --cfg foundations_unstable
           RUSTFLAGS: --cfg docsrs -Dwarnings --cfg tokio_unstable --cfg foundations_unstable
 
-  # based on tokio minver cbecm
-  minimal-versions:
-    name: minimal-versions
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v4
-        with:
-          submodules: "recursive"
-      - name: Install Nightly Rust
-        uses: dtolnay/rust-toolchain@stable
-        with:
-          toolchain: nightly
-      - name: Install cargo-hack
-        uses: taiki-e/install-action@cargo-hack
-      - uses: Swatinem/rust-cache@v2
-      - name: Check minimal versions
-        env:
-          # empty those flags!
-          RUSTFLAGS:
-        run: |
-          # Remove dev-dependencies from Cargo.toml to prevent the next `cargo update`
-          # from determining minimal versions based on dev-dependencies.
-          cargo hack --remove-dev-deps --workspace
-          # Update Cargo.lock to minimal version dependencies.
-          cargo update -Z minimal-versions
-          cargo hack check --all-features --ignore-private
-      - name: Check minimal versions with unstable features
-        env:
-          RUSTFLAGS: --cfg tokio_unstable --cfg foundations_unstable
-        run: |
-          # Remove dev-dependencies from Cargo.toml to prevent the next `cargo update`
-          # from determining minimal versions based on dev-dependencies.
-          cargo hack --remove-dev-deps --workspace
-          # Update Cargo.lock to minimal version dependencies.
-          cargo update -Z minimal-versions
-          cargo hack check --all-features --ignore-private
-
   deny:
     name: Cargo deny checks
     runs-on: ubuntu-latest
diff --git a/Cargo.toml b/Cargo.toml
index c5c190b..00b277c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -3,12 +3,12 @@ members = [
     "foundations",
     "foundations-macros",
     "examples",
-    "tools/gen-syscall-enum"
+    "tools/gen-syscall-enum",
 ]
 resolver = "2"
 
 [workspace.package]
-version = "3.3.0"
+version = "3.4.0"
 repository = "https://github.com/cloudflare/foundations"
 edition = "2021"
 authors = ["Cloudflare"]
@@ -31,7 +31,7 @@ bindgen = { version = "0.68.1", default-features = false }
 cc = "1.0"
 clap = "4.4"
 crossbeam-channel = "0.5"
-darling = "0.14.4"
+darling = "0.20.10"
 erased-serde = "0.3.28"
 futures-util = "0.3.28"
 governor = "0.6"
@@ -51,8 +51,8 @@ quote = "1"
 regex = "1.8"
 reqwest = { version = "0.11", default-features = false }
 routerify = "3"
-socket2 = { version = "0.5.3", features = [ "all" ] }
-syn = "1"
+socket2 = { version = "0.5.3", features = ["all"] }
+syn = "2"
 serde = "1"
 serde_path_to_error = "0.1.15"
 serde_yaml = "0.8.26"
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 731df51..81304c9 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,4 +1,17 @@
+
+3.4.0
+- 2024-10-07 Disable minimal-versions check until it can be fixed
+- 2024-08-12 Fix seccomp violation in MemoryProfiler introduce by Rust 1.80.0 std
+- 2024-07-24 Rename depricated tokio metric
+- 2024-08-06 Make clippy happy
+- 2024-10-07 Backport allow of unexpected cfgs
+- 2024-07-22 Bump syn and darling (closes #50)
+- 2024-10-03 Let GitHub run CI actions for version branches (#73)
+- 2024-09-21 Provide metrics::add_extra_producer() to enable external metrics
+- 2024-03-26 Improve metrics bind error message
+
 3.3.0
+- 2024-03-21 Release 3.3.0
 - 2024-03-21 Fix new lints
 - 2024-03-19 Construct metrics registry with default() when name_in_metrics is empty
 - 2024-03-10 Fix paths in gen-syscall-enum tool
diff --git a/foundations-macros/src/common.rs b/foundations-macros/src/common.rs
index 2fb16a6..67ea47a 100644
--- a/foundations-macros/src/common.rs
+++ b/foundations-macros/src/common.rs
@@ -1,7 +1,9 @@
+use darling::ast::NestedMeta;
+use quote::ToTokens as _;
 use syn::parse::{Parse, ParseStream, Parser};
 use syn::punctuated::Punctuated;
 use syn::spanned::Spanned;
-use syn::{Attribute, NestedMeta, Token};
+use syn::{Attribute, Ident, Token};
 
 pub(crate) type Result<T> = std::result::Result<T, syn::Error>;
 
@@ -34,11 +36,12 @@ where
     T: Parse,
 {
     let parser = |input: ParseStream| {
+        let _name = input.parse::<Ident>()?;
         let _equal_token = input.parse::<Token![=]>()?;
         input.parse::<T>()
     };
 
-    parser.parse2(attr.tokens)
+    parser.parse2(attr.meta.to_token_stream())
 }
 
 #[cfg(test)]
diff --git a/foundations-macros/src/info_metric/mod.rs b/foundations-macros/src/info_metric/mod.rs
index 8666392..4eaee47 100644
--- a/foundations-macros/src/info_metric/mod.rs
+++ b/foundations-macros/src/info_metric/mod.rs
@@ -5,8 +5,7 @@ use proc_macro2::Span;
 use quote::{quote, ToTokens};
 use syn::punctuated::Punctuated;
 use syn::{
-    parse_macro_input, parse_quote, Attribute, AttributeArgs, Ident, LitStr, Path, Token, Type,
-    Visibility,
+    parse_macro_input, parse_quote, Attribute, Ident, LitStr, Path, Token, Type, Visibility,
 };
 
 mod parsing;
@@ -63,11 +62,8 @@ struct FieldAttrs {
 }
 
 pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream {
+    let args = parse_macro_input!(args as MacroArgs);
     let mod_ = parse_macro_input!(item as Struct);
-    let args = match MacroArgs::from_list(&parse_macro_input!(args as AttributeArgs)) {
-        Ok(args) => args,
-        Err(e) => return e.write_errors().into(),
-    };
 
     expand_from_parsed(args, mod_)
         .unwrap_or_else(|e| e.to_compile_error())
diff --git a/foundations-macros/src/info_metric/parsing.rs b/foundations-macros/src/info_metric/parsing.rs
index bf26c5f..3d62e6d 100644
--- a/foundations-macros/src/info_metric/parsing.rs
+++ b/foundations-macros/src/info_metric/parsing.rs
@@ -19,6 +19,7 @@ impl Parse for MacroArgs {
         }
 
         let meta_list = parse_meta_list(&input)?;
+
         Ok(Self::from_list(&meta_list)?)
     }
 }
@@ -30,9 +31,11 @@ impl Parse for Struct {
             let mut doc = "".to_owned();
 
             for attr in attrs {
-                if attr.path.is_ident("cfg") {
+                let path = attr.path();
+
+                if path.is_ident("cfg") {
                     cfg.push(attr);
-                } else if attr.path.is_ident("doc") {
+                } else if path.is_ident("doc") {
                     doc.push_str(&parse_attr_value::<LitStr>(attr)?.value());
                 } else {
                     return error(&attr, STRUCT_ATTR_ERROR);
@@ -76,13 +79,15 @@ impl Parse for Field {
             let mut attrs = FieldAttrs::default();
 
             for attr in raw_attrs {
-                if attr.path.is_ident("serde") {
+                let path = attr.path();
+
+                if path.is_ident("serde") {
                     if attrs.serde.is_some() {
                         return error(&attr, DUPLICATE_SERDE_ATTR_ERROR);
                     }
 
                     attrs.serde = Some(attr);
-                } else if attr.path.is_ident("serde_as") {
+                } else if path.is_ident("serde_as") {
                     if attrs.serde_as.is_some() {
                         return error(&attr, DUPLICATE_SERDE_AS_ATTR_ERROR);
                     }
diff --git a/foundations-macros/src/metrics/mod.rs b/foundations-macros/src/metrics/mod.rs
index eb404af..92c7645 100644
--- a/foundations-macros/src/metrics/mod.rs
+++ b/foundations-macros/src/metrics/mod.rs
@@ -4,8 +4,8 @@ use proc_macro2::Span;
 use quote::{quote, ToTokens};
 use syn::punctuated::Punctuated;
 use syn::{
-    parse_macro_input, parse_quote, Attribute, AttributeArgs, ExprStruct, Ident, LitStr, Path,
-    Token, Type, Visibility,
+    parse_macro_input, parse_quote, Attribute, ExprStruct, Ident, LitStr, Path, Token, Type,
+    Visibility,
 };
 
 mod parsing;
@@ -77,11 +77,8 @@ enum ArgMode {
 }
 
 pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream {
+    let args = parse_macro_input!(args as MacroArgs);
     let mod_ = parse_macro_input!(item as Mod);
-    let args = match MacroArgs::from_list(&parse_macro_input!(args as AttributeArgs)) {
-        Ok(args) => args,
-        Err(e) => return e.write_errors().into(),
-    };
 
     expand_from_parsed(args, mod_).into()
 }
diff --git a/foundations-macros/src/metrics/parsing.rs b/foundations-macros/src/metrics/parsing.rs
index b01928f..60b8961 100644
--- a/foundations-macros/src/metrics/parsing.rs
+++ b/foundations-macros/src/metrics/parsing.rs
@@ -1,6 +1,7 @@
 use super::{ArgAttrs, ArgMode, FnArg, FnAttrs, ItemFn, MacroArgs, Mod};
 use crate::common::{error, parse_attr_value, parse_meta_list, Result};
 use darling::FromMeta;
+use quote::ToTokens as _;
 use syn::parse::{Parse, ParseStream};
 use syn::punctuated::Punctuated;
 use syn::{
@@ -28,6 +29,7 @@ impl Parse for MacroArgs {
         }
 
         let meta_list = parse_meta_list(&input)?;
+
         Ok(Self::from_list(&meta_list)?)
     }
 }
@@ -65,25 +67,31 @@ impl Parse for ItemFn {
             let mut optional = None;
 
             for attr in attrs {
-                if attr.path.is_ident("cfg") {
+                let path = attr.path();
+
+                if path.is_ident("cfg") {
                     cfg.push(attr);
-                } else if attr.path.is_ident("doc") {
+                } else if path.is_ident("doc") {
                     doc.push_str(&parse_attr_value::<LitStr>(attr)?.value());
-                } else if attr.path.is_ident("ctor") {
+                } else if path.is_ident("ctor") {
                     if ctor.is_some() {
                         return error(&attr, DUPLICATE_CTOR_ATTR_ERROR);
                     }
 
                     ctor = Some(parse_attr_value(attr)?);
-                } else if attr.path.is_ident("optional") {
+                } else if path.is_ident("optional") {
                     if optional.is_some() {
                         return error(&attr, DUPLICATE_OPTIONAL_ATTR_ERROR);
                     }
 
-                    if attr.tokens.is_empty() {
+                    if attr.to_token_stream().is_empty() {
                         optional = Some(true);
                     } else {
-                        optional = Some(parse_attr_value::<LitBool>(attr)?.value);
+                        optional = Some(
+                            parse_attr_value::<LitBool>(attr)
+                                .map(|l| l.value)
+                                .unwrap_or(true),
+                        );
                     }
                 } else {
                     return error(&attr, FN_ATTR_ERROR);
@@ -138,13 +146,15 @@ impl Parse for FnArg {
             let mut attrs = ArgAttrs::default();
 
             for attr in raw_attrs {
-                if attr.path.is_ident("serde") {
+                let path = attr.path();
+
+                if path.is_ident("serde") {
                     if attrs.serde.is_some() {
                         return error(&attr, DUPLICATE_SERDE_ATTR_ERROR);
                     }
 
                     attrs.serde = Some(attr);
-                } else if attr.path.is_ident("serde_as") {
+                } else if path.is_ident("serde_as") {
                     if attrs.serde_as.is_some() {
                         return error(&attr, DUPLICATE_SERDE_AS_ATTR_ERROR);
                     }
diff --git a/foundations-macros/src/settings.rs b/foundations-macros/src/settings.rs
index 2aef1e3..f312f16 100644
--- a/foundations-macros/src/settings.rs
+++ b/foundations-macros/src/settings.rs
@@ -1,12 +1,13 @@
 use crate::common::{error, parse_meta_list, Result};
+use darling::ast::NestedMeta;
 use darling::FromMeta;
 use proc_macro::TokenStream;
 use quote::{quote, quote_spanned, TokenStreamExt};
 use syn::parse::{Parse, ParseStream};
 use syn::spanned::Spanned;
 use syn::{
-    parse_macro_input, parse_quote, Attribute, Field, Fields, Ident, Item, ItemEnum, ItemStruct,
-    Lit, LitStr, Meta, MetaNameValue, NestedMeta, Path,
+    parse_macro_input, parse_quote, Attribute, Expr, ExprLit, Field, Fields, Ident, Item, ItemEnum,
+    ItemStruct, Lit, LitStr, Meta, MetaNameValue, Path,
 };
 
 const ERR_NOT_STRUCT_OR_ENUM: &str = "Settings should be either structure or enum.";
@@ -221,7 +222,7 @@ fn impl_settings_trait_for_field(
     let cfg_attrs = field
         .attrs
         .iter()
-        .filter(|a| a.path.is_ident("cfg"))
+        .filter(|a| a.path().is_ident("cfg"))
         .collect::<Vec<_>>();
 
     impl_for_field.append_all(quote_spanned! { span=>
@@ -254,16 +255,20 @@ fn extract_doc_comments(attrs: &[Attribute]) -> Vec<LitStr> {
     let mut comments = vec![];
 
     for attr in attrs {
-        if !attr.path.is_ident("doc") {
+        if !attr.path().is_ident("doc") {
             continue;
         }
 
-        if let Ok(Meta::NameValue(MetaNameValue {
-            lit: Lit::Str(lit_str),
+        if let Meta::NameValue(MetaNameValue {
+            value:
+                Expr::Lit(ExprLit {
+                    lit: Lit::Str(lit_str),
+                    ..
+                }),
             ..
-        })) = attr.parse_meta()
+        }) = &attr.meta
         {
-            comments.push(lit_str);
+            comments.push(lit_str.clone());
         }
     }
 
@@ -281,7 +286,11 @@ fn impl_serde_aware_default(item: &ItemStruct) -> proc_macro2::TokenStream {
             .expect("should not generate field docs for tuple struct");
 
         let span = field.ty.span();
-        let cfg_attrs = field.attrs.iter().filter(|attr| attr.path.is_ident("cfg"));
+
+        let cfg_attrs = field
+            .attrs
+            .iter()
+            .filter(|attr| attr.path().is_ident("cfg"));
 
         let function_path = get_field_default_fn(field)
             .unwrap_or_else(|| quote_spanned! { span=> Default::default });
@@ -300,15 +309,19 @@ fn impl_serde_aware_default(item: &ItemStruct) -> proc_macro2::TokenStream {
 
 fn get_field_default_fn(field: &Field) -> Option<proc_macro2::TokenStream> {
     for attr in &field.attrs {
-        if !attr.path.is_ident("serde") {
+        if !attr.path().is_ident("serde") {
             continue;
         }
 
-        let Ok(Meta::List(list)) = attr.parse_meta() else {
+        let Meta::List(list) = &attr.meta else {
+            continue;
+        };
+
+        let Ok(nested_meta_list) = NestedMeta::parse_meta_list(list.tokens.clone()) else {
             continue;
         };
 
-        for meta in list.nested {
+        for meta in nested_meta_list {
             let NestedMeta::Meta(Meta::NameValue(mnv)) = meta else {
                 continue;
             };
@@ -317,7 +330,12 @@ fn get_field_default_fn(field: &Field) -> Option<proc_macro2::TokenStream> {
                 continue;
             }
 
-            let Lit::Str(val) = mnv.lit else { continue };
+            let Expr::Lit(ExprLit {
+                lit: Lit::Str(val), ..
+            }) = mnv.value
+            else {
+                continue;
+            };
 
             match val.parse() {
                 Ok(tokens) => return Some(tokens),
diff --git a/foundations-macros/src/span_fn.rs b/foundations-macros/src/span_fn.rs
index 38068e3..4dcbc08 100644
--- a/foundations-macros/src/span_fn.rs
+++ b/foundations-macros/src/span_fn.rs
@@ -130,7 +130,7 @@ fn expand_from_parsed(args: Args, item_fn: ItemFn) -> TokenStream2 {
 
 fn try_async_trait_fn_rewrite(args: &Args, body: &Block) -> Option<TokenStream2> {
     let (last_expr_fn_call, last_expr_fn_call_args) = match body.stmts.last()? {
-        Stmt::Expr(Expr::Call(ExprCall { func, args, .. })) => (func, args),
+        Stmt::Expr(Expr::Call(ExprCall { func, args, .. }), ..) => (func, args),
         _ => return None,
     };
 
diff --git a/foundations/src/lib.rs b/foundations/src/lib.rs
index 283b17d..d0cb6c6 100644
--- a/foundations/src/lib.rs
+++ b/foundations/src/lib.rs
@@ -22,7 +22,7 @@
 //!
 //! - **default**: All features are enabled by default.
 //! - **platform-common-default**: The same as **default**, but excludes platform-specific features,
-//! such as **security**.
+//!   such as **security**.
 //! - **server-client-common-default**: A subset of features that can be used both on server and client sides.
 //!   Useful for libraries that can be used either way.
 //! - **settings**: Enables serializable documented settings functionality.
@@ -35,11 +35,11 @@
 //! - **testing**: Enables testing-related functionality.
 //! - **security**: Enables security features. Available only on Linux (x86_64, aarch64).
 //! - **jemalloc**: Enables [jemalloc] memory allocator which is known to perform much better than
-//! system allocators for long living service.
+//!   system allocators for long living service.
 //! - **memory-profiling**: Enables memory profiling functionality and telemetry. Implicity enables
-//!  **jemalloc** feature.
+//!   **jemalloc** feature.
 //! - **cli**: Enables command line interface (CLI) functionality. Implicitly enabled **settings**
-//! feature.
+//!   feature.
 //!
 //! # Unstable Features
 //! Foundations has unstable features which are gated behind `--cfg foundations_unstable`:
@@ -53,6 +53,8 @@
 //! [jemalloc]: https://github.com/jemalloc/jemalloc
 //! [examples]: https://github.com/cloudflare/foundations/tree/main/examples
 
+// NOTE: required to allow cfgs like `tokio_unstable` on nightly which is used in tests.
+#![allow(unexpected_cfgs)]
 #![warn(missing_docs)]
 #![cfg_attr(docsrs, feature(doc_auto_cfg))]
 #![cfg_attr(docsrs, feature(doc_cfg))]
diff --git a/foundations/src/security/common_syscall_allow_lists.rs b/foundations/src/security/common_syscall_allow_lists.rs
index 3ff1579..c4d3cf5 100644
--- a/foundations/src/security/common_syscall_allow_lists.rs
+++ b/foundations/src/security/common_syscall_allow_lists.rs
@@ -17,6 +17,13 @@ allow_list! {
         sched_getaffinity,
         madvise, // memory allocation
         mprotect,
+        // NOTE: rust 1.80.0 std library introduced an assertion that uses `fcntl`:
+        // https://github.com/rust-lang/rust/commit/38ded129236f112a7421f311aeb8ca750b661443
+        // it's later has been changed to debug-only assertion:
+        // https://github.com/rust-lang/rust/commit/1ba00d9cb2fcfef464b6a188fa3a7543c66eecaa,
+        // so we allow this syscall only in debug mode.
+        #[cfg(debug_assertions)]
+        fcntl,
         prctl if [ ArgCmp::Equal { arg_idx: 0, value: PR_SET_NAME.into() } ] // tokio-runtime thread name
     ]
 }
diff --git a/foundations/src/security/mod.rs b/foundations/src/security/mod.rs
index fb4f036..2a05f60 100644
--- a/foundations/src/security/mod.rs
+++ b/foundations/src/security/mod.rs
@@ -328,8 +328,6 @@ pub enum Rule {
 /// use foundations::security::{enable_syscall_sandboxing, ViolationAction, allow_list};
 /// use foundations::security::common_syscall_allow_lists::SERVICE_BASICS;
 /// use std::net::TcpListener;
-/// use std::panic;
-/// use std::thread;
 ///
 /// allow_list! {
 ///    static ALLOW_BIND = [
diff --git a/foundations/src/telemetry/metrics/internal.rs b/foundations/src/telemetry/metrics/internal.rs
index 58e124d..78a133e 100644
--- a/foundations/src/telemetry/metrics/internal.rs
+++ b/foundations/src/telemetry/metrics/internal.rs
@@ -1,4 +1,4 @@
-use super::{info_metric, InfoMetric};
+use super::{info_metric, ExtraProducer, InfoMetric};
 use crate::telemetry::settings::{MetricsSettings, ServiceNameFormat};
 use crate::{Result, ServiceInfo};
 use once_cell::sync::OnceCell;
@@ -18,6 +18,7 @@ pub struct Registries {
     opt: RwLock<Registry>,
     pub(super) info: RwLock<HashMap<TypeId, Box<dyn ErasedInfoMetric>>>,
     extra_label: Option<(String, String)>,
+    extra_producers: parking_lot::RwLock<Vec<Box<dyn ExtraProducer>>>,
 }
 
 impl Registries {
@@ -34,6 +35,7 @@ impl Registries {
             opt: new_registry(&service_info.name_in_metrics, &settings.service_name_format),
             info: Default::default(),
             extra_label,
+            extra_producers: Default::default(),
         });
     }
 
@@ -48,6 +50,11 @@ impl Registries {
             encode_registry(buffer, &registries.opt.read())?;
         }
 
+        for producer in registries.extra_producers.read().iter() {
+            producer.produce(buffer);
+            truncate_eof(buffer);
+        }
+
         Ok(())
     }
 
@@ -84,12 +91,17 @@ impl Registries {
         )
     }
 
+    pub fn add_extra_producer(&self, producer: Box<dyn ExtraProducer>) {
+        self.extra_producers.write().push(producer);
+    }
+
     pub(super) fn get() -> &'static Registries {
         REGISTRIES.get_or_init(|| Registries {
             main: new_registry("undefined", &ServiceNameFormat::MetricPrefix),
             opt: new_registry("undefined", &ServiceNameFormat::MetricPrefix),
             info: Default::default(),
             extra_label: None,
+            extra_producers: Default::default(),
         })
     }
 }
@@ -163,9 +175,13 @@ pub(super) fn encode_registry(
 ) -> Result<()> {
     encode(buffer, registry)?;
 
+    truncate_eof(buffer);
+
+    Ok(())
+}
+
+fn truncate_eof(buffer: &mut Vec<u8>) {
     if buffer.ends_with(b"# EOF\n") {
         buffer.truncate(buffer.len() - b"# EOF\n".len());
     }
-
-    Ok(())
 }
diff --git a/foundations/src/telemetry/metrics/mod.rs b/foundations/src/telemetry/metrics/mod.rs
index 127ad78..43a7faf 100644
--- a/foundations/src/telemetry/metrics/mod.rs
+++ b/foundations/src/telemetry/metrics/mod.rs
@@ -3,7 +3,7 @@
 //! Foundations provides simple and ergonomic interface to [Prometheus] metrics:
 //! - Use [`metrics`] macro to define regular metrics.
 //! - Use [`report_info`] function to register service information metrics (metrics, whose value is
-//! persistent during the service lifetime, e.g. software version).
+//!   persistent during the service lifetime, e.g. software version).
 //! - Use [`collect`] method to obtain metrics report programmatically.
 //! - Use [telemetry server] to expose a metrics endpoint.
 //!
@@ -196,22 +196,22 @@ pub fn collect(settings: &MetricsSettings) -> Result<String> {
 ///     let endpoint = Arc::new("http-over-tcp".to_owned());
 ///     let l4_protocol = labels::L4Protocol::Tcp;
 ///     let ingress_ip = "127.0.0.1".parse::<IpAddr>().unwrap();
-///     
+///
 ///     my_app_metrics::client_connections_total(
 ///         &endpoint,
 ///         l4_protocol,
 ///         ingress_ip,
 ///     ).inc();
-///     
+///
 ///     let client_connections_active = my_app_metrics::client_connections_active(
 ///         &endpoint,
 ///         l4_protocol,
 ///         labels::IpVersion::V4,
 ///         ingress_ip,
 ///     );
-///     
+///
 ///     client_connections_active.inc();
-///     
+///
 ///     my_app_metrics::proxy_status_serialization_error_count().inc();
 ///
 ///     client_connections_active.dec();
@@ -376,3 +376,76 @@ impl MetricConstructor<TimeHistogram> for HistogramBuilder {
         TimeHistogram::new(self.buckets.iter().cloned())
     }
 }
+
+/// Adds an [ExtraProducer] that runs whenever Prometheus metrics are scraped.
+/// The producer appends metrics into a provided buffer to make them available.
+///
+/// The motivation for this is enabling metrics export from third party libraries that
+/// do not integrate with `foundations`` directly in a forward and backward compatible way.
+///
+/// One can ask "why not expose a `Registry` from `prometheus_client`?" The reason is that
+/// it would require compatibility between `prometheus_client` version that `foundations`
+/// depend on and the version that the third party crates depend on. With a producer
+/// that simply appends bytes into a buffer we avoid the need to have this match,
+/// at the cost of requiring the consumers to do the encoding themselves.
+///
+/// # Example
+///
+/// In this example we have a `Cache` that would be provided from an external crate, which
+/// does not expose metrics directly, but allows registering them in a provided `Registry`.
+///
+/// The consumer code would make a `Registry` with whatever version they want and do
+/// the encoding in a text format to make a suitable [ExtraProducer].
+///
+/// ```
+/// #[derive(Default)]
+/// struct Cache {
+///   calls: prometheus_client::metrics::counter::Counter,
+/// }
+///
+/// impl Cache {
+///   fn register_metrics(&self, registry: &mut prometheus_client::registry::Registry) {
+///     registry.register(
+///       "calls",
+///       "The number of calls into cache",
+///       Box::new(self.calls.clone()),
+///     )
+///   }
+/// }
+///
+/// let cache = Cache::default();
+///
+/// let mut registry = prometheus_client::registry::Registry::default();
+/// let mut sub_registry = registry.sub_registry_with_prefix("cache").sub_registry_with_label((
+///     std::borrow::Cow::Borrowed("cache"),
+///     std::borrow::Cow::Borrowed("things"),
+/// ));
+///
+/// cache.register_metrics(&mut sub_registry);
+///
+/// foundations::telemetry::metrics::add_extra_producer(move |buffer: &mut Vec<u8>| {
+///     prometheus_client::encoding::text::encode(buffer, &registry).unwrap();
+/// });
+/// ```
+pub fn add_extra_producer<P>(p: P)
+where
+    P: ExtraProducer + 'static,
+{
+    Registries::get().add_extra_producer(Box::new(p));
+}
+
+/// Describes something that can expand prometheus metrics but appending
+/// them in a text format to a provided buffer.
+pub trait ExtraProducer: Send + Sync {
+    /// Takes a buffer and appends prometheus metrics in text format into it.
+    fn produce(&self, buffer: &mut Vec<u8>);
+}
+
+impl<F> ExtraProducer for F
+where
+    F: Fn(&mut Vec<u8>) + Send + Sync,
+{
+    fn produce(&self, buffer: &mut Vec<u8>) {
+        self(buffer)
+    }
+}
diff --git a/foundations/src/telemetry/tokio_runtime_metrics/metrics.rs b/foundations/src/telemetry/tokio_runtime_metrics/metrics.rs
index f7402e9..60144a7 100644
--- a/foundations/src/telemetry/tokio_runtime_metrics/metrics.rs
+++ b/foundations/src/telemetry/tokio_runtime_metrics/metrics.rs
@@ -15,7 +15,7 @@ pub(super) mod tokio_runtime_core {
     pub fn blocking_threads(runtime_name: &Option<Arc<str>>, runtime_id: Option<usize>) -> Gauge;
 
     /// Current number of active tasks on the runtime.
-    pub fn active_tasks(runtime_name: &Option<Arc<str>>, runtime_id: Option<usize>) -> Gauge;
+    pub fn num_alive_tasks(runtime_name: &Option<Arc<str>>, runtime_id: Option<usize>) -> Gauge;
 
     /// Current number of idle blocking threads on the runtime which aren't doing anything.
     ///
diff --git a/foundations/src/telemetry/tokio_runtime_metrics/mod.rs b/foundations/src/telemetry/tokio_runtime_metrics/mod.rs
index 6a3e9b9..c090079 100644
--- a/foundations/src/telemetry/tokio_runtime_metrics/mod.rs
+++ b/foundations/src/telemetry/tokio_runtime_metrics/mod.rs
@@ -11,7 +11,7 @@
 //! |:-------------------------------------------------|:--------------------------------------------------------------------|:---------------------------------------|
 //! | tokio_runtime_workers                            | [`tokio::runtime::RuntimeMetrics::num_workers`]                     | runtime_name?, runtime_id?             |
 //! | tokio_runtime_blocking_threads                   | [`tokio::runtime::RuntimeMetrics::num_blocking_threads`]            | runtime_name?, runtime_id?             |
-//! | tokio_runtime_active_tasks                       | [`tokio::runtime::RuntimeMetrics::active_tasks_count`]              | runtime_name?, runtime_id?             |
+//! | tokio_runtime_num_alive_tasks                    | [`tokio::runtime::RuntimeMetrics::num_alive_tasks`]                 | runtime_name?, runtime_id?             |
 //! | tokio_runtime_idle_blocking_threads              | [`tokio::runtime::RuntimeMetrics::num_idle_blocking_threads`]       | runtime_name?, runtime_id?             |
 //! | tokio_runtime_remote_schedules_total             | [`tokio::runtime::RuntimeMetrics::remote_schedule_count`]           | runtime_name?, runtime_id?             |
 //! | tokio_runtime_budget_forced_yields_total         | [`tokio::runtime::RuntimeMetrics::budget_forced_yield_count`]       | runtime_name?, runtime_id?             |
diff --git a/foundations/src/telemetry/tokio_runtime_metrics/runtime_handle.rs b/foundations/src/telemetry/tokio_runtime_metrics/runtime_handle.rs
index 356bd52..f8a522d 100644
--- a/foundations/src/telemetry/tokio_runtime_metrics/runtime_handle.rs
+++ b/foundations/src/telemetry/tokio_runtime_metrics/runtime_handle.rs
@@ -27,8 +27,8 @@ impl RuntimeHandle {
         tokio_runtime_core::blocking_threads(&self.runtime_name, self.runtime_id)
             .set(metrics.num_blocking_threads() as u64);
 
-        tokio_runtime_core::active_tasks(&self.runtime_name, self.runtime_id)
-            .set(metrics.active_tasks_count() as u64);
+        tokio_runtime_core::num_alive_tasks(&self.runtime_name, self.runtime_id)
+            .set(metrics.num_alive_tasks() as u64);
 
         tokio_runtime_core::idle_blocking_threads(&self.runtime_name, self.runtime_id)
             .set(metrics.num_idle_blocking_threads() as u64);


Test patch: