├── .github ├── dependabot.yml └── workflows │ ├── check-examples.yml │ └── check.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── benches └── bench.rs ├── examples ├── check-examples.sh ├── lambda-http │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ └── main.rs └── lambda │ ├── .gitignore │ ├── Cargo.toml │ └── src │ └── main.rs ├── rustfmt.toml └── src ├── builder.rs ├── collector.rs ├── emf.rs ├── lambda.rs ├── lib.rs └── test.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: cargo 8 | directory: / 9 | schedule: 10 | interval: daily 11 | ignore: 12 | - dependency-name: "*" 13 | # patch and minor updates don't matter for libraries 14 | # remove this ignore rule if your package has binaries 15 | update-types: 16 | - "version-update:semver-patch" 17 | - "version-update:semver-minor" -------------------------------------------------------------------------------- /.github/workflows/check-examples.yml: -------------------------------------------------------------------------------- 1 | name: Check examples 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: dtolnay/rust-toolchain@stable 14 | - uses: Swatinem/rust-cache@v2 15 | 16 | - name: Check examples 17 | working-directory: examples 18 | shell: bash 19 | run: ./check-examples.sh -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | permissions: 2 | contents: read 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | name: check 8 | jobs: 9 | fmt: 10 | runs-on: ubuntu-latest 11 | name: stable / fmt 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: true 16 | - name: Install stable 17 | uses: dtolnay/rust-toolchain@stable 18 | with: 19 | components: rustfmt 20 | - name: cargo fmt --check 21 | run: cargo fmt --check 22 | clippy: 23 | runs-on: ubuntu-latest 24 | name: ${{ matrix.toolchain }} / clippy 25 | permissions: 26 | contents: read 27 | checks: write 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | toolchain: [stable, beta] 32 | steps: 33 | - uses: actions/checkout@v4 34 | with: 35 | submodules: true 36 | - name: Install ${{ matrix.toolchain }} 37 | uses: dtolnay/rust-toolchain@master 38 | with: 39 | toolchain: ${{ matrix.toolchain }} 40 | components: clippy 41 | - name: cargo clippy 42 | uses: actions-rs/clippy-check@v1 43 | with: 44 | token: ${{ secrets.GITHUB_TOKEN }} 45 | doc: 46 | runs-on: ubuntu-latest 47 | name: nightly / doc 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | submodules: true 52 | - name: Install nightly 53 | uses: dtolnay/rust-toolchain@nightly 54 | - name: cargo doc 55 | run: cargo doc --no-deps --all-features 56 | env: 57 | RUSTDOCFLAGS: --cfg docsrs 58 | hack: 59 | runs-on: ubuntu-latest 60 | name: ubuntu / stable / features 61 | steps: 62 | - uses: actions/checkout@v4 63 | with: 64 | submodules: true 65 | - name: Install stable 66 | uses: dtolnay/rust-toolchain@stable 67 | - name: cargo install cargo-hack 68 | uses: taiki-e/install-action@cargo-hack 69 | - name: cargo hack 70 | run: cargo hack --feature-powerset check --lib --tests 71 | msrv: 72 | runs-on: ubuntu-latest 73 | # we use a matrix here just because env can't be used in job names 74 | # https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability 75 | strategy: 76 | matrix: 77 | msrv: [1.81] 78 | name: ubuntu / ${{ matrix.msrv }} 79 | steps: 80 | - uses: actions/checkout@v4 81 | with: 82 | submodules: true 83 | - name: Install ${{ matrix.msrv }} 84 | uses: dtolnay/rust-toolchain@master 85 | with: 86 | toolchain: ${{ matrix.msrv }} 87 | - name: cargo +${{ matrix.msrv }} check 88 | run: cargo check -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cargo/ 2 | /target 3 | Cargo.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.6.1 (2025-05-3) 2 | * Fix bug re-registering gaugues contributed by Øyvind Ringnes (oringnes) 3 | 4 | ## v0.6.0 (2025-03-16) 5 | * updated lambda http to 0.14 6 | * updated tower to 0.5.2 7 | * MSRV increased to 1.81 8 | * added Builder::emit_zeros(bool) for optionally emitting metrics with a delta of zero per feedback from Oliver Gavin (OliverGavin) 9 | 10 | ## v0.5.1 (2024-12-21) 11 | * updated lambda runtime to 0.13 12 | * updatd metrics to 0.24 13 | Thanks Peter Allwin (peterall) and Andrey Kutejko (andy128k) 14 | 15 | ## v0.5.0 (2024-04-05) 16 | * updated lambda runtime to 0.11 17 | * updatd metrics to 0.22.3 18 | Thanks Peter Allwin (peterall) 19 | 20 | ## v0.4.3 (2024-01-13) 21 | * updated lambda runtime to 0.9 22 | 23 | ## v0.4.2 (2022-07-31) 24 | * removed stability disclaimer 25 | * tested metric properties and confirmed that pretty much any json value will get your metric data to ingest 26 | 27 | ## v0.4.1 (2023-07-06) 28 | * updated examples to use info_span! and match casing of lambda power tools 29 | * fixed metrics dependency to 0.21.1 30 | 31 | ## v0.4.0 (2023-07-02) 32 | 33 | * added Builder::lambda_cold_start_span() for tracking cold starts in traces 34 | * added Collector::write_single() for writing a single metric 35 | * Builder::lambda_cold_start_metric() now calls into Collector::flush_single() under the hood 36 | * removed Collector::flush_to, Collector::flush inputs std::io::Write 37 | * replaced Collector::flush_to_with_timestamp with Builder::with_timestamp 38 | * reduced memory allocations in Collector::flush() by replacing a couple single element vectors with arrays 39 | * eliminated a string copy on metrics::describe_* 40 | 41 | ## v0.3.1 (2023-06-29) 42 | 43 | * fixed lambda::handler::run_http and lambda::service::run_http 44 | 45 | ## v0.3.0 (2023-06-29) 46 | 47 | * First draft of the lambda feature 48 | * added MetricsService 49 | * added Builder::lambda_cold_start_metric() 50 | * added Builder::with_lambda_request_id() 51 | * added Builder::with_lambda_xray_trace_id() 52 | * Added a check for more than 30 dimensions/labels 53 | 54 | ## v0.2.0 (2023-06-26) 55 | 56 | * Fixed repository link 57 | * Added a dependency on tracing so we can emit errors when failing to register a metric or overflowing a histogram 58 | 59 | ## v0.1.0 (2023-06-25) 60 | 61 | Initial release -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "metrics_cloudwatch_embedded" 3 | version = "0.6.1" 4 | authors = ["brianmorin "] 5 | edition = "2021" 6 | rust-version = "1.81" 7 | 8 | description = "CloudWatch embedded metrics format emitter for the metrics crate" 9 | license = "Apache-2.0" 10 | documentation = "https://docs.rs/metrics_cloudwatch_embedded" 11 | homepage = "https://github.com/BMorinDrifter/metrics-cloudwatch-embedded" 12 | repository = "https://github.com/BMorinDrifter/metrics-cloudwatch-embedded" 13 | readme = "README.md" 14 | keywords = ["metrics", "cloudwatch", "aws"] 15 | 16 | [features] 17 | default = ["lambda"] 18 | lambda = ["dep:http", "dep:lambda_http", "dep:lambda_runtime", "dep:pin-project", "dep:tower"] 19 | 20 | [dependencies] 21 | http = { version = "1.0", optional = true } 22 | lambda_http = { version = "0.14", optional = true } 23 | lambda_runtime = { version = "0.13", optional = true } 24 | metrics = "0.24" 25 | pin-project = { version = "1", optional = true } 26 | serde = {version = "1.0", features = ["derive"] } 27 | serde_json = "1.0" 28 | tower = {version = "0.5.2", optional = true } 29 | tracing = "0.1" 30 | futures = "0.3" 31 | bytes = "1" 32 | 33 | [dev-dependencies] 34 | criterion = { version = "0.5", features = ["async_tokio"] } 35 | rusty-fork = "0.3.0" 36 | tokio = { version = "1", features = ["macros"] } 37 | tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "json"] } 38 | 39 | [[bench]] 40 | name = "bench" 41 | harness = false 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | metrics_cloudwatch_embedded 2 | =========================== 3 | [![Crates.io version shield](https://img.shields.io/crates/v/metrics_cloudwatch_embedded.svg)](https://crates.io/crates/metrics_cloudwatch_embedded) 4 | [![Crates.io license shield](https://img.shields.io/crates/l/metrics_cloudwatch_embedded.svg)](https://crates.io/crates/metrics_cloudwatch_embedded) 5 | 6 | Purpose 7 | ------- 8 | 9 | Provide a backend for the [`metrics` facade crate](https://crates.io/crates/metrics), 10 | to emit metrics in [CloudWatch Embedded Metrics Format](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html) 11 | 12 | Simple Example 13 | -------------- 14 | 15 | ```rust 16 | let metrics = metrics_cloudwatch_embedded::Builder::new() 17 | .cloudwatch_namespace("MyApplication") 18 | .init() 19 | .unwrap(); 20 | 21 | metrics::counter!("requests", "Method" => "Default").increment(1); 22 | 23 | metrics 24 | .set_property("RequestId", "ABC123") 25 | .flush(std::io::stdout()); 26 | ``` 27 | 28 | AWS Lambda Example 29 | ------------------ 30 | The [Lambda Runtime](https://crates.io/crates/lambda-runtime) intergration feature handles flushing metrics 31 | after each invoke via either `run()` alternatives or `MetricService` which implements the 32 | [`tower::Service`](https://crates.io/crates/tower) trait. 33 | 34 | It also provides optional helpers for: 35 | * emiting a metric on cold starts 36 | * wrapping cold starts in a [`tracing`](https://crates.io/crates/tracing) span 37 | * decorating metric documents with request id and/or x-ray trace id 38 | 39 | In your Cargo.toml add: 40 | ```toml 41 | metrics = "0.24" 42 | metrics_cloudwatch_embedded = { version = "0.6.1", features = ["lambda"] } 43 | tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "json"] } 44 | ``` 45 | 46 | main.rs: 47 | ```rust 48 | use lambda_runtime::{Error, LambdaEvent}; 49 | use metrics_cloudwatch_embedded::lambda::handler::run; 50 | use serde::{Deserialize, Serialize}; 51 | use tracing::{info, info_span}; 52 | 53 | #[derive(Deserialize)] 54 | struct Request {} 55 | 56 | #[derive(Serialize)] 57 | struct Response { 58 | req_id: String, 59 | } 60 | 61 | async fn function_handler(event: LambdaEvent) -> Result { 62 | let resp = Response { 63 | req_id: event.context.request_id.clone(), 64 | }; 65 | 66 | info!("Hello from function_handler"); 67 | 68 | metrics::counter!("requests", "Method" => "Default").increment(1); 69 | 70 | Ok(resp) 71 | } 72 | 73 | #[tokio::main] 74 | async fn main() -> Result<(), Error> { 75 | tracing_subscriber::fmt() 76 | .json() 77 | .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) 78 | .with_target(false) 79 | .with_current_span(false) 80 | .without_time() 81 | .init(); 82 | 83 | let metrics = metrics_cloudwatch_embedded::Builder::new() 84 | .cloudwatch_namespace("MetricsTest") 85 | .with_dimension("function", std::env::var("AWS_LAMBDA_FUNCTION_NAME").unwrap()) 86 | .lambda_cold_start_span(info_span!("cold start").entered()) 87 | .lambda_cold_start_metric("ColdStart") 88 | .with_lambda_request_id("RequestId") 89 | .init() 90 | .unwrap(); 91 | 92 | info!("Hello from main"); 93 | 94 | run(metrics, function_handler).await 95 | } 96 | ``` 97 | CloudWatch log after a single invoke (cold start): 98 | ```plaintext 99 | INIT_START Runtime Version: provided:al2.v19 Runtime Version ARN: arn:aws:lambda:us-west-2::runtime:d1007133cb0d993d9a42f9fc10442cede0efec65d732c7943b51ebb979b8f3f8 100 | {"level":"INFO","fields":{"message":"Hello from main"},"spans":[{"name":"cold start"}]} 101 | START RequestId: fce53486-160d-41e8-b8c3-8ef0fd0f4051 Version: $LATEST 102 | {"_aws":{"Timestamp":1688294472338,"CloudWatchMetrics":[{"Namespace":"MetricsTest","Dimensions":[["Function"]],"Metrics":[{"Name":"ColdStart","Unit":"Count"}]}]},"Function":"MetricsTest","RequestId":"fce53486-160d-41e8-b8c3-8ef0fd0f4051","ColdStart":1} 103 | {"level":"INFO","fields":{"message":"Hello from function_handler"},"spans":[{"name":"cold start"},{"requestId":"fce53486-160d-41e8-b8c3-8ef0fd0f4051","xrayTraceId":"Root=1-64a15448-4aa914a00d66aa066325d7e3;Parent=60a7d0c22fb2f001;Sampled=0;Lineage=16f3a795:0","name":"Lambda runtime invoke"}]} 104 | {"_aws":{"Timestamp":1688294472338,"CloudWatchMetrics":[{"Namespace":"MetricsTest","Dimensions":[["Function","Method"]],"Metrics":[{"Name":"requests"}]}]},"Function":"MetricsTest","Method":"Default","RequestId":"fce53486-160d-41e8-b8c3-8ef0fd0f4051","requests":1} 105 | END RequestId: fce53486-160d-41e8-b8c3-8ef0fd0f4051 106 | REPORT RequestId: fce53486-160d-41e8-b8c3-8ef0fd0f4051 Duration: 1.22 ms Billed Duration: 11 ms Memory Size: 128 MB Max Memory Used: 13 MB Init Duration: 8.99 ms 107 | ``` 108 | 109 | Limitations 110 | ----------- 111 | * Histograms retain up to 100 values (the maximum for a single metric document) between calls to 112 | `collector::Collector::flush`, overflow will report an error via the `tracing` crate 113 | * Dimensions set at initialization via `Builder::with_dimension(...)` 114 | may not overlap with metric `labels` 115 | * Only the subset of metric units in `metrics::Unit` are supported 116 | 117 | * Registering different metric types with the same `metrics::Key` will fail with an error via the `tracing` crate 118 | * The Embedded Metric Format supports a maximum of 30 dimensions per metric, attempting to register a metric with 119 | more than 30 dimensions/labels will fail with an error via the `tracing` crate 120 | 121 | Supported Rust Versions (MSRV) 122 | ------------------------------ 123 | 124 | This crate requires a minimum of Rust 1.81, and is not guaranteed to build on compiler versions earlier than that. 125 | 126 | License 127 | ------- 128 | 129 | This project is licensed under the Apache-2.0 License. Apache-2.0 was chosen to match the [Lambda Runtime](https://crates.io/crates/lambda-runtime) 130 | 131 | Contribution 132 | ------------ 133 | 134 | Unless you explicitly state otherwise, any contribution intentionally submitted 135 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 136 | licensed as above, without any additional terms or conditions. 137 | 138 | Thanks 139 | ------ 140 | * Simon Andersson (ramn) and contributors - For the metrics_cloudwatch crate I used as a reference 141 | * Toby Lawrence (tobz) - For answering my metrics crate questions before I even had something working 142 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | use criterion::{criterion_group, criterion_main, Criterion}; 2 | 3 | fn criterion_benchmark(c: &mut Criterion) { 4 | let metrics = metrics_cloudwatch_embedded::Builder::new() 5 | .cloudwatch_namespace("MyApplication") 6 | .with_dimension("Function", "My_Function_Name") 7 | .init() 8 | .unwrap(); 9 | 10 | metrics::gauge!("four", "Method" => "Default").set(1.0); 11 | metrics::gauge!("score", "Method" => "Default").set(1.0); 12 | metrics::gauge!("andseven", "Method" => "Another").set(1.0); 13 | metrics::gauge!("years", "Method" => "YetAnother").set(1.0); 14 | 15 | c.bench_function("flush", |b| { 16 | b.iter(|| metrics.set_property("RequestId", "ABC123").flush(std::io::sink())) 17 | }); 18 | } 19 | 20 | criterion_group!(benches, criterion_benchmark); 21 | criterion_main!(benches); 22 | -------------------------------------------------------------------------------- /examples/check-examples.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 5 | export CARGO_TARGET_DIR="$SCRIPT_DIR/../target" 6 | 7 | echo "==> Using shared target directory: $CARGO_TARGET_DIR" 8 | 9 | for f in *; do 10 | if [ -d "$f" ]; then 11 | echo "==> Checking example: $f" 12 | cd $f 13 | cargo check 14 | cd .. 15 | fi 16 | done 17 | -------------------------------------------------------------------------------- /examples/lambda-http/.gitignore: -------------------------------------------------------------------------------- 1 | /.cargo/ 2 | /target -------------------------------------------------------------------------------- /examples/lambda-http/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lambda-http-test" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | lambda_http = { version = "0.14", default-features = false, features = ["apigw_http"] } 8 | lambda_runtime = "0.13" 9 | metrics = "0.24" 10 | metrics_cloudwatch_embedded = { path = "../..", features = ["lambda"] } 11 | serde = {version = "1.0", features = ["derive"] } 12 | tokio = { version = "1", features = ["macros"] } 13 | tracing = { version = "0.1", features = ["log"] } 14 | tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "json"] } 15 | 16 | [profile.release] 17 | opt-level = "z" 18 | lto = true 19 | codegen-units = 1 20 | panic = "abort" -------------------------------------------------------------------------------- /examples/lambda-http/src/main.rs: -------------------------------------------------------------------------------- 1 | use lambda_http::{Error, IntoResponse, Request, Response}; 2 | use metrics_cloudwatch_embedded::lambda::handler::run_http; 3 | use serde::Deserialize; 4 | use tracing::{info, info_span}; 5 | 6 | #[derive(Deserialize)] 7 | struct Payload {} 8 | 9 | async fn function_handler(_event: Request) -> Result { 10 | info!("Hello from function_handler"); 11 | 12 | metrics::counter!("requests", "Method" => "Default").increment(1); 13 | 14 | let resp = Response::builder().status(200).body("".to_string()).map_err(Box::new)?; 15 | Ok(resp) 16 | } 17 | 18 | #[tokio::main] 19 | async fn main() -> Result<(), Error> { 20 | tracing_subscriber::fmt() 21 | .json() 22 | .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) 23 | .with_target(false) 24 | .with_current_span(false) 25 | .without_time() 26 | .init(); 27 | 28 | let metrics = metrics_cloudwatch_embedded::Builder::new() 29 | .cloudwatch_namespace("MetricsTest") 30 | .with_dimension("function", std::env::var("AWS_LAMBDA_FUNCTION_NAME").unwrap()) 31 | .lambda_cold_start_span(info_span!("cold start")) 32 | .lambda_cold_start_metric("ColdStart") 33 | .with_lambda_request_id("RequestId") 34 | .init() 35 | .unwrap(); 36 | 37 | info!("Hello from main"); 38 | 39 | run_http(metrics, function_handler).await 40 | } 41 | -------------------------------------------------------------------------------- /examples/lambda/.gitignore: -------------------------------------------------------------------------------- 1 | /.cargo/ 2 | /target -------------------------------------------------------------------------------- /examples/lambda/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lambda-test" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | lambda_runtime = "0.13" 8 | metrics = "0.24" 9 | metrics_cloudwatch_embedded = { path = "../..", features = ["lambda"] } 10 | serde = {version = "1.0", features = ["derive"] } 11 | tokio = { version = "1", features = ["macros"] } 12 | tracing = { version = "0.1", features = ["log"] } 13 | tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "json"] } 14 | 15 | [profile.release] 16 | opt-level = "z" 17 | lto = true 18 | codegen-units = 1 19 | panic = "abort" -------------------------------------------------------------------------------- /examples/lambda/src/main.rs: -------------------------------------------------------------------------------- 1 | use lambda_runtime::{Error, LambdaEvent}; 2 | use metrics_cloudwatch_embedded::lambda::handler::run; 3 | use serde::{Deserialize, Serialize}; 4 | use tracing::{info, info_span}; 5 | 6 | #[derive(Deserialize)] 7 | struct Request {} 8 | 9 | #[derive(Serialize)] 10 | struct Response { 11 | req_id: String, 12 | } 13 | 14 | async fn function_handler(event: LambdaEvent) -> Result { 15 | let resp = Response { 16 | req_id: event.context.request_id, 17 | }; 18 | 19 | info!("Hello from function_handler"); 20 | 21 | metrics::counter!("requests", "Method" => "Default").increment(1); 22 | 23 | Ok(resp) 24 | } 25 | 26 | #[tokio::main] 27 | async fn main() -> Result<(), Error> { 28 | tracing_subscriber::fmt() 29 | .json() 30 | .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) 31 | .with_target(false) 32 | .with_current_span(false) 33 | .without_time() 34 | .init(); 35 | 36 | let metrics = metrics_cloudwatch_embedded::Builder::new() 37 | .cloudwatch_namespace("MetricsTest") 38 | .with_dimension("function", std::env::var("AWS_LAMBDA_FUNCTION_NAME").unwrap()) 39 | .lambda_cold_start_span(info_span!("cold start")) 40 | .lambda_cold_start_metric("ColdStart") 41 | .with_lambda_request_id("RequestId") 42 | .init() 43 | .unwrap(); 44 | 45 | info!("Hello from main"); 46 | 47 | run(metrics, function_handler).await 48 | } 49 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | # imports_granularity is unstable 3 | # # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#merge_imports 4 | # imports_granularity = "Crate" 5 | # https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#max_width 6 | max_width = 120 7 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use super::{collector, Error}; 3 | use metrics::SharedString; 4 | 5 | /// Builder for the Embedded Cloudwatch Metrics Collector 6 | /// 7 | /// # Example 8 | /// ``` 9 | /// let metrics = metrics_cloudwatch_embedded::Builder::new() 10 | /// .cloudwatch_namespace("MyApplication") 11 | /// .init() 12 | /// .unwrap(); 13 | /// ``` 14 | pub struct Builder { 15 | cloudwatch_namespace: Option, 16 | default_dimensions: Vec<(SharedString, SharedString)>, 17 | timestamp: Option, 18 | emit_zeros: bool, 19 | #[cfg(feature = "lambda")] 20 | lambda_cold_start_span: Option, 21 | #[cfg(feature = "lambda")] 22 | lambda_cold_start: Option<&'static str>, 23 | #[cfg(feature = "lambda")] 24 | lambda_request_id: Option<&'static str>, 25 | #[cfg(feature = "lambda")] 26 | lambda_xray_trace_id: Option<&'static str>, 27 | } 28 | 29 | impl Builder { 30 | #[allow(clippy::new_without_default)] 31 | pub fn new() -> Self { 32 | Builder { 33 | cloudwatch_namespace: Default::default(), 34 | default_dimensions: Default::default(), 35 | timestamp: None, 36 | emit_zeros: false, 37 | #[cfg(feature = "lambda")] 38 | lambda_cold_start_span: None, 39 | #[cfg(feature = "lambda")] 40 | lambda_cold_start: None, 41 | #[cfg(feature = "lambda")] 42 | lambda_request_id: None, 43 | #[cfg(feature = "lambda")] 44 | lambda_xray_trace_id: None, 45 | } 46 | } 47 | 48 | /// Sets the CloudWatch namespace for all metrics 49 | /// * Must be set or init() will return Err("cloudwatch_namespace missing") 50 | pub fn cloudwatch_namespace(self, namespace: impl Into) -> Self { 51 | Self { 52 | cloudwatch_namespace: Some(namespace.into()), 53 | ..self 54 | } 55 | } 56 | 57 | /// Adds a static dimension (name, value), that will be sent with each MetricDatum. 58 | /// * This method can be called multiple times with distinct names 59 | /// * Dimention names may not overlap with metrics::Label names 60 | /// * Metrics can have no more than 30 dimensions + labels 61 | pub fn with_dimension(mut self, name: impl Into, value: impl Into) -> Self { 62 | self.default_dimensions.push((name.into(), value.into())); 63 | self 64 | } 65 | 66 | /// Sets the timestamp for flush to a constant value to simplify tests 67 | pub fn with_timestamp(mut self, timestamp: u64) -> Self { 68 | self.timestamp = Some(timestamp); 69 | self 70 | } 71 | 72 | /// If set to true, the collector will emit a zero value metrics instead of skipping 73 | /// defaults to `false` 74 | pub fn emit_zeros(mut self, emit_zeros: bool) -> Self { 75 | self.emit_zeros = emit_zeros; 76 | self 77 | } 78 | 79 | /// Passes a tracing span to drop after our cold start is complete 80 | /// 81 | /// *requires the `lambda` feature flag* 82 | /// 83 | #[cfg(feature = "lambda")] 84 | pub fn lambda_cold_start_span(mut self, cold_start_span: tracing::span::Span) -> Self { 85 | self.lambda_cold_start_span = Some(cold_start_span); 86 | self 87 | } 88 | 89 | /// Emits a cold start metric with the given name once to mark a cold start 90 | /// 91 | /// *requires the `lambda` feature flag* 92 | /// 93 | #[cfg(feature = "lambda")] 94 | pub fn lambda_cold_start_metric(mut self, name: &'static str) -> Self { 95 | self.lambda_cold_start = Some(name); 96 | self 97 | } 98 | 99 | /// Decorates every metric with request_id from the lambda request context as a property 100 | /// with the given name 101 | /// 102 | /// *requires the `lambda` feature flag* 103 | /// 104 | #[cfg(feature = "lambda")] 105 | pub fn with_lambda_request_id(mut self, name: &'static str) -> Self { 106 | self.lambda_request_id = Some(name); 107 | self 108 | } 109 | 110 | /// Decorates every metric with lambda_xray_trace_id from the lambda request context as a property 111 | /// with the given name 112 | /// 113 | /// *requires the `lambda` feature flag* 114 | /// 115 | #[cfg(feature = "lambda")] 116 | pub fn with_lambda_xray_trace_id(mut self, name: &'static str) -> Self { 117 | self.lambda_xray_trace_id = Some(name); 118 | self 119 | } 120 | 121 | /// Private helper for consuming the builder into collector configuration (non-lambda) 122 | #[cfg(not(feature = "lambda"))] 123 | fn build(self) -> Result { 124 | Ok(collector::Config { 125 | cloudwatch_namespace: self.cloudwatch_namespace.ok_or("cloudwatch_namespace missing")?, 126 | default_dimensions: self.default_dimensions, 127 | timestamp: self.timestamp, 128 | emit_zeros: false, 129 | }) 130 | } 131 | 132 | /// Private helper for consuming the builder into collector configuration (lambda) 133 | #[cfg(feature = "lambda")] 134 | fn build(self) -> Result<(collector::Config, Option), Error> { 135 | Ok(( 136 | collector::Config { 137 | cloudwatch_namespace: self.cloudwatch_namespace.ok_or("cloudwatch_namespace missing")?, 138 | default_dimensions: self.default_dimensions, 139 | timestamp: self.timestamp, 140 | emit_zeros: self.emit_zeros, 141 | lambda_cold_start: self.lambda_cold_start, 142 | lambda_request_id: self.lambda_request_id, 143 | lambda_xray_trace_id: self.lambda_xray_trace_id, 144 | }, 145 | self.lambda_cold_start_span, 146 | )) 147 | } 148 | 149 | /// Intialize the metrics collector including the call to [metrics::set_global_recorder] 150 | pub fn init(self) -> Result<&'static collector::Collector, Error> { 151 | #[cfg(not(feature = "lambda"))] 152 | let config = self.build()?; 153 | #[cfg(not(feature = "lambda"))] 154 | let collector: &'static collector::Collector = Box::leak(Box::new(collector::Collector::new(config))); 155 | 156 | // Since we need to mutate the cold start span (if present), we can't just drop it in collector::Config 157 | #[cfg(feature = "lambda")] 158 | let (config, lambda_cold_start_span) = self.build()?; 159 | #[cfg(feature = "lambda")] 160 | let collector: &'static collector::Collector = 161 | Box::leak(Box::new(collector::Collector::new(config, lambda_cold_start_span))); 162 | 163 | metrics::set_global_recorder::(collector.into()).map_err(|e| e.to_string())?; 164 | Ok(collector) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/collector.rs: -------------------------------------------------------------------------------- 1 | //! # Collector 2 | //! 3 | //! Metrics Collector + Emitter returned from metrics_cloudwatch_embedded::Builder 4 | 5 | #![allow(dead_code)] 6 | use super::emf; 7 | use metrics::SharedString; 8 | use serde_json::value::Value; 9 | use std::collections::{BTreeMap, HashMap}; 10 | use std::sync::atomic::{AtomicU64, Ordering}; 11 | use std::sync::{mpsc, Arc, Mutex}; 12 | use std::time::{SystemTime, UNIX_EPOCH}; 13 | use tracing::error; 14 | 15 | /// The Embedded Metric Format supports a maximum of 100 values per key 16 | const MAX_HISTOGRAM_VALUES: usize = 100; 17 | 18 | /// The Embedded Metric Format supports a maximum of 30 dimensions per metric 19 | const MAX_DIMENSIONS: usize = 30; 20 | 21 | /// Configuration via Builder 22 | pub struct Config { 23 | pub cloudwatch_namespace: SharedString, 24 | pub default_dimensions: Vec<(SharedString, SharedString)>, 25 | pub timestamp: Option, 26 | pub emit_zeros: bool, 27 | #[cfg(feature = "lambda")] 28 | pub lambda_cold_start: Option<&'static str>, 29 | #[cfg(feature = "lambda")] 30 | pub lambda_request_id: Option<&'static str>, 31 | #[cfg(feature = "lambda")] 32 | pub lambda_xray_trace_id: Option<&'static str>, 33 | } 34 | 35 | /// Histogram Handler implemented as mpsc::SyncSender 36 | struct HistogramHandle { 37 | sender: mpsc::SyncSender, 38 | } 39 | 40 | impl metrics::HistogramFn for HistogramHandle { 41 | // Sends the metric value to our sync_channel 42 | fn record(&self, value: f64) { 43 | if self.sender.send(value).is_err() { 44 | error!("Failed to record histogram value, more than 100 unflushed values?"); 45 | } 46 | } 47 | } 48 | 49 | // Metric information stored in an index 50 | enum MetricInfo { 51 | Counter(CounterInfo), 52 | Gauge(GaugeInfo), 53 | Histogram(HistogramInfo), 54 | } 55 | 56 | struct CounterInfo { 57 | value: Arc, 58 | } 59 | 60 | struct GaugeInfo { 61 | value: Arc, 62 | } 63 | 64 | struct HistogramInfo { 65 | sender: mpsc::SyncSender, 66 | receiver: mpsc::Receiver, 67 | } 68 | 69 | /// Collector state used to register new metrics and flush 70 | /// This lives within a mutex 71 | struct CollectorState { 72 | /// Tree of labels to name to metric details 73 | info_tree: BTreeMap, BTreeMap>, 74 | /// Store units seperate because describe_xxx isn't scoped to labels 75 | /// Key is a copied String until at least metrics cl #381 is released in metrics 76 | units: HashMap, 77 | /// Properties to be written with metrics 78 | properties: BTreeMap, 79 | /// Cold start span to drop after first invoke 80 | #[cfg(feature = "lambda")] 81 | lambda_cold_start_span: Option, 82 | } 83 | 84 | /// Embedded CloudWatch Metrics Collector + Emitter 85 | /// 86 | /// Use [Builder](super::Builder) to construct 87 | /// 88 | /// # Example 89 | /// ``` 90 | /// let metrics = metrics_cloudwatch_embedded::Builder::new() 91 | /// .cloudwatch_namespace("MyApplication") 92 | /// .init() 93 | /// .unwrap(); 94 | /// 95 | /// metrics::counter!("requests", "Method" => "Default").increment(1); 96 | /// 97 | /// metrics 98 | /// .set_property("RequestId", "ABC123") 99 | /// .flush(std::io::stdout()); 100 | /// ``` 101 | pub struct Collector { 102 | state: Mutex, 103 | pub config: Config, 104 | } 105 | 106 | impl Collector { 107 | pub fn new(config: Config, #[cfg(feature = "lambda")] lambda_cold_start_span: Option) -> Self { 108 | Self { 109 | state: Mutex::new(CollectorState { 110 | info_tree: BTreeMap::new(), 111 | units: HashMap::new(), 112 | properties: BTreeMap::new(), 113 | #[cfg(feature = "lambda")] 114 | lambda_cold_start_span, 115 | }), 116 | config, 117 | } 118 | } 119 | 120 | /// Set a property to emit with the metrics 121 | /// * Properites persist accross flush calls 122 | /// * Setting a property with same name multiple times will overwrite the previous value 123 | pub fn set_property(&self, name: impl Into, value: impl Into) -> &Self { 124 | { 125 | let mut state = self.state.lock().unwrap(); 126 | state.properties.insert(name.into(), value.into()); 127 | } 128 | self 129 | } 130 | 131 | /// Removes a property to emit with the metrics 132 | pub fn remove_property<'a>(&'a self, name: impl Into<&'a str>) -> &'a Self { 133 | { 134 | let mut state = self.state.lock().unwrap(); 135 | state.properties.remove(name.into()); 136 | } 137 | self 138 | } 139 | 140 | /// Compute the timestamp unless it was set via [Builder::with_timestamp] 141 | fn timestamp(&self) -> u64 { 142 | // Timestamp can be set to a 143 | match self.config.timestamp { 144 | Some(t) => t, 145 | None => SystemTime::now() 146 | .duration_since(UNIX_EPOCH) 147 | .expect("Time went backwards") 148 | .as_millis() as u64, 149 | } 150 | } 151 | 152 | /// Flush the current counter values to an implementation of std::io::Write 153 | pub fn flush(&self, mut writer: impl std::io::Write) -> std::io::Result<()> { 154 | let mut emf = emf::EmbeddedMetrics { 155 | aws: emf::EmbeddedMetricsAws { 156 | timestamp: self.timestamp(), 157 | cloudwatch_metrics: [emf::EmbeddedNamespace { 158 | namespace: &self.config.cloudwatch_namespace, 159 | dimensions: [Vec::with_capacity(MAX_DIMENSIONS)], 160 | metrics: Vec::new(), 161 | }], 162 | }, 163 | dimensions: BTreeMap::new(), 164 | properties: BTreeMap::new(), 165 | values: BTreeMap::new(), 166 | }; 167 | 168 | for dimension in &self.config.default_dimensions { 169 | emf.aws.cloudwatch_metrics[0].dimensions[0].push(&dimension.0); 170 | emf.dimensions.insert(&dimension.0, &dimension.1); 171 | } 172 | 173 | // Delay aquiring the mutex until we need it 174 | let state = self.state.lock().unwrap(); 175 | 176 | for (key, value) in &state.properties { 177 | emf.properties.insert(key, value.clone()); 178 | } 179 | 180 | // Emit an embedded metrics document for each distinct label set 181 | for (labels, metrics) in &state.info_tree { 182 | emf.aws.cloudwatch_metrics[0].metrics.clear(); 183 | emf.values.clear(); 184 | let mut should_flush = false; 185 | 186 | for label in labels { 187 | emf.aws.cloudwatch_metrics[0].dimensions[0].push(label.key()); 188 | emf.dimensions.insert(label.key(), label.value()); 189 | } 190 | 191 | for (key, info) in metrics { 192 | match info { 193 | MetricInfo::Counter(counter) => { 194 | let value = counter.value.swap(0, Ordering::Relaxed); 195 | 196 | // Omit this metric if there is no delta since last flushed, unless we're configured to emit zeros 197 | if value != 0 || self.config.emit_zeros { 198 | emf.aws.cloudwatch_metrics[0].metrics.push(emf::EmbeddedMetric { 199 | name: key.name(), 200 | unit: state.units.get(key.name()).map(emf::unit_to_str), 201 | }); 202 | emf.values.insert(key.name(), value.into()); 203 | should_flush = true; 204 | } 205 | } 206 | MetricInfo::Gauge(gauge) => { 207 | let value = f64::from_bits(gauge.value.load(Ordering::Relaxed)); 208 | 209 | emf.aws.cloudwatch_metrics[0].metrics.push(emf::EmbeddedMetric { 210 | name: key.name(), 211 | unit: state.units.get(key.name()).map(emf::unit_to_str), 212 | }); 213 | emf.values.insert(key.name(), value.into()); 214 | should_flush = true; 215 | } 216 | MetricInfo::Histogram(histogram) => { 217 | let mut values: Vec = Vec::new(); 218 | while let Ok(value) = histogram.receiver.try_recv() { 219 | values.push(value); 220 | } 221 | 222 | // Omit this metric if there is no new values since last flushed 223 | if !values.is_empty() { 224 | emf.aws.cloudwatch_metrics[0].metrics.push(emf::EmbeddedMetric { 225 | name: key.name(), 226 | unit: state.units.get(key.name()).map(emf::unit_to_str), 227 | }); 228 | emf.values.insert(key.name(), values.into()); 229 | should_flush = true; 230 | } 231 | } 232 | } 233 | } 234 | 235 | // Skip if we have no data to flush 236 | if should_flush { 237 | serde_json::to_writer(&mut writer, &emf)?; 238 | writeln!(writer)?; 239 | } 240 | 241 | // Rollback our labels/dimensions (but keep any default dimensions) 242 | for label in labels { 243 | emf.aws.cloudwatch_metrics[0].dimensions[0].pop(); 244 | emf.dimensions.remove(&label.key()); 245 | } 246 | } 247 | 248 | Ok(()) 249 | } 250 | 251 | /// Write a single metric to an implementation of [std::io::Write], avoids the overhead of 252 | /// going through the metrics recorder 253 | pub fn write_single( 254 | &self, 255 | name: impl Into, 256 | unit: Option, 257 | value: impl Into, 258 | mut writer: impl std::io::Write, 259 | ) -> std::io::Result<()> { 260 | let mut emf = emf::EmbeddedMetrics { 261 | aws: emf::EmbeddedMetricsAws { 262 | timestamp: self.timestamp(), 263 | cloudwatch_metrics: [emf::EmbeddedNamespace { 264 | namespace: &self.config.cloudwatch_namespace, 265 | dimensions: [Vec::with_capacity(MAX_DIMENSIONS)], 266 | metrics: Vec::new(), 267 | }], 268 | }, 269 | dimensions: BTreeMap::new(), 270 | properties: BTreeMap::new(), 271 | values: BTreeMap::new(), 272 | }; 273 | 274 | for dimension in &self.config.default_dimensions { 275 | emf.aws.cloudwatch_metrics[0].dimensions[0].push(&dimension.0); 276 | emf.dimensions.insert(&dimension.0, &dimension.1); 277 | } 278 | 279 | // Delay aquiring the mutex until we need it 280 | let state = self.state.lock().unwrap(); 281 | 282 | for (key, value) in &state.properties { 283 | emf.properties.insert(key, value.clone()); 284 | } 285 | 286 | let name = name.into(); 287 | emf.aws.cloudwatch_metrics[0].metrics.push(emf::EmbeddedMetric { 288 | name: &name, 289 | unit: unit.map(|u| emf::unit_to_str(&u)), 290 | }); 291 | emf.values.insert(&name, value.into()); 292 | 293 | serde_json::to_writer(&mut writer, &emf)?; 294 | writeln!(writer) 295 | } 296 | 297 | /// update the unit for a metric name, disregard what metric type it is 298 | fn update_unit(&self, key: metrics::KeyName, unit: Option) { 299 | let mut state = self.state.lock().unwrap(); 300 | 301 | if let Some(unit) = unit { 302 | state.units.insert(key, unit); 303 | } else { 304 | state.units.remove(&key); 305 | } 306 | } 307 | 308 | #[cfg(feature = "lambda")] 309 | pub fn take_cold_start_span(&self) -> Option { 310 | let mut state = self.state.lock().unwrap(); 311 | state.lambda_cold_start_span.take() 312 | } 313 | } 314 | 315 | pub struct Recorder { 316 | collector: &'static Collector, 317 | } 318 | 319 | impl From<&'static Collector> for Recorder { 320 | fn from(collector: &'static Collector) -> Self { 321 | Self { collector } 322 | } 323 | } 324 | 325 | impl metrics::Recorder for Recorder { 326 | fn describe_counter(&self, key: metrics::KeyName, unit: Option, _description: SharedString) { 327 | self.collector.update_unit(key, unit) 328 | } 329 | 330 | fn describe_gauge(&self, key: metrics::KeyName, unit: Option, _description: SharedString) { 331 | self.collector.update_unit(key, unit) 332 | } 333 | 334 | fn describe_histogram(&self, key: metrics::KeyName, unit: Option, _description: SharedString) { 335 | self.collector.update_unit(key, unit) 336 | } 337 | 338 | #[allow(clippy::mutable_key_type)] // metrics::Key has interior mutability 339 | fn register_counter(&self, key: &metrics::Key, _metadata: &metrics::Metadata) -> metrics::Counter { 340 | // Build our own copy of the labels before aquiring the mutex 341 | let labels: Vec = key.labels().cloned().collect(); 342 | 343 | if self.collector.config.default_dimensions.len() + labels.len() > MAX_DIMENSIONS { 344 | error!("Unable to register counter {key} as it has more than {MAX_DIMENSIONS} dimensions/labels"); 345 | return metrics::Counter::noop(); 346 | } 347 | 348 | let mut state = self.collector.state.lock().unwrap(); 349 | 350 | // Does this metric already exist? 351 | if let Some(label_info) = state.info_tree.get_mut(&labels) { 352 | if let Some(info) = label_info.get(key) { 353 | match info { 354 | MetricInfo::Counter(info) => { 355 | return metrics::Counter::from_arc(info.value.clone()); 356 | } 357 | MetricInfo::Gauge(_) => { 358 | error!("Unable to register counter {key} as it was already registered as a gauge"); 359 | return metrics::Counter::noop(); 360 | } 361 | MetricInfo::Histogram(_) => { 362 | error!("Unable to register counter {key} as it was already registered as a histogram"); 363 | return metrics::Counter::noop(); 364 | } 365 | } 366 | } else { 367 | // Label exists, counter does not 368 | let value = Arc::new(AtomicU64::new(0)); 369 | label_info.insert(key.clone(), MetricInfo::Counter(CounterInfo { value: value.clone() })); 370 | 371 | return metrics::Counter::from_arc(value); 372 | } 373 | } 374 | 375 | // Neither the label nor the counter exists 376 | let value = Arc::new(AtomicU64::new(0)); 377 | let mut label_info = BTreeMap::new(); 378 | label_info.insert(key.clone(), MetricInfo::Counter(CounterInfo { value: value.clone() })); 379 | state.info_tree.insert(labels, label_info); 380 | 381 | metrics::Counter::from_arc(value) 382 | } 383 | 384 | #[allow(clippy::mutable_key_type)] // metrics::Key has interior mutability 385 | fn register_gauge(&self, key: &metrics::Key, _metadata: &metrics::Metadata) -> metrics::Gauge { 386 | // Build our own copy of the labels before aquiring the mutex 387 | let labels: Vec = key.labels().cloned().collect(); 388 | 389 | if self.collector.config.default_dimensions.len() + labels.len() > MAX_DIMENSIONS { 390 | error!( 391 | "Unable to register counter {key} as a gauge as it has more than {MAX_DIMENSIONS} dimensions/labels" 392 | ); 393 | return metrics::Gauge::noop(); 394 | } 395 | 396 | let mut state = self.collector.state.lock().unwrap(); 397 | 398 | // Does this metric already exist? 399 | if let Some(label_info) = state.info_tree.get_mut(&labels) { 400 | if let Some(info) = label_info.get(key) { 401 | match info { 402 | MetricInfo::Gauge(info) => { 403 | return metrics::Gauge::from_arc(info.value.clone()); 404 | } 405 | MetricInfo::Counter(_) => { 406 | error!("Unable to register gauge {key} as it was already registered as a counter"); 407 | return metrics::Gauge::noop(); 408 | } 409 | MetricInfo::Histogram(_) => { 410 | error!("Unable to register gauge {key} as it was already registered as a histogram"); 411 | return metrics::Gauge::noop(); 412 | } 413 | } 414 | } else { 415 | // Label exists, gauge does not 416 | let value = Arc::new(AtomicU64::new(0)); 417 | label_info.insert(key.clone(), MetricInfo::Gauge(GaugeInfo { value: value.clone() })); 418 | 419 | return metrics::Gauge::from_arc(value); 420 | } 421 | } 422 | 423 | // Neither the label nor the gauge exists 424 | let value = Arc::new(AtomicU64::new(0)); 425 | let mut label_info = BTreeMap::new(); 426 | label_info.insert(key.clone(), MetricInfo::Gauge(GaugeInfo { value: value.clone() })); 427 | state.info_tree.insert(labels, label_info); 428 | 429 | metrics::Gauge::from_arc(value) 430 | } 431 | 432 | #[allow(clippy::mutable_key_type)] // metrics::Key has interior mutability 433 | fn register_histogram(&self, key: &metrics::Key, _metadata: &metrics::Metadata) -> metrics::Histogram { 434 | // Build our own copy of the labels before aquiring the mutex 435 | let labels: Vec = key.labels().cloned().collect(); 436 | 437 | if self.collector.config.default_dimensions.len() + labels.len() > MAX_DIMENSIONS { 438 | error!("Unable to register histogram {key} as it has more than {MAX_DIMENSIONS} dimensions/labels"); 439 | return metrics::Histogram::noop(); 440 | } 441 | 442 | let mut state = self.collector.state.lock().unwrap(); 443 | 444 | // Does this metric already exist? 445 | if let Some(label_info) = state.info_tree.get_mut(&labels) { 446 | if let Some(info) = label_info.get(key) { 447 | match info { 448 | MetricInfo::Histogram(info) => { 449 | let histogram = Arc::new(HistogramHandle { 450 | sender: info.sender.clone(), 451 | }); 452 | return metrics::Histogram::from_arc(histogram); 453 | } 454 | MetricInfo::Counter(_) => { 455 | error!("Unable to register histogram {key} as it was already registered as a counter"); 456 | return metrics::Histogram::noop(); 457 | } 458 | MetricInfo::Gauge(_) => { 459 | error!("Unable to register histogram {key} as it was already registered as a gauge"); 460 | return metrics::Histogram::noop(); 461 | } 462 | } 463 | } else { 464 | // Label exists, histogram does not 465 | let (sender, receiver) = mpsc::sync_channel(MAX_HISTOGRAM_VALUES); 466 | let histogram = Arc::new(HistogramHandle { sender: sender.clone() }); 467 | label_info.insert(key.clone(), MetricInfo::Histogram(HistogramInfo { sender, receiver })); 468 | 469 | return metrics::Histogram::from_arc(histogram); 470 | } 471 | } 472 | 473 | // Neither the label nor the gauge exists 474 | let (sender, receiver) = mpsc::sync_channel(MAX_HISTOGRAM_VALUES); 475 | let histogram = Arc::new(HistogramHandle { sender: sender.clone() }); 476 | let mut label_info = BTreeMap::new(); 477 | label_info.insert(key.clone(), MetricInfo::Histogram(HistogramInfo { sender, receiver })); 478 | state.info_tree.insert(labels, label_info); 479 | 480 | metrics::Histogram::from_arc(histogram) 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /src/emf.rs: -------------------------------------------------------------------------------- 1 | //! # EMF 2 | //! 3 | //! Helpers for serializing CloudWatch Embedded Metrics via serde_json 4 | //! 5 | //! 6 | 7 | use serde::Serialize; 8 | use serde_json::value::Value; 9 | use std::collections::BTreeMap; 10 | 11 | #[derive(Serialize)] 12 | pub struct EmbeddedMetrics<'a> { 13 | #[serde(rename = "_aws")] 14 | pub aws: EmbeddedMetricsAws<'a>, 15 | #[serde(flatten)] 16 | pub dimensions: BTreeMap<&'a str, &'a str>, 17 | #[serde(flatten)] 18 | pub properties: BTreeMap<&'a str, Value>, 19 | #[serde(flatten)] 20 | pub values: BTreeMap<&'a str, Value>, 21 | } 22 | 23 | #[derive(Serialize)] 24 | pub struct EmbeddedMetricsAws<'a> { 25 | #[serde(rename = "Timestamp")] 26 | pub timestamp: u64, 27 | // This crate never uses more than one namespace in a metrics document 28 | #[serde(rename = "CloudWatchMetrics")] 29 | pub cloudwatch_metrics: [EmbeddedNamespace<'a>; 1], 30 | } 31 | 32 | #[derive(Serialize)] 33 | pub struct EmbeddedNamespace<'a> { 34 | #[serde(rename = "Namespace")] 35 | pub namespace: &'a str, 36 | // This create builds a single dimension set with all dimensions 37 | #[serde(rename = "Dimensions")] 38 | pub dimensions: [Vec<&'a str>; 1], 39 | #[serde(rename = "Metrics")] 40 | pub metrics: Vec>, 41 | } 42 | 43 | #[derive(Serialize)] 44 | pub struct EmbeddedMetric<'a> { 45 | #[serde(rename = "Name")] 46 | pub name: &'a str, 47 | #[serde(rename = "Unit")] 48 | #[serde(skip_serializing_if = "Option::is_none")] 49 | pub unit: Option<&'a str>, 50 | } 51 | 52 | /// Convert a metrics::Unit into the cloudwatch string 53 | /// 54 | /// 55 | pub fn unit_to_str(unit: &metrics::Unit) -> &'static str { 56 | match unit { 57 | metrics::Unit::Count => "Count", 58 | metrics::Unit::Percent => "Percent", 59 | metrics::Unit::Seconds => "Seconds", 60 | metrics::Unit::Milliseconds => "Milliseconds", 61 | metrics::Unit::Microseconds => "Microseconds", 62 | metrics::Unit::Nanoseconds => "Nanoseconds", 63 | metrics::Unit::Tebibytes => "Terabytes", 64 | metrics::Unit::Gibibytes => "Gigabytes", 65 | metrics::Unit::Mebibytes => "Megabytes", 66 | metrics::Unit::Kibibytes => "Kilobytes", 67 | metrics::Unit::Bytes => "Bytes", 68 | metrics::Unit::TerabitsPerSecond => "Terabits/Second", 69 | metrics::Unit::GigabitsPerSecond => "Gigabits/Second", 70 | metrics::Unit::MegabitsPerSecond => "Megabits/Second", 71 | metrics::Unit::KilobitsPerSecond => "Kilobits/Second", 72 | metrics::Unit::BitsPerSecond => "Bits/Second", 73 | metrics::Unit::CountPerSecond => "Count/Second", 74 | } 75 | } 76 | 77 | #[allow(unused_imports)] 78 | mod tests { 79 | use super::*; 80 | use serde_json::json; 81 | 82 | #[test] 83 | fn embedded_metrics() { 84 | let mut metrics_test = EmbeddedMetrics { 85 | aws: EmbeddedMetricsAws { 86 | timestamp: 0, 87 | cloudwatch_metrics: [EmbeddedNamespace { 88 | namespace: "GameServerMetrics", 89 | dimensions: [vec!["Address", "Port"]], 90 | metrics: Vec::new(), 91 | }], 92 | }, 93 | dimensions: BTreeMap::new(), 94 | properties: BTreeMap::new(), 95 | values: BTreeMap::new(), 96 | }; 97 | 98 | metrics_test.aws.timestamp = 1687394207903; 99 | 100 | metrics_test.dimensions.insert("Address", "10.172.207.225"); 101 | metrics_test.dimensions.insert("Port", "7779"); 102 | 103 | metrics_test.aws.cloudwatch_metrics[0].metrics.clear(); 104 | metrics_test.values.clear(); 105 | 106 | metrics_test.aws.cloudwatch_metrics[0].metrics.push(EmbeddedMetric { 107 | name: "FrameTime", 108 | unit: Some(unit_to_str(&metrics::Unit::Milliseconds)), 109 | }); 110 | metrics_test.values.insert("FrameTime", json!(10.0)); 111 | 112 | metrics_test.aws.cloudwatch_metrics[0].metrics.push(EmbeddedMetric { 113 | name: "CpuUsage", 114 | unit: Some(unit_to_str(&metrics::Unit::Percent)), 115 | }); 116 | metrics_test.values.insert("CpuUsage", json!(5.5)); 117 | 118 | metrics_test.aws.cloudwatch_metrics[0].metrics.push(EmbeddedMetric { 119 | name: "MemoryUsage", 120 | unit: Some(unit_to_str(&metrics::Unit::Kibibytes)), 121 | }); 122 | metrics_test.values.insert("MemoryUsage", json!(10 * 1024)); 123 | 124 | assert_eq!( 125 | serde_json::to_string(&metrics_test).unwrap(), 126 | r#"{"_aws":{"Timestamp":1687394207903,"CloudWatchMetrics":[{"Namespace":"GameServerMetrics","Dimensions":[["Address","Port"]],"Metrics":[{"Name":"FrameTime","Unit":"Milliseconds"},{"Name":"CpuUsage","Unit":"Percent"},{"Name":"MemoryUsage","Unit":"Kilobytes"}]}]},"Address":"10.172.207.225","Port":"7779","CpuUsage":5.5,"FrameTime":10.0,"MemoryUsage":10240}"# 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/lambda.rs: -------------------------------------------------------------------------------- 1 | //! Additional functionality for integration with [lambda_runtime] and [lambda_http] 2 | //! 3 | //! Inspired by Lambda Power Tools 4 | //! 5 | //! *this module requires the `lambda` feature flag* 6 | //! 7 | //! # Simple Example 8 | //! 9 | //! ```no_run 10 | //! use lambda_runtime::{Error, LambdaEvent}; 11 | //! // This replaces lambda_runtime::run and lambda_runtime::service_fn 12 | //! use metrics_cloudwatch_embedded::lambda::handler::run; 13 | //! use serde::{Deserialize, Serialize}; 14 | //! use tracing::{info, info_span}; 15 | //! 16 | //! #[derive(Deserialize)] 17 | //! struct Request {} 18 | //! 19 | //! #[derive(Serialize)] 20 | //! struct Response {} 21 | //! 22 | //! async fn function_handler(event: LambdaEvent) -> Result { 23 | //! 24 | //! // Do something important 25 | //! 26 | //! info!("Hello from function_handler"); 27 | //! 28 | //! metrics::counter!("requests", "Method" => "Default").increment(1); 29 | //! 30 | //! Ok(Response {}) 31 | //! } 32 | //! 33 | //! #[tokio::main] 34 | //! async fn main() -> Result<(), Error> { 35 | //! tracing_subscriber::fmt() 36 | //! .json() 37 | //! .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env()) 38 | //! .with_target(false) 39 | //! .with_current_span(false) 40 | //! .without_time() 41 | //! .init(); 42 | //! 43 | //! let metrics = metrics_cloudwatch_embedded::Builder::new() 44 | //! .cloudwatch_namespace("MetricsExample") 45 | //! .with_dimension("function", std::env::var("AWS_LAMBDA_FUNCTION_NAME").unwrap()) 46 | //! .lambda_cold_start_span(info_span!("cold start")) 47 | //! .lambda_cold_start_metric("ColdStart") 48 | //! .with_lambda_request_id("RequestId") 49 | //! .init() 50 | //! .unwrap(); 51 | //! 52 | //! run(metrics, function_handler).await 53 | //! } 54 | //! ``` 55 | //! 56 | //! # Output 57 | //! 58 | //! ```plaintext 59 | //! INIT_START Runtime Version: provided:al2.v19 Runtime Version ARN: arn:aws:lambda:us-west-2::runtime:d1007133cb0d993d9a42f9fc10442cede0efec65d732c7943b51ebb979b8f3f8 60 | //! {"level":"INFO","fields":{"message":"Hello from main"},"spans":[{"name":"cold start"}]} 61 | //! START RequestId: fce53486-160d-41e8-b8c3-8ef0fd0f4051 Version: $LATEST 62 | //! {"_aws":{"Timestamp":1688294472338,"CloudWatchMetrics":[{"Namespace":"MetricsTest","Dimensions":[["Function"]],"Metrics":[{"Name":"ColdStart","Unit":"Count"}]}]},"Function":"MetricsTest","RequestId":"fce53486-160d-41e8-b8c3-8ef0fd0f4051","ColdStart":1} 63 | //! {"level":"INFO","fields":{"message":"Hello from function_handler"},"spans":[{"name":"cold start"},{"requestId":"fce53486-160d-41e8-b8c3-8ef0fd0f4051","xrayTraceId":"Root=1-64a15448-4aa914a00d66aa066325d7e3;Parent=60a7d0c22fb2f001;Sampled=0;Lineage=16f3a795:0","name":"Lambda runtime invoke"}]} 64 | //! {"_aws":{"Timestamp":1688294472338,"CloudWatchMetrics":[{"Namespace":"MetricsTest","Dimensions":[["Function","Method"]],"Metrics":[{"Name":"requests"}]}]},"Function":"MetricsTest","Method":"Default","RequestId":"fce53486-160d-41e8-b8c3-8ef0fd0f4051","requests":1} 65 | //! END RequestId: fce53486-160d-41e8-b8c3-8ef0fd0f4051 66 | //! REPORT RequestId: fce53486-160d-41e8-b8c3-8ef0fd0f4051 Duration: 1.22 ms Billed Duration: 11 ms Memory Size: 128 MB Max Memory Used: 13 MB Init Duration: 8.99 ms 67 | //! ``` 68 | //! 69 | //! # Advanced Usage 70 | //! 71 | //! If you're building a more sophisticated [tower] stack, use [MetricsService] instead 72 | //! 73 | 74 | #![allow(dead_code)] 75 | use super::collector::Collector; 76 | use lambda_runtime::{LambdaEvent, LambdaInvocation}; 77 | use pin_project::pin_project; 78 | use std::future::Future; 79 | use std::pin::Pin; 80 | use std::task::{Context, Poll}; 81 | use tower::Layer; 82 | 83 | /// [tower::Layer] for automatically [flushing](super::Collector::flush()) after each request and enabling 84 | /// `lambda` features in [Builder](super::Builder) 85 | /// 86 | /// For composing your own [tower] stacks to input into the Rust Lambda Runtime 87 | pub struct MetricsLayer { 88 | pub(crate) collector: &'static Collector, 89 | } 90 | 91 | impl MetricsLayer { 92 | pub fn new(collector: &'static Collector) -> Self { 93 | Self { collector } 94 | } 95 | } 96 | 97 | impl Layer for MetricsLayer { 98 | type Service = MetricsService; 99 | 100 | fn layer(&self, inner: S) -> Self::Service { 101 | MetricsService { 102 | metrics: self.collector, 103 | inner, 104 | } 105 | } 106 | } 107 | 108 | /// [tower::Service] for automatically [flushing](super::Collector::flush()) after each request and enabling 109 | /// `lambda` features in [Builder](super::Builder) 110 | /// 111 | /// For composing your own [tower] stacks to input into the Rust Lambda Runtime 112 | pub struct MetricsService { 113 | metrics: &'static Collector, 114 | inner: S, 115 | } 116 | 117 | impl MetricsService { 118 | /// Constructs a new [MetricsService] with the given [Collector] and inner [`tower::Service>`] 119 | /// to wrap 120 | pub fn new(metrics: &'static Collector, inner: S) -> MetricsService 121 | where 122 | S: tower::Service>, 123 | { 124 | Self { metrics, inner } 125 | } 126 | } 127 | 128 | impl tower::Service for MetricsService 129 | where 130 | S: tower::Service, 131 | { 132 | type Response = S::Response; 133 | type Error = S::Error; 134 | type Future = MetricsServiceFuture; 135 | 136 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 137 | self.inner.poll_ready(cx) 138 | } 139 | 140 | fn call(&mut self, req: LambdaInvocation) -> Self::Future { 141 | if let Some(prop_name) = self.metrics.config.lambda_request_id { 142 | self.metrics.set_property(prop_name, req.context.request_id.clone()); 143 | } 144 | if let Some(prop_name) = self.metrics.config.lambda_xray_trace_id { 145 | self.metrics.set_property(prop_name, req.context.xray_trace_id.clone()); 146 | } 147 | 148 | let mut cold_start_span = None; 149 | if let Some(counter_name) = self.metrics.config.lambda_cold_start { 150 | static COLD_START_BEGIN: std::sync::Once = std::sync::Once::new(); 151 | COLD_START_BEGIN.call_once(|| { 152 | cold_start_span = self.metrics.take_cold_start_span().map(|span| span.entered()); 153 | self.metrics 154 | .write_single(counter_name, Some(metrics::Unit::Count), 1, std::io::stdout()) 155 | .expect("failed to flush cold start metric"); 156 | }); 157 | } 158 | 159 | // Wrap the inner Future so we can flush after it's done 160 | MetricsServiceFuture { 161 | metrics: self.metrics, 162 | inner: self.inner.call(req), 163 | cold_start_span, 164 | } 165 | } 166 | } 167 | 168 | #[pin_project] 169 | #[doc(hidden)] 170 | pub struct MetricsServiceFuture { 171 | #[pin] 172 | metrics: &'static Collector, 173 | #[pin] 174 | inner: F, 175 | cold_start_span: Option, 176 | } 177 | 178 | impl Future for MetricsServiceFuture 179 | where 180 | F: Future>, 181 | Error: Into, 182 | { 183 | type Output = Result; 184 | 185 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 186 | let this = self.project(); 187 | 188 | if let Poll::Ready(result) = this.inner.poll(cx) { 189 | // Flush our metrics after the inner service is finished 190 | this.metrics.flush(std::io::stdout()).expect("failed to flush metrics"); 191 | 192 | static COLD_START_END: std::sync::Once = std::sync::Once::new(); 193 | COLD_START_END.call_once(|| { 194 | let _span = this.cold_start_span.take(); 195 | }); 196 | 197 | return Poll::Ready(result); 198 | } 199 | 200 | Poll::Pending 201 | } 202 | } 203 | 204 | /// Helpers for starting the Lambda Rust runtime with a [tower::Service] with a 205 | /// [TracingLayer] and a [MetricsLayer] 206 | /// 207 | /// Reduces the amount of ceremony needed in `main()` for simple use cases 208 | /// 209 | pub mod service { 210 | 211 | use core::fmt::Debug; 212 | 213 | use futures::Stream; 214 | use lambda_runtime::{layers::TracingLayer, Diagnostic, IntoFunctionResponse}; 215 | use serde::{Deserialize, Serialize}; 216 | use tower::Service; 217 | 218 | use super::*; 219 | 220 | /// Start the Lambda Rust runtime with a given [`tower::Service>`] 221 | /// which is then layered with [TracingLayer] and [MetricsLayer] with a given [Collector] 222 | pub async fn run(metrics: &'static Collector, handler: F) -> Result<(), lambda_runtime::Error> 223 | where 224 | F: Service, Response = R>, 225 | F::Future: Future>, 226 | F::Error: Into + std::fmt::Debug, 227 | A: for<'de> Deserialize<'de>, 228 | R: IntoFunctionResponse, 229 | B: Serialize, 230 | S: Stream> + Unpin + Send + 'static, 231 | D: Into + Send, 232 | E: Into + Send + Debug, 233 | { 234 | let runtime = lambda_runtime::Runtime::new(handler) 235 | .layer(TracingLayer::new()) 236 | .layer(MetricsLayer::new(metrics)); 237 | runtime.run().await 238 | } 239 | 240 | /// Start the Lambda Rust runtime with a given [tower::Service] 241 | /// which is then layered with [TracingLayer] and [MetricsLayer] with a given [Collector] 242 | pub async fn run_http<'a, R, S, E>(metrics: &'static Collector, handler: S) -> Result<(), lambda_runtime::Error> 243 | where 244 | S: Service, 245 | S::Future: Send + 'a, 246 | R: lambda_http::IntoResponse, 247 | E: std::fmt::Debug + Into, 248 | { 249 | run(metrics, lambda_http::Adapter::from(handler)).await 250 | } 251 | } 252 | 253 | /// Helpers for starting the Lambda Rust runtime with a handler function and 254 | /// a [lambda_runtime::layers::TracingLayer] and a [MetricsLayer] 255 | /// 256 | /// Reduces the amount of ceremony needed in `main()` for simple use cases 257 | /// 258 | pub mod handler { 259 | 260 | use lambda_http::service_fn; 261 | 262 | use super::*; 263 | 264 | /// Start the Lambda Rust runtime with a given [LambdaEvent] handler function 265 | /// which is then layered with [lambda_runtime::layers::TracingLayer] and [MetricsLayer] with a given [Collector] 266 | pub async fn run( 267 | metrics: &'static Collector, 268 | handler: T, 269 | ) -> Result<(), lambda_runtime::Error> 270 | where 271 | T: FnMut(LambdaEvent) -> F, 272 | F: Future>, 273 | Request: for<'de> serde::Deserialize<'de>, 274 | Response: serde::Serialize, 275 | { 276 | super::service::run(metrics, lambda_runtime::service_fn(handler)).await 277 | } 278 | 279 | /// Start the Lambda Rust runtime with a given [lambda_http::Request] handler function 280 | /// which is then layered with [lambda_runtime::layers::TracingLayer] and [MetricsLayer] with a given [Collector] 281 | pub async fn run_http<'a, T, F, Response>( 282 | metrics: &'static Collector, 283 | handler: T, 284 | ) -> Result<(), lambda_runtime::Error> 285 | where 286 | T: FnMut(lambda_http::Request) -> F, 287 | F: Future> + Send + 'a, 288 | Response: lambda_http::IntoResponse, 289 | { 290 | super::service::run(metrics, lambda_http::Adapter::from(service_fn(handler))).await 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Backend for the [metrics] crate to emit metrics in the CloudWatch Embedded Metrics Format 2 | //! () 3 | //! 4 | //! Counters, Gauges and Histograms are supported. 5 | //! 6 | //! # Example 7 | //! ``` 8 | //! let metrics = metrics_cloudwatch_embedded::Builder::new() 9 | //! .cloudwatch_namespace("MyApplication") 10 | //! .init() 11 | //! .unwrap(); 12 | //! 13 | //! metrics::counter!("requests", "Method" => "Default").increment(1); 14 | //! 15 | //! metrics 16 | //! .set_property("RequestId", "ABC123") 17 | //! .flush(std::io::stdout()); 18 | //! ``` 19 | //! 20 | //! # Implementation Details 21 | //! 22 | //! Intended for use with the [lambda_runtime], however [Collector::flush(...)](collector::Collector::flush) 23 | //! could be used for anything that writes logs that end up in CloudWatch. 24 | //! 25 | //! * Counters are Guages are implented as [AtomicU64](std::sync::atomic::AtomicU64) via the 26 | //! [CounterFn](metrics::CounterFn) and [GaugeFn](metrics::GaugeFn) implementations in the [metrics crate](metrics) 27 | //! * Histograms are implemented as [mpsc::SyncSender](std::sync::mpsc::SyncSender) 28 | //! * [serde_json] is used to serialize metric documents to simplify maintainence and for consistancy with other 29 | //! crates in the ecosystem 30 | //! * Registering and flushing of metrics uses state within a [Mutex](std::sync::Mutex), recording previously 31 | //! registered metrics should not block on this [Mutex](std::sync::Mutex) 32 | //! * Metric names are mapped to [metrics::Unit] regardless of their type and [labels](metrics::Label) 33 | //! * Metric descriptions are unused 34 | //! 35 | //! # Limitations 36 | //! * Histograms retain up to 100 values (the maximum for a single metric document) between calls to 37 | //! [Collector::flush()](collector::Collector::flush), overflow will report an error via the [tracing] crate 38 | //! * Dimensions set at initialization via [Builder::with_dimension(...)][builder::Builder::with_dimension] 39 | //! may not overlap with metric [labels](metrics::Label) 40 | //! * Only the subset of metric units in [metrics::Unit] are supported 41 | //! 42 | //! * Registering different metric types with the same [metrics::Key] will fail with an error via the [tracing] crate 43 | //! * The Embedded Metric Format supports a maximum of 30 dimensions per metric, attempting to register a metric with 44 | //! more than 30 dimensions/labels will fail with an error via the [tracing] crate 45 | //! 46 | 47 | pub use {builder::Builder, collector::Collector}; 48 | 49 | #[doc(hidden)] 50 | pub type Error = Box; 51 | 52 | mod builder; 53 | mod collector; 54 | mod emf; 55 | #[cfg(feature = "lambda")] 56 | pub mod lambda; 57 | #[cfg(test)] 58 | mod test; 59 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | 3 | #[cfg(test)] 4 | mod tests { 5 | use super::*; 6 | 7 | // Because the metrics registar is a singleton, we need to run tests in forked processes 8 | use rusty_fork::rusty_fork_test; 9 | 10 | rusty_fork_test! { 11 | #[test] 12 | fn simple_test() { 13 | let port = format!("{}", 7779); 14 | let metrics = builder::Builder::new() 15 | .cloudwatch_namespace("namespace") 16 | .with_dimension("Address", "10.172.207.225") 17 | .with_dimension("Port", port) 18 | .with_timestamp(1687657545423) 19 | .init() 20 | .unwrap(); 21 | 22 | metrics::describe_counter!("success", metrics::Unit::Count, ""); 23 | metrics::describe_histogram!("runtime", metrics::Unit::Milliseconds, ""); 24 | 25 | metrics::counter!("success", "module" => "directory", "api" => "a_function").increment(1); 26 | metrics::counter!("not_found", "module" => "directory", "api" => "a_function").increment(1); 27 | metrics::describe_counter!("not_found", metrics::Unit::Count, ""); 28 | metrics::counter!("success", "module" => "directory", "api" => "b_function").increment(1); 29 | metrics::counter!("success", "module" => "directory", "api" => "a_function").increment(1); 30 | metrics::gauge!("thing", "module" => "directory", "api" => "a_function").set(3.15); 31 | metrics::histogram!("runtime", "module" => "directory", "api" => "a_function").record(4.0); 32 | metrics::gauge!("thing", "module" => "directory", "api" => "a_function").set(7.11); 33 | metrics::histogram!("runtime", "module" => "directory", "api" => "a_function").record(5.0); 34 | 35 | let mut output = Vec::new(); 36 | metrics.flush(&mut output).unwrap(); 37 | let output_str = std::str::from_utf8(&output).unwrap(); 38 | assert_eq!( 39 | output_str, 40 | r#"{"_aws":{"Timestamp":1687657545423,"CloudWatchMetrics":[{"Namespace":"namespace","Dimensions":[["Address","Port","module","api"]],"Metrics":[{"Name":"not_found","Unit":"Count"},{"Name":"runtime","Unit":"Milliseconds"},{"Name":"success","Unit":"Count"},{"Name":"thing"}]}]},"Address":"10.172.207.225","Port":"7779","api":"a_function","module":"directory","not_found":1,"runtime":[4.0,5.0],"success":2,"thing":7.11} 41 | {"_aws":{"Timestamp":1687657545423,"CloudWatchMetrics":[{"Namespace":"namespace","Dimensions":[["Address","Port","module","api"]],"Metrics":[{"Name":"success","Unit":"Count"}]}]},"Address":"10.172.207.225","Port":"7779","api":"b_function","module":"directory","success":1} 42 | "# 43 | ); 44 | } 45 | } 46 | 47 | rusty_fork_test! { 48 | 49 | #[test] 50 | fn no_emit_zero() { 51 | let port = format!("{}", 7779); 52 | let metrics = builder::Builder::new() 53 | .cloudwatch_namespace("namespace") 54 | .with_dimension("Address", "10.172.207.225") 55 | .with_dimension("Port", port) 56 | .with_timestamp(1687657545423) 57 | .init() 58 | .unwrap(); 59 | 60 | metrics::describe_counter!("success", metrics::Unit::Count, ""); 61 | metrics::describe_histogram!("runtime", metrics::Unit::Milliseconds, ""); 62 | 63 | metrics::counter!("success", "module" => "directory", "api" => "a_function").increment(1); 64 | metrics::counter!("not_found", "module" => "directory", "api" => "a_function").increment(1); 65 | metrics::describe_counter!("not_found", metrics::Unit::Count, ""); 66 | metrics::counter!("success", "module" => "directory", "api" => "b_function").increment(1); 67 | metrics::counter!("success", "module" => "directory", "api" => "a_function").increment(1); 68 | metrics::gauge!("thing", "module" => "directory", "api" => "a_function").set(3.15); 69 | metrics::histogram!("runtime", "module" => "directory", "api" => "a_function").record(4.0); 70 | metrics::gauge!("thing", "module" => "directory", "api" => "a_function").set(7.11); 71 | metrics::histogram!("runtime", "module" => "directory", "api" => "a_function").record(5.0); 72 | 73 | let mut output = Vec::new(); 74 | metrics.flush(&mut output).unwrap(); 75 | let output_str = std::str::from_utf8(&output).unwrap(); 76 | assert_eq!( 77 | output_str, 78 | r#"{"_aws":{"Timestamp":1687657545423,"CloudWatchMetrics":[{"Namespace":"namespace","Dimensions":[["Address","Port","module","api"]],"Metrics":[{"Name":"not_found","Unit":"Count"},{"Name":"runtime","Unit":"Milliseconds"},{"Name":"success","Unit":"Count"},{"Name":"thing"}]}]},"Address":"10.172.207.225","Port":"7779","api":"a_function","module":"directory","not_found":1,"runtime":[4.0,5.0],"success":2,"thing":7.11} 79 | {"_aws":{"Timestamp":1687657545423,"CloudWatchMetrics":[{"Namespace":"namespace","Dimensions":[["Address","Port","module","api"]],"Metrics":[{"Name":"success","Unit":"Count"}]}]},"Address":"10.172.207.225","Port":"7779","api":"b_function","module":"directory","success":1} 80 | "# 81 | ); 82 | 83 | // Update a single metric and confirm only that metric is emitted 84 | metrics::counter!("success", "module" => "directory", "api" => "a_function").increment(1); 85 | 86 | let mut output = Vec::new(); 87 | metrics.flush(&mut output).unwrap(); 88 | let output_str = std::str::from_utf8(&output).unwrap(); 89 | assert_eq!( 90 | output_str, 91 | r#"{"_aws":{"Timestamp":1687657545423,"CloudWatchMetrics":[{"Namespace":"namespace","Dimensions":[["Address","Port","module","api"]],"Metrics":[{"Name":"success","Unit":"Count"},{"Name":"thing"}]}]},"Address":"10.172.207.225","Port":"7779","api":"a_function","module":"directory","success":1,"thing":7.11} 92 | "# 93 | ); 94 | } 95 | } 96 | 97 | rusty_fork_test! { 98 | 99 | #[test] 100 | fn emit_zero() { 101 | let port = format!("{}", 7779); 102 | let metrics = builder::Builder::new() 103 | .cloudwatch_namespace("namespace") 104 | .with_dimension("Address", "10.172.207.225") 105 | .with_dimension("Port", port) 106 | .with_timestamp(1687657545423) 107 | .emit_zeros(true) 108 | .init() 109 | .unwrap(); 110 | 111 | metrics::describe_counter!("success", metrics::Unit::Count, ""); 112 | metrics::describe_histogram!("runtime", metrics::Unit::Milliseconds, ""); 113 | 114 | metrics::counter!("success", "module" => "directory", "api" => "a_function").increment(1); 115 | metrics::counter!("not_found", "module" => "directory", "api" => "a_function").increment(1); 116 | metrics::describe_counter!("not_found", metrics::Unit::Count, ""); 117 | metrics::counter!("success", "module" => "directory", "api" => "b_function").increment(1); 118 | metrics::counter!("success", "module" => "directory", "api" => "a_function").increment(1); 119 | metrics::gauge!("thing", "module" => "directory", "api" => "a_function").set(3.15); 120 | metrics::histogram!("runtime", "module" => "directory", "api" => "a_function").record(4.0); 121 | metrics::gauge!("thing", "module" => "directory", "api" => "a_function").set(7.11); 122 | metrics::histogram!("runtime", "module" => "directory", "api" => "a_function").record(5.0); 123 | 124 | let mut output = Vec::new(); 125 | metrics.flush(&mut output).unwrap(); 126 | let output_str = std::str::from_utf8(&output).unwrap(); 127 | assert_eq!( 128 | output_str, 129 | r#"{"_aws":{"Timestamp":1687657545423,"CloudWatchMetrics":[{"Namespace":"namespace","Dimensions":[["Address","Port","module","api"]],"Metrics":[{"Name":"not_found","Unit":"Count"},{"Name":"runtime","Unit":"Milliseconds"},{"Name":"success","Unit":"Count"},{"Name":"thing"}]}]},"Address":"10.172.207.225","Port":"7779","api":"a_function","module":"directory","not_found":1,"runtime":[4.0,5.0],"success":2,"thing":7.11} 130 | {"_aws":{"Timestamp":1687657545423,"CloudWatchMetrics":[{"Namespace":"namespace","Dimensions":[["Address","Port","module","api"]],"Metrics":[{"Name":"success","Unit":"Count"}]}]},"Address":"10.172.207.225","Port":"7779","api":"b_function","module":"directory","success":1} 131 | "# 132 | ); 133 | 134 | // Update a single metric and confirm only that metric is emitted 135 | metrics::counter!("success", "module" => "directory", "api" => "a_function").increment(1); 136 | 137 | let mut output = Vec::new(); 138 | metrics.flush(&mut output).unwrap(); 139 | let output_str = std::str::from_utf8(&output).unwrap(); 140 | 141 | assert_eq!( 142 | output_str, 143 | r#"{"_aws":{"Timestamp":1687657545423,"CloudWatchMetrics":[{"Namespace":"namespace","Dimensions":[["Address","Port","module","api"]],"Metrics":[{"Name":"not_found","Unit":"Count"},{"Name":"success","Unit":"Count"},{"Name":"thing"}]}]},"Address":"10.172.207.225","Port":"7779","api":"a_function","module":"directory","not_found":0,"success":1,"thing":7.11} 144 | {"_aws":{"Timestamp":1687657545423,"CloudWatchMetrics":[{"Namespace":"namespace","Dimensions":[["Address","Port","module","api"]],"Metrics":[{"Name":"success","Unit":"Count"}]}]},"Address":"10.172.207.225","Port":"7779","api":"b_function","module":"directory","success":0} 145 | "# 146 | ); 147 | } 148 | } 149 | } 150 | --------------------------------------------------------------------------------