├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── ------help.md │ ├── ---bug-report.md │ └── ---feature-request.md ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── daily.yml │ ├── java_build.yml │ ├── on_merge.yml │ └── on_push.yml ├── .gitignore ├── .mvn ├── extensions.xml ├── settings.xml └── wrapper │ └── maven-wrapper.properties ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── context │ └── README.md ├── events │ └── README.md ├── info │ ├── concept │ │ └── README.md │ └── errorhandling │ │ └── README.md ├── integrations │ └── README.md ├── registers │ └── README.md ├── schedulers │ └── README.md └── services │ ├── README.md │ ├── httpclient │ └── README.md │ ├── httpserver │ └── README.md │ ├── logger │ └── README.md │ └── metricservice │ └── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── org │ └── nanonative │ └── nano │ ├── core │ ├── Nano.java │ ├── NanoBase.java │ ├── NanoServices.java │ ├── NanoThreads.java │ └── model │ │ ├── Context.java │ │ ├── NanoThread.java │ │ ├── Scheduler.java │ │ ├── Service.java │ │ └── Unhandled.java │ ├── helper │ ├── ExRunnable.java │ ├── LockedBoolean.java │ ├── NanoUtils.java │ ├── config │ │ └── ConfigRegister.java │ └── event │ │ ├── EventChannelRegister.java │ │ └── model │ │ └── Event.java │ └── services │ ├── http │ ├── HttpClient.java │ ├── HttpServer.java │ └── model │ │ ├── ContentType.java │ │ ├── HttpHeaders.java │ │ ├── HttpMethod.java │ │ └── HttpObject.java │ ├── logging │ ├── LogFormatRegister.java │ ├── LogFormatterConsole.java │ ├── LogFormatterJson.java │ ├── LogService.java │ └── model │ │ └── LogLevel.java │ └── metric │ ├── logic │ └── MetricService.java │ └── model │ ├── MetricCache.java │ ├── MetricType.java │ └── MetricUpdate.java └── test ├── java └── org │ └── nanonative │ └── nano │ ├── core │ ├── NanoTest.java │ ├── config │ │ └── TestConfig.java │ └── model │ │ ├── ConfigTest.java │ │ ├── ContextTest.java │ │ ├── LockedBooleanTest.java │ │ ├── NanoThreadTest.java │ │ ├── SchedulerTest.java │ │ ├── ServiceTest.java │ │ └── UnhandledTest.java │ ├── examples │ ├── ErrorHandling.java │ ├── HttpReceive.java │ ├── HttpSend.java │ ├── Kazim.java │ ├── MetricCreation.java │ └── Yuna.java │ ├── model │ ├── EventChannelRegisterTest.java │ ├── HttpObjectTest.java │ └── TestService.java │ └── services │ ├── http │ └── logic │ │ └── HttpClientTest.java │ └── metric │ ├── logic │ └── MetricServiceTest.java │ └── model │ └── MetricCacheTest.java └── resources ├── Nano.png ├── application-local.properties ├── application.properties ├── junit-platform.properties └── tiny_java.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ NanoNative ] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ["https://www.paypal.com/donate/?hosted_button_id=HFHFUT3G6TZF6"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/------help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F468‍\U0001F4BB Help" 3 | about: Help needed 4 | title: '' 5 | labels: help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What do you want to achieve?** 11 | A clear and concise description of what you want to achieve. 12 | 13 | **What did you try already?** 14 | A clear and concise description of what did you already try out. 15 | 16 | **Operating System** 17 | > N/A 18 | 19 | **Project Version** 20 | > N/A 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What happened?** 11 | A clear and concise description of what the bug is. 12 | 13 | **How can we reproduce the issue?** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Relevant log output** 21 | > N/A 22 | 23 | **Operating System** 24 | > N/A 25 | 26 | **Project Version** 27 | > N/A 28 | 29 | **Expected behavior** 30 | A clear and concise description of what you expected to happen. 31 | 32 | **Screenshots** 33 | If applicable, add screenshots to help explain your problem. 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/---feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Operating System** 20 | > N/A 21 | 22 | **Project Version** 23 | > N/A 24 | 25 | **Additional context** 26 | Add any other context or screenshots about the feature request here. 27 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Closes #000 2 | 3 | ### Types of changes 4 | 5 | - [ ] Bug fix (non-breaking change which fixes an issue) 6 | - [ ] New feature (non-breaking change which adds functionality) 7 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 8 | 9 | ## Motivation 10 | > Why is this change required? What problem does it solve? 11 | 12 | ## Changes 13 | > Behaviour, Functionality, Screenshots, etc. 14 | 15 | ## Success Check 16 | > How can we see or measure the change? 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | run_update: 7 | type: boolean 8 | default: false 9 | required: false 10 | description: "Tries to update the repository like maven/gradle wrapper and maven properties" 11 | run_test: 12 | type: boolean 13 | default: true 14 | required: false 15 | description: "Runs maven/gradle tests" 16 | run_deploy: 17 | type: choice 18 | required: false 19 | default: "disabled" 20 | description: "version increment (Main branch with changes only) [major, minor, patch, rc, disabled]" 21 | options: 22 | - "disabled" 23 | - "major" 24 | - "minor" 25 | - "patch" 26 | - "rc" 27 | ref: 28 | type: string 29 | required: false 30 | description: "[ref] e.g. branch, tag or commit to checkout [default: github_ref_name || github_head_ref ]" 31 | 32 | jobs: 33 | builld: 34 | uses: ./.github/workflows/java_build.yml 35 | with: 36 | ref: ${{ github.event.inputs.ref || github.ref || github.ref_name || github.head_ref }} 37 | run_update: ${{ inputs.run_update }} 38 | run_test: ${{ inputs.run_test }} 39 | run_deploy: ${{ inputs.run_deploy }} 40 | secrets: inherit 41 | -------------------------------------------------------------------------------- /.github/workflows/daily.yml: -------------------------------------------------------------------------------- 1 | name: "Daily" 2 | 3 | on: 4 | schedule: 5 | - cron: '0 7 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | builld: 10 | uses: ./.github/workflows/java_build.yml 11 | with: 12 | run_update: true 13 | run_test: true 14 | run_deploy: patch 15 | secrets: inherit 16 | -------------------------------------------------------------------------------- /.github/workflows/on_merge.yml: -------------------------------------------------------------------------------- 1 | name: "On Merge" 2 | 3 | on: 4 | pull_request_target: 5 | types: [ closed ] 6 | branches: 7 | - main 8 | - master 9 | - default 10 | paths-ignore: 11 | - '*.md' 12 | - '*.cmd' 13 | - '*.bat' 14 | - '*.sh' 15 | - 'FOUNDING.yml' 16 | - 'FOUNDING.yaml' 17 | - '.editorconfig' 18 | - '.gitignore' 19 | - 'docs/**' 20 | - '.github/**' 21 | - '.mvn/**' 22 | - '.gradle/**' 23 | jobs: 24 | builld: 25 | if: github.event.pull_request.merged == true 26 | uses: ./.github/workflows/java_build.yml 27 | with: 28 | ref: ${{ github.ref_name }} 29 | run_update: false # Updates only on main branches 30 | run_test: true # Always run tests 31 | run_deploy: disabled # Never deploy on non-main branches 32 | secrets: inherit 33 | -------------------------------------------------------------------------------- /.github/workflows/on_push.yml: -------------------------------------------------------------------------------- 1 | name: "On Push" 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - main 7 | - master 8 | - default 9 | 10 | jobs: 11 | builld: 12 | uses: ./.github/workflows/java_build.yml 13 | with: 14 | ref: ${{ github.ref_name }} 15 | run_update: false # Updates only on main branches 16 | run_test: true # Always run tests 17 | run_deploy: disabled # Never deploy on non-main branches 18 | secrets: inherit 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### STS ### 2 | .apt_generated 3 | .classpath 4 | .factorypath 5 | .project 6 | .settings 7 | .springBeans 8 | .sts4-cache 9 | /bin/ 10 | !**/src/main/**/bin/ 11 | !**/src/test/**/bin/ 12 | hs_err_pid*.log 13 | 14 | ### NetBeans ### 15 | /nbproject/private/ 16 | /nbbuild/ 17 | /nbdist/ 18 | /.nb-gradle/ 19 | /temp 20 | 21 | ### Editors ### 22 | .vscode/ 23 | coverage 24 | target/ 25 | .idea 26 | *.iws 27 | *.iml 28 | *.ipr 29 | out/ 30 | !**/src/main/**/out/ 31 | !**/src/test/**/out/ 32 | .gradle 33 | build/ 34 | !gradle/wrapper/gradle-wrapper.jar 35 | !**/src/main/**/build/ 36 | !**/src/test/**/build/ 37 | !**/cdk.out 38 | infrastructure/cdk.out 39 | functions/cdk.out 40 | common/cdk.out 41 | *.DS_Store 42 | .venv 43 | __pycache__ 44 | *.bat 45 | 46 | /cc-reporter* 47 | /public-key.asc 48 | 49 | lib/ 50 | node_modules 51 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | com.github.gzm55.maven 5 | project-settings-extension 6 | 0.1.1 7 | 8 | -------------------------------------------------------------------------------- /.mvn/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gpg 6 | 7 | true 8 | 9 | 10 | gpg 11 | ${GPG_SECRET} 12 | 13 | 14 | 15 | 16 | 17 | ossrh 18 | ${OSSH_USER} 19 | ${OSSH_PASS} 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 4 | 5 | ## Pull Request Checklist 6 | 1) Create your branch from the main branch 7 | 2) Use [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) 8 | 3) Increase the version by using [Semantic Versioning](https://semver.org) 9 | 4) Ensure your changes are covered by tests 10 | 5) Follow the rules of [Clean Code](https://gist.github.com/wojteklu/73c6914cc446146b8b533c0988cf8d29) while coding 11 | 6) No reflection is used in the code 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🧬 Project Nano 2 | 3 | [//]: # ([![Build][build_shield]][build_link]) 4 | 5 | [//]: # ([![Maintainable][maintainable_shield]][maintainable_link]) 6 | 7 | [//]: # ([![Coverage][coverage_shield]][coverage_link]) 8 | [![Issues][issues_shield]][issues_link] 9 | [![Commit][commit_shield]][commit_link] 10 | [![License][license_shield]][license_link] 11 | [![Central][central_shield]][central_link] 12 | [![Tag][tag_shield]][tag_link] 13 | [![Javadoc][javadoc_shield]][javadoc_link] 14 | [![Size][size_shield]][size_shield] 15 | ![Label][label_shield] 16 | ![Label][java_version] 17 | 18 | > [Introduction](#-introduction) 19 | > | [Core Concept](#-core-concept) 20 | > | [Mechanics](#-mechanics) 21 | > | [Components](#-components) 22 | > | [Getting Started](#-getting-started) 23 | > | [Build Nano](#-build-nano) 24 | > | [Benefits](#-benefits-of-nano) 25 | 26 | ## 🖼️ Introduction 27 | 28 | **Back to basics and forget about frameworks!** 29 | 30 | Nano is a lightweight concept which makes it easier for developer to write microservices in 31 | **functional, fluent, chaining, plain, modern java** with a nano footprint. 32 | Nano is also designed to be fully compilable with [GraalVM](https://www.graalvm.org) to create native executables. 33 | To enhance efficiency and performance, Nano utilizes non-blocking virtual threads from [Project Loom](https://jdk.java.net/loom/). 34 | 35 | ## 📐 Core Concept 36 | 37 | Nano handles threads for you and provides a basic construct for event driven architecture. 38 | It's providing a simple way to write microservices in a functional fluent and chaining style. 39 | **Objects are less needed** thanks to the underlying [TypeMap](https://github.com/YunaBraska/type-map). 40 | Nano provides full access to all internal components, resulting in very few private methods or fields. 41 | 42 | [Read more...](docs/info/concept/README.md) 43 | 44 | ## 📚 Components 45 | 46 | **All you need to know are few classes:** 47 | [Context](docs/context/README.md), 48 | [Events](docs/events/README.md), 49 | [Schedulers](docs/schedulers/README.md), 50 | [Services](docs/services/README.md) 51 | 52 | ```mermaid 53 | flowchart LR 54 | nano(((Nano))) --> context[Context] 55 | context --> events[Events] 56 | events --> services[Services] 57 | services --> schedulers[Schedulers] 58 | 59 | style nano fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 60 | style context fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 61 | style events fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 62 | style services fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 63 | style schedulers fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 64 | ``` 65 | 66 | ## ⚙️ Mechanics 67 | 68 | * [Error Handling](docs/info/errorhandling/README.md) 69 | * [Registers](docs/registers/README.md) _(ConfigRegister, TypeConversionRegister, LogFormatRegister, 70 | EventChannelRegister)_ 71 | * [Integrations](docs/integrations/README.md) _(🌱 Spring Boot, 🧑‍🚀 Micronaut, 🐸 Quarkus)_ 72 | * [Code Examples](src/test/java/berlin/yuna/nano/examples) 73 | 74 | ## 📚 Getting Started 75 | 76 | Maven example 77 | 78 | ```xml 79 | 80 | 81 | org.nanonative 82 | nano 83 | 1.0.0 84 | 85 | ``` 86 | 87 | Gradle example 88 | 89 | ```groovy 90 | dependencies { 91 | implementation 'org.nanonative:nano:1.0.0' 92 | } 93 | ``` 94 | 95 | Simple Nano example with [HttpServer](docs/services/httpserver/README.md) _(a default service)_ 96 | 97 | ```java 98 | public static void main(final String[] args) { 99 | // Start Nano with HttpServer 100 | final Nano app = new Nano(args, new HttpServer()); 101 | 102 | // listen to /hello 103 | app.subscribeEvent(EVENT_HTTP_REQUEST, event -> event.payloadOpt(HttpObject.class) 104 | .filter(HttpObject::isMethodGet) 105 | .filter(request -> request.pathMatch("/hello")) 106 | .ifPresent(request -> request.response().body(Map.of("Hello", System.getProperty("user.name"))).respond(event))); 107 | 108 | // Override error handling for HTTP requests 109 | app.subscribeEvent(EVENT_APP_UNHANDLED, event -> event.payloadOpt(HttpObject.class).ifPresent(request -> 110 | request.response().body("Internal Server Error [" + event.error().getMessage() + "]").statusCode(500).respond(event))); 111 | } 112 | ``` 113 | 114 | ## 🔨 Build Nano 115 | 116 | add the native-image profile to your `pom.xml` and run `mvn package -Pnative-image` 117 | 118 | ```xml 119 | 120 | 121 | 122 | 123 | org.graalvm.nativeimage 124 | native-image-maven-plugin 125 | 21.2.0 126 | 127 | ExampleApp 128 | de.yuna.berlin.nativeapp.helper.ExampleApp 129 | 130 | 131 | --no-fallback 132 | 133 | --no-server 134 | 135 | --initialize-at-build-time 136 | 137 | -H:IncludeResources=resources/config/.* 138 | 139 | 140 | 141 | 142 | 143 | native-image 144 | 145 | package 146 | 147 | 148 | 149 | 150 | ``` 151 | 152 | ## ✨ Benefits of Nano: 153 | 154 | * 🧩 **Modular Design**: Nano's architecture is modular, making it easy to understand, extend, and maintain. 155 | * 🧵 **Concurrency Management**: Efficiently handle asynchronous tasks using advanced thread management. 156 | * 📡 **Event-Driven Architecture**: Robust event handling that simplifies communication between different parts of your 157 | application. 158 | * ⚙️ **Flexible Configuration**: Configure your application using environment variables, system properties, or 159 | command-line 160 | arguments. 161 | * 📊 **Robust Logging and Error Handling**: Integrated logging and comprehensive error handling mechanisms for reliable 162 | operation. 163 | * 🚀 **Scalable and Performant**: Designed with scalability and performance in mind to handle high-concurrency scenarios. 164 | * 🪶 **Lightweight & Fast**: Starts in milliseconds, uses ~10MB memory. 165 | * 🌿 **Pure Java, Pure Simplicity**: No reflections, no regex, no unnecessary magic. 166 | * ⚡ **GraalVM Ready**: For ahead-of-time compilation and faster startup. 167 | * 🔒 **Minimal Dependencies**: Reduces CVE risks and simplifies updates. 168 | * 🌊 **Fluent & Stateless**: Intuitive API design for easy readability and maintenance. 169 | * 🛠️ **Rapid Service Development**: Build real services in minutes. 170 | 171 | ## 🤝 Contributing 172 | 173 | Contributions to Nano are welcome! Please refer to our [Contribution Guidelines](CONTRIBUTING.md) for more information. 174 | 175 | ## 📜 License 176 | 177 | Nano is open-source software licensed under the [Apache license](LICENSE). 178 | 179 | ## 🙋‍ Support 180 | 181 | If you encounter any issues or have questions, please file an 182 | issue [here](https://github.com/nanonative/nano/issues/new/choose). 183 | 184 | ## 🌐 Stay Connected 185 | 186 | * [GitHub](https://github.com/NanoNative) 187 | * [X (aka Twitter)](https://twitter.com/YunaMorgenstern) 188 | * [Mastodon](https://hachyderm.io/@LunaFreyja) 189 | * [LinkedIn](https://www.linkedin.com/in/yuna-morgenstern-6662a5145/) 190 | 191 | ![tiny_java_logo](src/test/resources/tiny_java.png) 192 | 193 | 194 | [build_shield]: https://github.com/nanonative/nano/workflows/MVN_RELEASE/badge.svg 195 | 196 | [build_link]: https://github.com/nanonative/nano/actions?query=workflow%3AMVN_RELEASE 197 | 198 | [maintainable_shield]: https://img.shields.io/codeclimate/maintainability/nanonative/nano?style=flat-square 199 | 200 | [maintainable_link]: https://codeclimate.com/github/nanonative/nano/maintainability 201 | 202 | [coverage_shield]: https://img.shields.io/codeclimate/coverage/nanonative/nano?style=flat-square 203 | 204 | [coverage_link]: https://codeclimate.com/github/nanonative/nano/test_coverage 205 | 206 | [issues_shield]: https://img.shields.io/github/issues/nanonative/nano?style=flat-square 207 | 208 | [issues_link]: https://github.com/nanonative/nano/issues/new/choose 209 | 210 | [commit_shield]: https://img.shields.io/github/last-commit/nanonative/nano?style=flat-square 211 | 212 | [commit_link]: https://github.com/nanonative/nano/commits/main 213 | 214 | [license_shield]: https://img.shields.io/github/license/nanonative/nano?style=flat-square 215 | 216 | [license_link]: https://github.com/nanonative/nano/blob/main/LICENSE 217 | 218 | [dependency_shield]: https://img.shields.io/librariesio/github/nanonative/nano?style=flat-square 219 | 220 | [dependency_link]: https://libraries.io/github/nanonative/nano 221 | 222 | [central_shield]: https://img.shields.io/maven-central/v/org.nanonative/nano?style=flat-square 223 | 224 | [central_link]:https://search.maven.org/artifact/org.nanonative/nano 225 | 226 | [tag_shield]: https://img.shields.io/github/v/tag/nanonative/nano?style=flat-square 227 | 228 | [tag_link]: https://github.com/nanonative/nano/releases 229 | 230 | [javadoc_shield]: https://javadoc.io/badge2/org.nanonative/nano/javadoc.svg?style=flat-square 231 | 232 | [javadoc_link]: https://javadoc.io/doc/org.nanonative/nano 233 | 234 | [size_shield]: https://img.shields.io/github/repo-size/nanonative/nano?style=flat-square 235 | 236 | [label_shield]: https://img.shields.io/badge/Yuna-QueenInside-blueviolet?style=flat-square 237 | 238 | [gitter_shield]: https://img.shields.io/gitter/room/nanonative/nano?style=flat-square 239 | 240 | [gitter_link]: https://gitter.im/nano/Lobby 241 | 242 | [java_version]: https://img.shields.io/badge/java-21-blueviolet?style=flat-square 243 | 244 | 245 | -------------------------------------------------------------------------------- /docs/events/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../README.md) / [Components](../../README.md#-components) 2 | 3 | [Context](../context/README.md) 4 | | [**> Events <**](README.md) 5 | | [Schedulers](../schedulers/README.md) 6 | | [Services](../services/README.md) 7 | 8 | # Events 9 | 10 | [Events](../events/README.md) are the backbone of communication within the Nano Framework, to decoupled interaction between different parts of 11 | an application. 12 | They are a core API concept for using [Services](../services/README.md). 13 | See [Event.java](../../src/main/java/org/nanonative/nano/helper/event/model/Event.java) 14 | 15 | ```mermaid 16 | flowchart LR 17 | eventRegistry(((Event Registry))) --> channelId[channelId] 18 | channelId --> sendEvent[sendEvent] 19 | sendEvent --> eventListeners[EventListeners] 20 | sendEvent --> services[Services] 21 | services --> eventResponse[Event Response] 22 | eventListeners --> eventResponse[Event Response] 23 | 24 | style eventRegistry fill:#E3F2FD,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 25 | style channelId fill:#E3F2FD,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 26 | style sendEvent fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 27 | style eventListeners fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 28 | style services fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 29 | style eventResponse fill:#E3F2FD,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 30 | ``` 31 | 32 | ## ChannelIds 33 | 34 | `ChannelIds` are globally, unique IDs to identify the right channel to send events into, 35 | They can be registered once with `ChannelIdRegister.registerChannelId("MY_EVENT_NAME");` - 36 | see [EventChanelRegister.java](../../src/main/java/org/nanonative/nano/helper/event/EventChannelRegister.java) 37 | and [DefaultEventChannel](../../src/main/java/org/nanonative/nano/helper/event/model/EventChannel.java) 38 | 39 | ## Sending Events 40 | 41 | [Events](../events/README.md) can be sent **synchronous**, **asynchronous**, **single cast** or **broadcast**. 42 | 43 | * synchronous (SingleCast) 44 | * `context.newEvent(channelId).payload(MyPayloadObject).send()` 45 | * asynchronous (SingleCast) 46 | * `context.newEvent(channelId).payload(MyPayloadObject).async(true).send()` 47 | * asynchronous with listener (SingleCast) 48 | * `context.newEvent(channelId).payload(MyPayloadObject).async(response -> myListener).send()` 49 | 50 | * synchronous (BroadCast) 51 | * `context.newEvent(channelId).payload(MyPayloadObject).broadcast(true).send()` 52 | * _broadcast will not stop at the first responding listener_ 53 | * asynchronous (BroadCast) 54 | * `context.newEvent(channelId).payload(MyPayloadObject).broadcast(true).async(true).send()` 55 | * _broadcast will not stop at the first responding listener_ 56 | * asynchronous with listener (SingleCast) 57 | * `context.newEvent(channelId).payload(MyPayloadObject).broadcast(true).async(true).send()` 58 | * _broadcast will not stop at the first responding listener_ 59 | 60 | # Listening to Events 61 | 62 | Listeners can be easily registered with `context.subscribeEvent(channelId, event -> System.out.println(event))`. 63 | [Services](../services/README.md) don't need to subscribe or unsubscribe to [Events](../events/README.md) as they are 64 | managed and receive the 65 | [Events](../events/README.md) through the build 66 | in method `onEvent` 67 | 68 | ## Default Events 69 | 70 | | In 🔲
Out 🔳 | [Event](../events/README.md) | Payload | Response | Description | 71 | |--------------------|----------------------------------|-------------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------| 72 | | 🔲 | `EVENT_APP_START` | `Nano` | `N/A` | Triggered when the Application is started | 73 | | 🔲 | `EVENT_APP_SHUTDOWN` | `null` | `N/A` | Triggered when the Application shuts down, can be also manually produced to shut down the Application | 74 | | 🔲 | `EVENT_APP_SERVICE_REGISTER` | `Service` | `N/A` | Triggered when a [Service](../services/README.md) is started | 75 | | 🔲 | `EVENT_APP_SERVICE_UNREGISTER` | `Service` | `N/A` | Triggered when a [Service](../services/README.md) is stopped | 76 | | 🔲 | `EVENT_APP_SCHEDULER_REGISTER` | `Scheduler` | `N/A` | Triggered when a [Scheduler](../schedulers/README.md) is started | 77 | | 🔲 | `EVENT_APP_SCHEDULER_UNREGISTER` | `Scheduler` | `N/A` | Triggered when a [Scheduler](../schedulers/README.md) is stopped | 78 | | 🔲 | `EVENT_APP_UNHANDLED` | `Unhandled`, `HttpObject`,... | `N/A` | Triggered when an unhandled error happened within the context | 79 | | 🔲 | `EVENT_APP_OOM` | `Double` | `N/A` | Triggered when the Application reached out of memory. When the event is not handled, the App will shutdown see config `app_oom_shutdown_threshold` | 80 | | 🔲 | `EVENT_APP_HEARTBEAT` | `Nano` | `N/A` | Send every 256ms | 81 | | 🔳 | `EVENT_CONFIG_CHANGE` | `TypeMap` | `N/A` | Used to change configs on the fly for services which supports it | 82 | 83 | -------------------------------------------------------------------------------- /docs/info/concept/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../../README.md) / **[Concept](README.md)** 2 | 3 | # Concept 4 | 5 | Nano is a minimalist standalone library designed to facilitate the creation of microservices using plain, modern Java. 6 | Nano is a tool, not a framework, and it emphasizes simplicity, security, and efficiency. 7 | 8 | ### Modern and Fluent Design 🚀 9 | 10 | Nano leverages fluent chaining and functional programming styles to create a syntax that resembles a stateless scripting 11 | language. By avoiding annotations and other “black magic,” Nano maintains transparency and simplicity in its codebase. 12 | Fluent and chaining means, there are no `get` and `set` prefixes and no `void` returns for methods. 13 | 14 | ### No External Dependencies 🔒 15 | 16 | Nano is built without any foreign dependencies, ensuring a lean, secure library free from common vulnerabilities and 17 | excessive dependencies. This results in a smaller, faster, and more secure codebase. You only need to trust and know the 18 | license agreements of Nano. 19 | 20 | ### Minimal Resource Consumption 🌱 21 | 22 | Nano is engineered for a minimal environmental footprint, utilizing fewer resources and making garbage collection more 23 | efficient due to its functional programming style. 24 | 25 | ### Non-Blocking Virtual Threads 🧵 26 | 27 | Nano utilizes non-blocking virtual threads from [Project Loom](https://jdk.java.net/loom/) to enhance efficiency and 28 | performance. These threads maximize CPU utilization without blocking the main thread, eliminating the need for manual 29 | thread limit settings. 30 | Note that Nano cannot control Java’s built-in `ForkJoinPool` used for `java.util.concurrent` objects like streams. 31 | To optimize performance, it is recommended to set the Java property to something like 32 | this `-Djava.util.concurrent.ForkJoinPool.common.parallelism=100.` in case of high parallelism. 33 | 34 | ### GraalVM Compatibility ⚡ 35 | 36 | Nano is fully compatible with [GraalVM](https://www.graalvm.org), allowing you to compile native executables that do not 37 | require a JVM to run. This feature is particularly useful in containerized and serverless environments. 38 | Nano avoids reflection and dynamic class loading, ensuring seamless [GraalVM](https://www.graalvm.org) integration 39 | without additional configuration. 40 | 41 | ### Extensible and Open 🪶 42 | 43 | All Nano functions and classes are `public` or `protected`, allowing developers to extend or modify the library as 44 | needed. This breaks the concept of immutable objects, but we think it's more important to be able to extend and modify 45 | Nano than closing it. Means, every developer is responsible for the own code! 46 | We still encourages contributions and improvements from the community. 47 | 48 | ### Modular Design 🧩 49 | 50 | Nano’s [Event](../../events/README.md) system enables decoupling of functions, plugin 51 | creation ([Services](../../services/README.md)), and function interception. 52 | For example, you can globally control and respond to every error that occurs, similar to a global `Controller Advice`. 53 | With that its also easy to change configurations on the fly. 54 | This modular design allows services, such as the built-in [HttpServer](../../services/httpserver/README.md) and 55 | [MetricService](../../services/metricservice/README.md), to operate independently while still being able to interact 56 | when started. 57 | 58 | ### Service-Based Architecture 📊 59 | 60 | ([Services](../../services/README.md)) in Nano function as plugins or extensions, executed only when explicitly added to 61 | Nano programmatically. 62 | This approach simplifies testing, as services and components can be tested independently without the need for mocking or 63 | stubbing. 64 | You execute only what you define, avoiding the pitfalls of auto-applying dependencies. 65 | 66 | ### Flexible Object Mapping 🔄 67 | 68 | Nano’s built-in `TypeConverter` eliminates the need for custom objects by enabling easy conversion of `JSON`, `XML`, and 69 | other simple Java objects. 70 | For example, HTTP requests can be converted to `TypeInfo`, `TypeMap` or `TypeList`, which lazily convert fields to 71 | the requested type. _See [TypeMap](https://github.com/YunaBraska/type-map) for more information._ 72 | If an object cannot be converted, it is straightforward to register a custom type conversion. 73 | These [TypeMaps](https://github.com/YunaBraska/type-map) and TypeLists are used extensively, such as in events and the context. 74 | 75 | ### Configuration Management ⚙️ 76 | 77 | Nano uses a [Context](../../context/README.md) object to manage logging, tracing and configurations. 78 | Nano reads property files and profiled properties which all end up in the [Context](../../context/README.md) Object. 79 | The properties can be converted to the required types as needed. 80 | This eliminates the need for custom configuration objects. 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /docs/info/errorhandling/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../../README.md) / **[ErrorHandling](README.md)** 2 | 3 | # Error Handling 4 | 5 | Error handling is pretty straight forward in Nano. 6 | All errors are [Events](../../events/README.md) which are logged automatically with the [LogService](../../services/logger/README.md) 7 | from the caller [Context](../../context/README.md). 8 | These [Events](../../events/README.md) are send to the `EVENT_APP_UNHANDLED` channel and can be caught or intercepted. 9 | 10 | ## Error Channel 11 | 12 | The channel `EVENT_APP_UNHANDLED` is used for all errors and also unhandled http events. 13 | _See [HttpServer](../../services/httpserver/README.md) for more information._ 14 | Therefore, its necessary to filter the right events to catch. Error events usually have a non nullable `error` property. 15 | 16 | ## Handle Error 17 | 18 | To handle an error, you can subscribe to the `EVENT_APP_UNHANDLED` channel and filter the events by the `error` 19 | property. 20 | `event.acknowledge()` is optional to stop further processing. _(Cough vs Intercept)_ 21 | 22 | ```java 23 | public static void main(final String[] args) { 24 | final Context context = new Nano(args).context(ErrorHandling.class); 25 | 26 | // Listen to exceptions 27 | context.subscribeEvent(EVENT_APP_UNHANDLED, event -> { 28 | // Print error message 29 | event.context().warn(() -> "Caught event [{}] with error [{}] ", event.nameOrg(), event.error().getMessage()); 30 | event.acknowledge(); // Set exception as handled (prevent further processing) 31 | }); 32 | 33 | // Throw an exception 34 | context.run(() -> { 35 | throw new RuntimeException("Test Exception"); 36 | }); 37 | } 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/integrations/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../README.md) / [**Integrations**](README.md) 2 | 3 | [Spring Boot](#-nano-in-spring-boot) 4 | | [Micronaut](#-nano-in-micronaut) 5 | | [Quarkus](#-nano-in-quarkus). 6 | 7 | # Integrations 8 | 9 | Nano is fully standalone and can be integrated into various frameworks and libraries. 10 | This section provides examples of how to integrate Nano into 11 | 12 | ## 🌱 Nano in Spring boot 13 | 14 | * Run Nano as Bean 15 | 16 | ```java 17 | 18 | @Configuration 19 | public class NanoConfiguration { 20 | 21 | @Bean 22 | public Nano nanoInstance() { 23 | // Initialize your Nano instance with the desired services 24 | return new Nano(); // Optionally add your services and configurations here 25 | } 26 | } 27 | ``` 28 | 29 | * Use Nano in a Service 30 | 31 | ```java 32 | 33 | @Service 34 | public class SomeService { 35 | 36 | private final Nano nano; 37 | 38 | @Autowired 39 | public SomeService(final Nano nano) { 40 | this.nano = nano; 41 | // Use Nano instance as needed 42 | } 43 | } 44 | ``` 45 | 46 | Nano has a graceful shutdown by itself, but it could be useful to trigger it from a Spring bean. 47 | 48 | * Graceful shutdown using `DisposableBean` 49 | 50 | ```java 51 | 52 | @Component 53 | public class NanoManager implements DisposableBean { 54 | 55 | private final Nano nano; 56 | 57 | public NanoManager(final Nano nano) { 58 | this.nano = nano; 59 | } 60 | 61 | @Override 62 | public void destroy() { 63 | nano.stop(); // Trigger Nano's shutdown process 64 | } 65 | } 66 | ``` 67 | 68 | * Graceful shutdown using `@PreDestroy` annotation 69 | 70 | ```java 71 | 72 | @Component 73 | public class NanoManager { 74 | 75 | private final Nano nano; 76 | 77 | public NanoManager(final Nano nano) { 78 | this.nano = nano; 79 | } 80 | 81 | @PreDestroy 82 | public void onDestroy() { 83 | nano.stop(); // Trigger Nano's shutdown process 84 | } 85 | } 86 | ``` 87 | 88 | ## 🧑‍🚀 Nano in Micronaut 89 | 90 | * Define the Nano Bean 91 | 92 | ```java 93 | 94 | @Factory 95 | public class NanoFactory { 96 | 97 | @Singleton 98 | public Nano nanoInstance() { 99 | // Initialize your Nano instance with desired services 100 | return new Nano(); // Optionally add services and configurations here 101 | } 102 | } 103 | ``` 104 | 105 | * Use Nano in Your Application 106 | 107 | ```java 108 | 109 | @Singleton 110 | public class SomeService { 111 | 112 | private final Nano nano; 113 | 114 | public SomeService(final Nano nano) { 115 | this.nano = nano; 116 | // Use the Nano instance as needed 117 | } 118 | } 119 | ``` 120 | 121 | * Graceful shutdown using `@ServerShutdownEvent` 122 | 123 | ```java 124 | 125 | @Singleton 126 | public class NanoManager implements ApplicationEventListener { 127 | 128 | private final Nano nano; 129 | 130 | public NanoManager(final Nano nano) { 131 | this.nano = nano; 132 | } 133 | 134 | @Override 135 | public void onApplicationEvent(final ServerShutdownEvent event) { 136 | nano.stop(); // Trigger Nano's shutdown process 137 | } 138 | } 139 | ``` 140 | 141 | ## 🐸 Nano in Quarkus 142 | 143 | * Define the Nano Producer 144 | 145 | ```java 146 | 147 | @ApplicationScoped 148 | public class NanoProducer { 149 | 150 | @Produces 151 | public Nano produceNano() { 152 | // Initialize your Nano instance with the desired services 153 | return new Nano(); // Optionally add your services and configurations here 154 | } 155 | } 156 | ``` 157 | -------------------------------------------------------------------------------- /docs/registers/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../README.md) / **[Registers](README.md)** 2 | 3 | # Registers 4 | 5 | Nano comes with a set of registers that are used to add custom functionality to internal components. 6 | It's recommended to use the register in `static` blocks to ensure that they are only executed on need. Like when the 7 | class is used. 8 | 9 | ### ConfigRegister 10 | 11 | The `ConfigRegister` is used to register custom configuration values. This register is non functional and mostly used 12 | for documentation purposes like the help menu. The config keys are usually separated by `_` and written in lowercase. 13 | This ensures a common naming convention which is compatible in environments like env variables and as parameters. 14 | 15 | **Usage:** 16 | 17 | ```java 18 | static { 19 | // Register a config key 20 | String key = ConfigRegister.registerConfig("my_config_key", "my description"); 21 | 22 | // Getting a config description 23 | String description = ConfigRegister.configDescriptionOf("my_config_key"); 24 | 25 | // Getting all configs 26 | Map allConfigs = CONFIG_KEYS; 27 | } 28 | ``` 29 | 30 | ### EventChannelRegister 31 | 32 | The `EventChannelRegister` is used to register custom [Event](../events/README.md) channels to send or 33 | subscribe [events](../events/README.md) to. 34 | the registration is needed to create unique channel ids for the [Event](../events/README.md) bus. These ids are faster 35 | than using `String` ids 36 | 37 | **Usage:** 38 | 39 | ```java 40 | static { 41 | // Register a channel 42 | int MY_EVENT_CHANNEL_ID = EventChannelRegister.registerChannelId("my_channel_name"); 43 | 44 | // Getting a channel name by id 45 | String myChanelName = EventChannelRegister.eventNameOf(MY_EVENT_CHANNEL_ID); 46 | 47 | // Getting a channelId by name 48 | int MY_EVENT_CHANNEL_ID = EventChannelRegister.eventIdOf("my_channel_name"); 49 | 50 | // checking if a channel is registered 51 | boolean isChannelAvailable = ConfigRegister.isChannelIdAvailable("my_config_key"); 52 | } 53 | ``` 54 | 55 | ### LogFormatRegister 56 | 57 | This register is used to register custom log formats. Default formats are `console` and `json`. 58 | The [LogService](../services/logger/README.md) is still under construction. The functionality might change in the future. 59 | Simply use the default log Formatter interface of java `java.util.logging.Formatter`. 60 | 61 | **Usage:** 62 | 63 | ```java 64 | static { 65 | // Register a log formatter 66 | LogFormatRegister.registerLogFormatter("xml", new XmlLogFormatter()); 67 | 68 | // Getting a log formatter by name 69 | Formatter jsonFormatter = LogFormatRegister.getLogFormatter("json"); 70 | } 71 | ``` 72 | 73 | ### TypeConversionRegister 74 | 75 | The `TypeConversionRegister` is used to register custom type converters. It's the core of Nano. 76 | These type conversion are used in the [Config/Context](../context/README.md), [Event](../events/README.md) 77 | Cache, [HttpServer](../services/httpserver/README.md) request & responses and everything which 78 | uses `TypeMap`, `TypeList` or `TypeInfo`. _See [TypeMap](https://github.com/YunaBraska/type-map) for more information._ 79 | 80 | **Usage:** 81 | 82 | ```java 83 | import berlin.yuna.typemap.logic.TypeConverter; 84 | 85 | static { 86 | // Register type conversion from String to LogLevel 87 | registerTypeConvert(String.class, LogLevel.class, LogLevel::nanoLogLevelOf); 88 | 89 | // Register type conversion from LogLevel to String 90 | registerTypeConvert(LogLevel.class, String.class, Enum::name); 91 | 92 | // Manual type conversion 93 | TypeConverter.convertObj("INFO", LogLevel.class); 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /docs/schedulers/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../README.md) / [Components](../../README.md#-components) 2 | 3 | [Context](../context/README.md) 4 | | [Events](../events/README.md) 5 | | [**> Schedulers <**](README.md) 6 | | [Services](../services/README.md) 7 | 8 | # Schedulers 9 | 10 | [Schedulers](../schedulers/README.md) are managed functions which run in the background. 11 | 12 | ```mermaid 13 | flowchart LR 14 | context(((Context))) --> schedulers[Schedulers] --> function[Custom Function] 15 | 16 | style context fill:#E3F2FD,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 17 | style schedulers fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 18 | style function fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 19 | ``` 20 | 21 | ## Examples 22 | 23 | * Run once with delay (128ms) 24 | * `context.run(() -> System.out.println("Scheduled"), 128, MILLISECONDS)` 25 | * Run periodically (evey 256ms) with initial delay (128ms) 26 | * `context.run(() -> System.out.println("Scheduled"), 128, 256, MILLISECONDS)` 27 | * Run at specific time (8:00:00) 28 | * `context.run(System.out.println("Scheduled"), LocalTime.of(8,0,0))` 29 | * Run at specific time (8:00:00) on specific day (Monday) 30 | * `context.run(() -> {}, LocalTime.of(8,0,0), DayOfWeek.MONDAY)` 31 | -------------------------------------------------------------------------------- /docs/services/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../README.md) / [Components](../../README.md#-components) 2 | 3 | [Context](../context/README.md) 4 | | [Events](../events/README.md) 5 | | [Schedulers](../schedulers/README.md) 6 | | [**> Services <**](README.md) 7 | 8 | # Services 9 | 10 | [Services](../services/README.md) are extensions for Nano which are independent managed programs that are running in the 11 | background. 12 | They are usually designed to be accessed by [Events](../events/README.md). 13 | Nano has default [Services](../services/README.md) 14 | like [HttpServer](httpserver/README.md), [MetricService](metricservice/README.md), [LogService](../services/logger/README.md) 15 | 16 | ## Start Services 17 | 18 | * `new Nano(new MetricService(), new HttpServer(), new HttpClient())` - [Services](../services/README.md) will start with 19 | Nano Startup 20 | * `context.run(new HttpServer())` - Service start 21 | 22 | ```mermaid 23 | flowchart TD 24 | services(((Services))) -.-> metricService[MetricService] 25 | services -.-> httpServer[HttpServer] 26 | metricService <--> events[Event] 27 | httpServer <--> events[Event] 28 | events[Event] <--> function[Custom Function] 29 | 30 | style services fill:#E3F2FD,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 31 | style events fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 32 | style httpServer fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 33 | style metricService fill:#90CAF9,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 34 | style function fill:#E3F2FD,stroke:#1565C0,stroke-width:1px,color:#1A237E,rx:2%,ry:2% 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/services/httpclient/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../../README.md) 2 | > / [Components](../../../README.md#-components) 3 | > / [Services](../../services/README.md) 4 | > / [**HttpClient**](README.md) 5 | 6 | * [Configuration](#configuration) 7 | * [Events](#events) 8 | 9 | # Http Client 10 | 11 | Is a default [Services](../../services/README.md) of Nano which is responsible for sending basic HTTP requests. 12 | 13 | ## Usage 14 | 15 | ### Start Http Service 16 | 17 | A) As startup [Service](../../services/README.md): `new Nano(new HttpClient())` 18 | 19 | B) Contextual `context.run(new HttpClient())` - this way its possible to provide a custom configuration. 20 | 21 | ### Intercept HTTP Requests 22 | 23 | The Event listener are executed in order of subscription. 24 | This makes it possible to define change data before sending the HTTP request. 25 | 26 | ```java 27 | public static void main(final String[] args) { 28 | final Nano app = new Nano(args, new HttpClient()); 29 | 30 | app.context(MyClass.class).newEvent(EVENT_SEND_HTTP, () -> new Httpobject().methodType(GET).path("http://localhost:8080/hello").body("Hello World")).send(); 31 | 32 | // Add Token to request 33 | app.subscribeEvent(EVENT_HTTP_REQUEST, event -> 34 | event.payloadOpt(HttpObject.class).ifPresent(request -> request.header("Authorization", "myCustomToken")) 35 | ); 36 | } 37 | ``` 38 | 39 | ### Send HTTP Requests 40 | 41 | ```java 42 | import org.nanonative.nano.services.http.HttpClient; 43 | 44 | public static void main(final String[] args) { 45 | final Context context = new Nano(args, new HttpClient()).context(MyClass.class); 46 | 47 | // send request via event 48 | final HttpObject response1 = context.sendEventR(EVENT_SEND_HTTP, () -> new HttpObject() 49 | .methodType(GET) 50 | .path("http://localhost:8080/hello") 51 | .body("Hello World") 52 | ).response(HttpObject.class); 53 | 54 | // send request via context 55 | final HttpObject response2 = new HttpObject() 56 | .methodType(GET) 57 | .path("http://localhost:8080/hello") 58 | .body("Hello World") 59 | .send(context); 60 | 61 | // send request manually 62 | final HttpObject response3 = new HttpClient().send(new HttpObject() 63 | .methodType(GET) 64 | .path("http://localhost:8080/hello") 65 | .body("Hello World") 66 | ); 67 | } 68 | ``` 69 | 70 | ## Configuration 71 | 72 | | [Config](../../context/README.md#configuration) | Type | Default | Description | 73 | |-------------------------------------------------|-----------|-------------------------------|-----------------------------------------------------| 74 | | `app_service_http_version` | `Integer` | `2` | (HttpClient) Http Version 1 or 2 | 75 | | `app_service_http_max_retries` | `Integer` | `3` | (HttpClient) Maximum number of retries | 76 | | `app_service_http_con_timeoutMs` | `Integer` | `5000` | (HttpClient) Connection timeout in milliseconds | 77 | | `app_service_http_read_timeoutMs` | `Integer` | `10000` | (HttpClient) Read timeout in milliseconds | 78 | | `app_service_http_follow_redirects` | `Boolean` | `true` | (HttpClient) Follow redirects | 79 | 80 | ## Events 81 | 82 | | In 🔲
Out 🔳 | [Event](../../events/README.md) | Payload | Response | Description | 83 | |--------------------|---------------------------------|--------------|--------------|-----------------------| 84 | | 🔳 | `EVENT_SEND_HTTP` | `HttpObject` | `HttpObject` | Sending a HttpRequest | 85 | 86 | -------------------------------------------------------------------------------- /docs/services/httpserver/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../../README.md) 2 | > / [Components](../../../README.md#-components) 3 | > / [Services](../../services/README.md) 4 | > / [**HttpServer**](README.md) 5 | 6 | * [Usage](#usage) 7 | * [Start HTTP Service](#start-http-service) 8 | * [Handle HTTP Requests](#handle-http-requests) 9 | * [Configuration](#configuration) 10 | * [Events](#events) 11 | 12 | # Http Service 13 | 14 | Is a default [Services](../../services/README.md) of Nano which is responsible for handling basic HTTP requests. 15 | Each request is processed in its own Thread. 16 | Support for Https/SSL is coming soon. 17 | 18 | ## Usage 19 | 20 | ### Start Http Service 21 | 22 | A) As startup [Service](../../services/README.md): `new Nano(new HttpServer())` 23 | 24 | B) Contextual `context.run(new HttpServer())` - this way its possible to provide a custom configuration. 25 | 26 | ### Handle HTTP Requests 27 | 28 | The Event listener are executed in order of subscription. 29 | This makes it possible to define authorization rules before the actual request is processed. 30 | 31 | ```java 32 | public static void main(final String[] args) { 33 | final Nano app = new Nano(args, new HttpServer()); 34 | 35 | // Authorization 36 | app.subscribeEvent(EVENT_HTTP_REQUEST, RestEndpoint::authorize); 37 | 38 | // Response 39 | app.subscribeEvent(EVENT_HTTP_REQUEST, RestEndpoint::helloWorldController); 40 | 41 | // Error handling 42 | app.subscribeEvent(EVENT_APP_UNHANDLED, RestEndpoint::controllerAdvice); 43 | } 44 | 45 | private static void helloWorldController(final Event event) { 46 | event.payloadOpt(HttpObject.class) 47 | .filter(HttpObject::isMethodGet) 48 | .filter(request -> request.pathMatch("/hello")) 49 | .ifPresent(request -> request.response().body(Map.of("Hello", System.getProperty("user.name"))).respond(event)); 50 | } 51 | 52 | private static void authorize(final Event event) { 53 | event.payloadOpt(HttpObject.class) 54 | .filter(request -> request.pathMatch("/hello/**")) 55 | .filter(request -> !"mySecretToken".equals(request.authToken())) 56 | .ifPresent(request -> request.response().body(Map.of("message", "You are unauthorized")).statusCode(401).respond(event)); 57 | } 58 | 59 | private static void controllerAdvice(final Event event) { 60 | event.payloadOpt(HttpObject.class).ifPresent(request -> 61 | request.response().body("Internal Server Error [" + event.error().getMessage() + "]").statusCode(500).respond(event)); 62 | } 63 | ``` 64 | 65 | ## Configuration 66 | 67 | | [Config](../../context/README.md#configuration) | Type | Default | Description | 68 | |-------------------------------------------------|-----------|-------------------------------|-----------------------------------------------------| 69 | | `app_service_http_port ` | `Integer` | `8080`, `8081`, ... (dynamic) | (HttpServer) Port | 70 | | `app_service_http_client` | `Boolean` | `false` | (HttpClient) If the HttpClient should start as well | 71 | 72 | ## Events 73 | 74 | | In 🔲
Out 🔳 | [Event](../../events/README.md) | Payload | Response | Description | 75 | |--------------------|---------------------------------|--------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 76 | | 🔲 | `EVENT_HTTP_REQUEST` | `HttpObject` | `HttpObject` | Triggered when an HTTP request is received.
If a response is returned for this event, it is sent back to the client. | 77 | | 🔲 | `EVENT_HTTP_REQUEST_UNHANDLED` | `HttpObject` | `HttpObject` | Triggered when an HTTP request is received but not handled.
If a response is returned for this event, it is sent back to the client.
Else client will receive a `404 | 78 | | 🔲 | `EVENT_APP_UNHANDLED` | `HttpObject` | `HttpObject` | Triggered when an exception occurs while handling an HTTP request.
If a response is returned for this event, it is sent back to the client.
Else client will receive a `500` | 79 | | 🔲 | `EVENT_HTTP_REQUEST` | `HttpObject` | `HttpObject` | Listening for HTTP request | 80 | 81 | -------------------------------------------------------------------------------- /docs/services/logger/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../../README.md) 2 | > / [Components](../../../README.md#-components) 3 | > / [Services](../../services/README.md) 4 | > / [**Logger**](README.md) 5 | 6 | # LogService 7 | 8 | The [LogService](../logger/README.md) is a simple wrapper around the build in `System.out.print` which comes with predefined log formats `console` 9 | and `json`. The javaLogger is not work concurrently. But `Level`, `LogRecord` and `Formatter` are supported. 10 | This service is starting automatically if no other LogService is provided. 11 | 12 | ## Placeholder 13 | 14 | The logger supports placeholders in the message string. The placeholders are replaced by the arguments passed to the 15 | logger. 16 | 17 | * `{}` and `%s` is replaced by the argument at the same index 18 | * `{0}` is replaced by the argument at the specified index 19 | 20 | ## Log Formatter 21 | 22 | The [LogService](../logger/README.md) supports two log formatters at default: 23 | 24 | * `console` - The console formatter logs the message to the console. 25 | * Example: `context.info(() -> "Hello {}", "World")` 26 | * Output: `[2024-11-11 11:11:11.111] [DEBUG] [Nano] - Hello World` 27 | * `json` - The json formatter logs the message as json to the console. 28 | * Example: `context.debug(() -> "Hello {}", "World")` 29 | Output: 30 | `{"Hello":"World", "level":"DEBUG","logger":"Nano","message":"Hello World","timestamp":"2024-11-11 11:11:11.111"}` 31 | 32 | ## Custom Log Formatter 33 | 34 | Custom log formatters can be registered by using `LogFormatRegister.registerLogFormatter(Name, Formatter)` - ( 35 | java.util.logging.Formatter) 36 | 37 | ## Custom LogService 38 | 39 | The default Logger can be overwritten by providing a custom `Service` which extends the `LogService` e.g. 40 | `new Nano(new CustomLogger())` 41 | -------------------------------------------------------------------------------- /docs/services/metricservice/README.md: -------------------------------------------------------------------------------- 1 | > [Home](../../../README.md) 2 | > / [Components](../../../README.md#-components) 3 | > / [Services](../../services/README.md) 4 | > / [**HttpServer**](README.md) 5 | 6 | * [Usage](#usage) 7 | * [Start Metric Service](#start-metric-service) 8 | * [Create Custom Metrics](#create-custom-metrics) 9 | * [Configuration](#configuration) 10 | * [Events](#events) 11 | 12 | # Metric Service 13 | 14 | Is a default [Services](../../services/README.md) of Nano which is responsible for collecting metrics. 15 | This service solves only basic metrics. 16 | Currently, there is no mechanism to push metrics to other applications. 17 | The standard and best practice is to use a dedicated metric collector like Prometheus and poll for metrics. 18 | Same as for tracking network traffic. 19 | This way ensures, that microservice will stay small, simple without putting unnecessary complexity into it. 20 | However, it is possible to extend or wrap the service with a custom implementation. E.g. with 21 | a [Scheduler](../../schedulers/README.md) and the [HttpClient](../httpserver/README.md#send-http-requests). 22 | The [MetricService](README.md) provides simple methods to get the metrics in the format 23 | of `Influx`, `Dynamo`, `Wavefront` and `Prometheus`. 24 | 25 | ## Usage 26 | 27 | ### Start Metric Service 28 | 29 | A) As startup [Service](../../services/README.md): `new Nano(new MetricService())` 30 | 31 | B) Contextual `context.run(new MetricService())` - this way its possible to provide a custom configuration. 32 | 33 | ### Metric Endpoints 34 | 35 | To get the metrics via HTTP, its necessary to also start 36 | the [HttpServer](../httpserver/README.md) `new Nano(new MetricService(), new HttpServer())`. 37 | 38 | The following endpoints are available: 39 | 40 | * `/metrics/influx` 41 | * `/metrics/dynamo` 42 | * `/metrics/wavefront` 43 | * `/metrics/prometheus` 44 | 45 | ### Create Custom Metrics 46 | 47 | ```java 48 | public static void main(final String[] args) { 49 | final Context context = new Nano(args, new HttpServer()).context(MyClass.class); 50 | 51 | // create counter 52 | context.newEvent(EVENT_METRIC_UPDATE).payload(() -> new MetricUpdate(COUNTER, "my.counter.key", 130624, metricTags)).send(); 53 | // create gauge 54 | context.newEvent(EVENT_METRIC_UPDATE).payload(() -> new MetricUpdate(GAUGE, "my.gauge.key", 200888, metricTags)).send(); 55 | // start timer 56 | context.newEvent(EVENT_METRIC_UPDATE).payload(() -> new MetricUpdate(TIMER_START, "my.timer.key", null, metricTags)).send(); 57 | // end timer 58 | context.newEvent(EVENT_METRIC_UPDATE).payload(() -> new MetricUpdate(TIMER_END, "my.timer.key", null, metricTags)).send(); 59 | } 60 | ``` 61 | 62 | ## Configuration 63 | 64 | | [Config](../../context/README.md#configuration) | Type | Default | Description | 65 | |-------------------------------------------------|----------|-----------------------|------------------------------------| 66 | | `app_service_metrics_base_url` | `String` | `/metrics` | Base path for all metric endpoints | 67 | | `app_service_influx_metrics_url` | `String` | `/metrics/influx` | Custom path for Influx | 68 | | `app_service_dynamo_metrics_url` | `String` | `/metrics/dynamo` | Custom path for Dynamo | 69 | | `app_service_prometheus_metrics_url` | `String` | `/metrics/prometheus` | Custom path for prometheus | 70 | | `app_service_wavefront_metrics_url` | `String` | `/metrics/wavefront` | Custom path for Wavefront | 71 | 72 | ## Events 73 | 74 | | In 🔲
Out 🔳 | [Event](../../events/README.md) | Payload | Response | Description | 75 | |--------------------|---------------------------------|----------------|--------------|--------------------------------------------| 76 | | 🔲 | `EVENT_METRIC_UPDATE` | `MetricUpdate` | `true` | Sets or updates specific metric | 77 | | 🔲 | `EVENT_APP_HEARTBEAT` | `-` | `true` | (Internal usage) Updates system metrics | 78 | | 🔲 | `EVENT_HTTP_REQUEST` | `HttpObject` | `HttpObject` | (Internal usage) provides metric endpoints | 79 | 80 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/core/NanoServices.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core; 2 | 3 | import org.nanonative.nano.core.model.Context; 4 | import org.nanonative.nano.core.model.Service; 5 | import org.nanonative.nano.helper.ExRunnable; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.concurrent.CopyOnWriteArrayList; 12 | 13 | import static java.util.Collections.emptyList; 14 | import static java.util.Collections.unmodifiableList; 15 | import static java.util.Optional.ofNullable; 16 | 17 | /** 18 | * The abstract base class for {@link Nano} framework providing {@link Service} handling functionalities. 19 | * 20 | * @param The type of the {@link NanoServices} implementation, used for method chaining. 21 | */ 22 | @SuppressWarnings({"unused", "UnusedReturnValue"}) 23 | public abstract class NanoServices> extends NanoThreads { 24 | 25 | protected final List services; 26 | 27 | /** 28 | * Initializes {@link NanoServices} with configurations and command-line arguments. 29 | * 30 | * @param config Configuration parameters for the {@link NanoServices} instance. 31 | * @param args Command-line arguments passed during the application start. 32 | */ 33 | protected NanoServices(final Map config, final String... args) { 34 | super(config, args); 35 | this.services = new CopyOnWriteArrayList<>(); 36 | subscribeEvent(Context.EVENT_APP_SERVICE_REGISTER, event -> event.payloadOpt(Service.class).map(this::registerService).ifPresent(nano -> event.acknowledge())); 37 | subscribeEvent(Context.EVENT_APP_SERVICE_UNREGISTER, event -> event.payloadOpt(Service.class).map(service -> unregisterService(event.context(), service)).ifPresent(nano -> event.acknowledge())); 38 | } 39 | 40 | /** 41 | * Retrieves a {@link Service} of a specified type. 42 | * 43 | * @param The type of the service to retrieve, which extends {@link Service}. 44 | * @param serviceClass The class of the {@link Service} to retrieve. 45 | * @return The first instance of the specified {@link Service}, or null if not found. 46 | */ 47 | public Optional serviceOpt(final Class serviceClass) { 48 | return ofNullable(service(serviceClass)); 49 | } 50 | 51 | /** 52 | * Retrieves a {@link Service} of a specified type. 53 | * 54 | * @param The type of the service to retrieve, which extends {@link Service}. 55 | * @param serviceClass The class of the {@link Service} to retrieve. 56 | * @return The first instance of the specified {@link Service}, or null if not found. 57 | */ 58 | public S service(final Class serviceClass) { 59 | final List results = services(serviceClass); 60 | if (results != null && !results.isEmpty()) { 61 | return results.getFirst(); 62 | } 63 | return null; 64 | } 65 | 66 | /** 67 | * Retrieves a list of services of a specified type. 68 | * 69 | * @param The type of the service to retrieve, which extends {@link Service}. 70 | * @param serviceClass The class of the service to retrieve. 71 | * @return A list of services of the specified type. If no services of this type are found, 72 | * an empty list is returned. 73 | */ 74 | public List services(final Class serviceClass) { 75 | if (serviceClass != null) { 76 | return services.stream() 77 | .filter(serviceClass::isInstance) 78 | .map(serviceClass::cast) 79 | .toList(); 80 | } 81 | return emptyList(); 82 | } 83 | 84 | /** 85 | * Provides an unmodifiable list of all registered {@link Service}. 86 | * 87 | * @return An unmodifiable list of {@link Service} instances. 88 | */ 89 | public List services() { 90 | return unmodifiableList(services); 91 | } 92 | 93 | /** 94 | * Shuts down all registered {@link Service} gracefully. 95 | * 96 | * @param context The {@link Context} in which the services are shut down. 97 | */ 98 | protected void shutdownServices(final Context context) { 99 | if (context.asBooleanOpt(Context.CONFIG_PARALLEL_SHUTDOWN).orElse(false)) { 100 | try { 101 | context.runAwait(services.stream().map(service -> (ExRunnable) () -> unregisterService(context, service)).toArray(ExRunnable[]::new)); 102 | } catch (final Exception err) { 103 | context.fatal(err, () -> "Service [{}] shutdown error. Looks like the Death Star blew up again.", Service.class.getSimpleName()); 104 | Thread.currentThread().interrupt(); 105 | } 106 | } else { 107 | new ArrayList<>(services).reversed().forEach(service -> unregisterService(context, service)); 108 | } 109 | } 110 | 111 | /** 112 | * Registers a new service in the {@link Nano} framework. 113 | * 114 | * @param service The {@link Service} to register. 115 | * @return Self for chaining 116 | */ 117 | @SuppressWarnings("unchecked") 118 | protected T registerService(final Service service) { 119 | if (service != null) { 120 | services.add(service); 121 | } 122 | return (T) this; 123 | } 124 | 125 | /** 126 | * Unregisters a {@link Service} from the {@link Nano} framework and stops it. 127 | * 128 | * @param context The {@link Context} in which the {@link Service} is unregistered and stopped. 129 | * @param service The {@link Service} to unregister and stop. 130 | * @return Self for chaining 131 | */ 132 | @SuppressWarnings("unchecked") 133 | protected T unregisterService(final Context context, final Service service) { 134 | if (service != null) { 135 | services.remove(service); 136 | try { 137 | if (service.isReadyState().compareAndSet(true, false)) 138 | service.stop(); 139 | } catch (final Exception e) { 140 | context.warn(e, () -> "Stop [{}] error. Somebody call the Ghostbusters!", service.name()); 141 | } 142 | } 143 | return (T) this; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/core/model/NanoThread.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import org.nanonative.nano.helper.ExRunnable; 4 | import org.nanonative.nano.helper.LockedBoolean; 5 | 6 | import java.lang.management.ManagementFactory; 7 | import java.lang.management.ThreadMXBean; 8 | import java.util.Arrays; 9 | import java.util.Date; 10 | import java.util.List; 11 | import java.util.Objects; 12 | import java.util.concurrent.CopyOnWriteArrayList; 13 | import java.util.concurrent.CountDownLatch; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.TimeUnit; 17 | import java.util.concurrent.atomic.AtomicLong; 18 | import java.util.function.BiConsumer; 19 | import java.util.function.Supplier; 20 | 21 | import static java.util.Optional.ofNullable; 22 | import static org.nanonative.nano.core.NanoThreads.runAsync; 23 | import static org.nanonative.nano.helper.NanoUtils.handleJavaError; 24 | 25 | public class NanoThread { 26 | 27 | protected final List> onCompleteCallbacks = new CopyOnWriteArrayList<>(); 28 | protected final LockedBoolean isComplete = new LockedBoolean(); 29 | 30 | public static final ExecutorService GLOBAL_THREAD_POOL = Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("nano-thread-", 0).factory()); 31 | protected static final AtomicLong activeNanoThreadCount = new AtomicLong(0); 32 | 33 | public boolean isComplete() { 34 | return isComplete.get(); 35 | } 36 | 37 | public NanoThread onComplete(final BiConsumer onComplete) { 38 | isComplete.run(state -> { 39 | if (Boolean.TRUE.equals(state)) { 40 | onComplete.accept(this, null); 41 | } else { 42 | onCompleteCallbacks.add(onComplete); 43 | } 44 | }); 45 | return this; 46 | } 47 | 48 | public NanoThread await() { 49 | return await(null); 50 | } 51 | 52 | public NanoThread await(final Runnable onDone) { 53 | return waitFor(onDone, this)[0]; 54 | } 55 | 56 | @SuppressWarnings("java:S1181") // Throwable is caught 57 | public NanoThread run(final Supplier context, final ExRunnable task) { 58 | runAsync(() -> { 59 | try { 60 | activeNanoThreadCount.incrementAndGet(); 61 | task.run(); 62 | isComplete.set(true, state -> onCompleteCallbacks.forEach(onComplete -> onComplete.accept(this, null))); 63 | } catch (final Throwable error) { 64 | handleJavaError(context, error); 65 | isComplete.set(true, state -> { 66 | if (!onCompleteCallbacks.isEmpty()) 67 | onCompleteCallbacks.forEach(onComplete -> onComplete.accept(this, error)); 68 | else 69 | ofNullable(context).map(Supplier::get).ifPresent(ctx -> ctx.sendEventError(task, error)); 70 | }); 71 | } finally { 72 | activeNanoThreadCount.decrementAndGet(); 73 | } 74 | }); 75 | return this; 76 | } 77 | 78 | public static long activeNanoThreads() { 79 | return activeNanoThreadCount.get(); 80 | } 81 | 82 | public static long activeCarrierThreads() { 83 | final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); 84 | final long[] threadIds = threadMXBean.getAllThreadIds(); 85 | return Arrays.stream(threadMXBean.getThreadInfo(threadIds)) 86 | .filter(Objects::nonNull) 87 | .filter(info -> (info.getThreadName() != null && info.getThreadName().startsWith("CarrierThread")) 88 | || (info.getLockName() != null && info.getLockName().startsWith("java.lang.VirtualThread")) 89 | || (info.getLockOwnerName() != null && info.getLockName().startsWith("nano-thread-")) 90 | ) 91 | .count(); 92 | } 93 | 94 | /** 95 | * Blocks until all provided {@code NanoThread} instances have completed execution. 96 | * This method waits indefinitely for all threads to finish. 97 | * 98 | * @param threads An array of {@code NanoThread} instances to wait for. 99 | * @return The same array of {@code NanoThread} instances, allowing for method chaining or further processing. 100 | */ 101 | public static NanoThread[] waitFor(final NanoThread... threads) { 102 | return waitFor(null, threads); 103 | } 104 | 105 | /** 106 | * Waits for all provided {@link NanoThread} instances to complete execution and optionally executes 107 | * a {@link Runnable} once all threads have finished. If {@code onComplete} is not null, it will be 108 | * executed asynchronously after all threads have completed. This variant allows for non-blocking 109 | * behavior if {@code onComplete} is provided, where the method returns immediately, and the 110 | * {@code onComplete} action is executed in the background once all threads are done. 111 | * 112 | * @param onComplete An optional {@link Runnable} to execute once all threads have completed. 113 | * If null, the method blocks until all threads are done. If non-null, the method 114 | * returns immediately, and the {@code Runnable} is executed asynchronously 115 | * after thread completion. 116 | * @param threads An array of {@link NanoThread} instances to wait for. 117 | * @return The same array of {@link NanoThread} instances, allowing for method chaining or further processing. 118 | */ 119 | public static NanoThread[] waitFor(final Runnable onComplete, final NanoThread... threads) { 120 | 121 | final CountDownLatch latch = new CountDownLatch(threads.length); 122 | for (final NanoThread thread : threads) { 123 | thread.onComplete((nt, error) -> { 124 | latch.countDown(); 125 | if (!(error instanceof Error) && latch.getCount() <= 0 && onComplete != null) { 126 | onComplete.run(); 127 | } 128 | }); 129 | } 130 | 131 | if (onComplete == null) { 132 | try { 133 | //TODO configurable timeout 134 | final boolean completed = latch.await(10, TimeUnit.SECONDS); 135 | if (!completed) { 136 | System.err.println(new Date() + " [FATAL] Threads did no complete"); 137 | } 138 | } catch (final InterruptedException ignored) { 139 | Thread.currentThread().interrupt(); 140 | } 141 | } 142 | return threads; 143 | } 144 | 145 | @Override 146 | public String toString() { 147 | return this.getClass().getSimpleName() + "{" + 148 | "onCompleteCallbacks=" + onCompleteCallbacks.size() + 149 | ", isComplete=" + isComplete() + 150 | '}'; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/core/model/Scheduler.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import berlin.yuna.typemap.model.LinkedTypeMap; 4 | 5 | import java.util.concurrent.RejectedExecutionHandler; 6 | import java.util.concurrent.ScheduledThreadPoolExecutor; 7 | import java.util.concurrent.ThreadPoolExecutor; 8 | 9 | public class Scheduler extends ScheduledThreadPoolExecutor { 10 | private final String id; 11 | 12 | public Scheduler(final String id) { 13 | this(id, 1, new ThreadPoolExecutor.CallerRunsPolicy()); 14 | } 15 | 16 | public Scheduler(final String id, final int corePoolSize, final RejectedExecutionHandler handler) { 17 | super(corePoolSize, handler); 18 | this.id = id; 19 | } 20 | 21 | public String id() { 22 | return id; 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return new LinkedTypeMap() 28 | .putR("id", id) 29 | .toJson(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/core/model/Service.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import berlin.yuna.typemap.model.TypeMap; 4 | import berlin.yuna.typemap.model.TypeMapI; 5 | import org.nanonative.nano.helper.event.model.Event; 6 | import org.nanonative.nano.services.metric.model.MetricUpdate; 7 | 8 | import java.util.Map; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | 11 | import static java.util.Arrays.stream; 12 | import static org.nanonative.nano.core.model.Context.EVENT_APP_SERVICE_REGISTER; 13 | import static org.nanonative.nano.core.model.Context.EVENT_CONFIG_CHANGE; 14 | import static org.nanonative.nano.services.metric.logic.MetricService.EVENT_METRIC_UPDATE; 15 | import static org.nanonative.nano.services.metric.model.MetricType.GAUGE; 16 | 17 | public abstract class Service { 18 | 19 | protected final long createdAtMs; 20 | protected final AtomicBoolean isReady = new AtomicBoolean(false); 21 | protected Context context; 22 | 23 | protected Service() { 24 | this.createdAtMs = System.currentTimeMillis(); 25 | } 26 | 27 | public abstract void start(); 28 | 29 | public abstract void stop(); 30 | 31 | public abstract Object onFailure(final Event error); 32 | 33 | public abstract void onEvent(final Event event); 34 | 35 | public void configure(final TypeMapI config) { 36 | configure(config, config); 37 | } 38 | 39 | public abstract void configure(final TypeMapI changes, final TypeMapI merged); 40 | 41 | public String name() { 42 | return this.getClass().getSimpleName(); 43 | } 44 | 45 | public Context context() { 46 | return context; 47 | } 48 | 49 | public boolean isReady() { 50 | return isReady.get(); 51 | } 52 | 53 | public AtomicBoolean isReadyState() { 54 | return isReady; 55 | } 56 | 57 | public Service context(final Context context) { 58 | this.context = context; 59 | return this; 60 | } 61 | 62 | public long createdAtMs() { 63 | return createdAtMs; 64 | } 65 | 66 | //########## GLOBAL SERVICE METHODS ########## 67 | public Service receiveEvent(final Event event) { 68 | if (event.channelId() == EVENT_CONFIG_CHANGE) { 69 | event.payloadOpt().filter(TypeMapI.class::isInstance).map(TypeMapI.class::cast) 70 | .or(() -> event.payloadOpt(Map.class).map(TypeMap::new).map(TypeMapI.class::cast)) 71 | .ifPresentOrElse(configs -> { 72 | final TypeMap merged = new TypeMap(context); 73 | context.forEach(merged::putIfAbsent); 74 | configure(configs, merged); 75 | context.putAll(configs); 76 | }, () -> onEvent(event)); 77 | } else { 78 | onEvent(event); 79 | } 80 | return this; 81 | } 82 | 83 | public NanoThread nanoThread(final Context context) { 84 | return new NanoThread().run(() -> context.nano() != null ? context : null, () -> { 85 | final long startTime = System.currentTimeMillis(); 86 | if (!isReady.get()) { 87 | this.context = context.newContext(this.getClass()); 88 | this.configure(context); 89 | this.start(); 90 | this.context.broadcastEvent(EVENT_APP_SERVICE_REGISTER, () -> this); 91 | this.context.sendEvent(EVENT_METRIC_UPDATE, () -> new MetricUpdate(GAUGE, "application.services.ready.time", System.currentTimeMillis() - startTime, Map.of("class", this.getClass().getSimpleName())), result -> {}); 92 | isReady.set(true); 93 | } 94 | }).onComplete((nanoThread, error) -> { 95 | if (error != null) 96 | this.context.sendEventError(context.newEvent(EVENT_APP_SERVICE_REGISTER).payload(() -> this), this, error); 97 | }); 98 | } 99 | 100 | public static NanoThread[] threadsOf(final Context context, final Service... services) { 101 | return stream(services).map(service -> service.nanoThread(context)).toArray(NanoThread[]::new); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/core/model/Unhandled.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import org.nanonative.nano.core.Nano; 4 | 5 | import java.util.Optional; 6 | 7 | import static berlin.yuna.typemap.logic.TypeConverter.convertObj; 8 | 9 | public record Unhandled(Context context, Object payload, Throwable exception) { 10 | 11 | public Nano nano() { 12 | return context == null ? null : context.nano(); 13 | } 14 | 15 | public Optional payloadOpt(final Class type) { 16 | return Optional.ofNullable(payload(type)); 17 | } 18 | 19 | public T payload(final Class type) { 20 | return convertObj(payload, type); 21 | } 22 | 23 | public Throwable exception() { 24 | return exception; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "Unhandled{" + 30 | "payload=" + payload + 31 | ", exception=" + exception + 32 | '}'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/helper/ExRunnable.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.helper; 2 | 3 | @FunctionalInterface 4 | public interface ExRunnable { 5 | @SuppressWarnings({"java:S112", "RedundantThrows"}) 6 | void run() throws Exception; 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/helper/LockedBoolean.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.helper; 2 | 3 | import java.util.concurrent.Semaphore; 4 | import java.util.function.Consumer; 5 | 6 | /** 7 | * A thread-safe class that manages a boolean state with synchronized actions. 8 | * Unlike {@link java.util.concurrent.atomic.AtomicBoolean}, LockedBoolean allows executing complex 9 | * synchronized actions based on the current state and conditionally updating the state within a locked context. 10 | * This class is designed for scenarios where actions need to be performed during state changes, 11 | * ensuring thread safety and atomicity of operations. 12 | */ 13 | public class LockedBoolean { 14 | private boolean state; 15 | private final Semaphore lock = new Semaphore(1); 16 | 17 | /** 18 | * Constructs a new LockedBoolean with the initial state set to false. 19 | */ 20 | public LockedBoolean() { 21 | this(false); 22 | } 23 | 24 | /** 25 | * Constructs a new LockedBoolean with the specified initial state. 26 | * 27 | * @param state the initial state 28 | */ 29 | public LockedBoolean(final boolean state) { 30 | this.state = state; 31 | } 32 | 33 | /** 34 | * Sets the state to the specified value with thread safety. 35 | * 36 | * @param state the new state value 37 | */ 38 | public void set(final boolean state) { 39 | execute(null, state, null); 40 | } 41 | 42 | /** 43 | * Sets the state to the specified value with thread safety. 44 | * 45 | * @param state the new state value 46 | * @param run The {@link Consumer} to execute while setting new state. 47 | */ 48 | public void set(final boolean state, final Consumer run) { 49 | execute(null, state, run); 50 | } 51 | 52 | /** 53 | * Conditionally sets the state based on a condition. 54 | * 55 | * @param when The condition that determines if the state should be set to {@code then}. 56 | * @param then The state to set if {@code when} is true. 57 | */ 58 | public void set(final boolean when, final boolean then) { 59 | set(when, then, null); 60 | } 61 | 62 | /** 63 | * Conditionally sets the state and executes a {@link Consumer} based on a condition. 64 | * 65 | * @param when The condition that determines if the state should be set to {@code then}. 66 | * @param then The state to set if {@code when} is true. 67 | * @param run The {@link Consumer} to execute if the condition {@code when} is true. 68 | */ 69 | public void set(final boolean when, final boolean then, final Consumer run) { 70 | execute(when, then, run); 71 | } 72 | 73 | /** 74 | * Executes a {@link Consumer} based on the current state. 75 | * 76 | * @param run The Runnable to execute if the current state is true. 77 | */ 78 | public void run(final Consumer run) { 79 | execute(null, null, run); 80 | } 81 | 82 | /** 83 | * Executes a {@link Consumer} based on a condition without changing the state. 84 | * 85 | * @param when The condition that determines which Runnable to execute. 86 | * @param then The {@link Consumer} to execute if {@code when} is true. 87 | */ 88 | public void run(final boolean when, final Consumer then) { 89 | execute(when, null, then); 90 | } 91 | 92 | /** 93 | * Retrieves the current state with thread safety. 94 | * 95 | * @return the current boolean state 96 | */ 97 | public boolean get() { 98 | try { 99 | lock.acquire(); 100 | return state; 101 | } catch (final InterruptedException e) { 102 | Thread.currentThread().interrupt(); 103 | return state; 104 | } finally { 105 | lock.release(); 106 | } 107 | } 108 | 109 | /** 110 | * Executes actions conditionally based on the expected state and optionally updates the state. 111 | * This private method is the core logic for state management and action execution. 112 | * 113 | * @param expected The expected state to trigger execution, or null if execution should not be conditional. 114 | * @param newValue The new state to set if execution occurs, or null if the state should not change. 115 | * @param run The {@link Consumer} to execute if expected matches the condition. 116 | */ 117 | private void execute(final Boolean expected, final Boolean newValue, final Consumer run) { 118 | try { 119 | lock.acquire(); 120 | if (expected == null || state == expected) { 121 | if (run != null) { 122 | run.accept(state); 123 | } 124 | if (newValue != null) 125 | state = newValue; 126 | } 127 | } catch (final InterruptedException e) { 128 | Thread.currentThread().interrupt(); 129 | } finally { 130 | lock.release(); 131 | } 132 | } 133 | 134 | @Override 135 | public String toString() { 136 | return this.getClass().getSimpleName() + "{" + 137 | "state=" + get() + 138 | '}'; 139 | } 140 | } 141 | 142 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/helper/config/ConfigRegister.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.helper.config; 2 | 3 | import org.nanonative.nano.helper.NanoUtils; 4 | import org.nanonative.nano.core.NanoBase; 5 | import org.nanonative.nano.core.model.Service; 6 | 7 | import static java.util.Optional.ofNullable; 8 | 9 | /** 10 | * Utility class for registering and retrieving configuration descriptions. 11 | *

12 | * This class is typically used for {@link Service} or custom functions to display 13 | * configuration keys and descriptions when the Java property -Dhelp=true is set. 14 | *

15 | */ 16 | public class ConfigRegister { 17 | 18 | /** 19 | * Registers a configuration key with its description. 20 | *

21 | * If the key is valid and non-null, it will be standardized and added to the configuration keys map 22 | * with the provided description. If the description is null, an empty string will be used. 23 | *

24 | * 25 | * @param key the configuration key to register 26 | * @param description the description of the configuration key 27 | * @return the standardized key if registration is successful, or {@code null} if the key is invalid 28 | */ 29 | public static String registerConfig(final String key, final String description) { 30 | return ofNullable(key) 31 | .filter(NanoUtils::hasText) 32 | .map(NanoBase::standardiseKey) 33 | .map(name -> { 34 | NanoBase.CONFIG_KEYS.computeIfAbsent(name, k -> description == null ? "" : description); 35 | return name; 36 | }) 37 | .orElse(null); 38 | } 39 | 40 | /** 41 | * Retrieves the description of a registered configuration key. 42 | *

43 | * If the key is valid and non-null, it will be standardized and its description 44 | * will be retrieved from the configuration keys map. 45 | *

46 | * 47 | * @param key the configuration key whose description is to be retrieved 48 | * @return the description of the configuration key, or {@code null} if the key is invalid or not found 49 | */ 50 | public static String configDescriptionOf(final String key) { 51 | return ofNullable(key) 52 | .filter(NanoUtils::hasText) 53 | .map(NanoBase::standardiseKey) 54 | .map(NanoBase.CONFIG_KEYS::get) 55 | .orElse(null); 56 | } 57 | 58 | private ConfigRegister() { 59 | // static util class 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/helper/event/EventChannelRegister.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.helper.event; 2 | 3 | import org.nanonative.nano.helper.NanoUtils; 4 | import org.nanonative.nano.core.NanoBase; 5 | 6 | import java.util.Map; 7 | import java.util.Optional; 8 | 9 | import static java.util.Optional.ofNullable; 10 | 11 | public class EventChannelRegister { 12 | 13 | /** 14 | * Registers a new event type with a given name if it does not already exist. 15 | * If the event type already exists, it returns the existing event type's ID. 16 | * 17 | * @param channelName The name of the event type to register. 18 | * @return The ID of the newly registered event type, or the ID of the existing event type 19 | * if it already exists. Returns -1 if the input is null or empty. 20 | */ 21 | public static int registerChannelId(final String channelName) { 22 | return ofNullable(channelName).filter(NanoUtils::hasText).map(name -> eventIdOf(channelName).orElseGet(() -> { 23 | final int channelId = NanoBase.EVENT_ID_COUNTER.incrementAndGet(); 24 | NanoBase.EVENT_TYPES.put(channelId, channelName); 25 | return channelId; 26 | })).orElse(-1); 27 | } 28 | 29 | /** 30 | * Retrieves the name of an event type given its ID. 31 | * 32 | * @param channelId The ID of the event type. 33 | * @return The name of the event type associated with the given ID, or null if not found. 34 | */ 35 | public static String eventNameOf(final int channelId) { 36 | return NanoBase.EVENT_TYPES.get(channelId); 37 | } 38 | 39 | /** 40 | * Attempts to find the ID of an event type based on its name. 41 | * This method is primarily used for debugging or startup purposes and is not optimized for performance. 42 | * 43 | * @param channelName The name of the event type. 44 | * @return An {@link Optional} containing the ID of the event type if found, or empty if not found 45 | * or if the input is null or empty. 46 | */ 47 | public static Optional eventIdOf(final String channelName) { 48 | return NanoUtils.hasText(channelName) ? NanoBase.EVENT_TYPES.entrySet().stream().filter(type -> type.getValue().equals(channelName)).map(Map.Entry::getKey).findFirst() : Optional.empty(); 49 | } 50 | 51 | /** 52 | * Checks if an event type with the given ID exists. 53 | * 54 | * @param channelId The ID of the event type to check. 55 | * @return true if an event type with the given ID exists, false otherwise. 56 | */ 57 | public static boolean isChannelIdAvailable(final int channelId) { 58 | return NanoBase.EVENT_TYPES.containsKey(channelId); 59 | } 60 | 61 | private EventChannelRegister() { 62 | // static util class 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/helper/event/model/Event.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.helper.event.model; 2 | 3 | import berlin.yuna.typemap.model.LinkedTypeMap; 4 | import berlin.yuna.typemap.model.Type; 5 | import berlin.yuna.typemap.model.TypeMap; 6 | import org.nanonative.nano.core.Nano; 7 | import org.nanonative.nano.core.model.Context; 8 | 9 | import java.util.Objects; 10 | import java.util.Optional; 11 | import java.util.function.Consumer; 12 | import java.util.function.Function; 13 | import java.util.function.Predicate; 14 | import java.util.function.Supplier; 15 | 16 | import static berlin.yuna.typemap.logic.TypeConverter.convertObj; 17 | import static java.util.Optional.ofNullable; 18 | import static org.nanonative.nano.helper.event.EventChannelRegister.eventNameOf; 19 | 20 | @SuppressWarnings({"unused", "UnusedReturnValue"}) 21 | public class Event extends TypeMap { 22 | 23 | protected int channelId; 24 | protected final Context context; 25 | protected transient Consumer responseListener; 26 | protected transient Supplier payload; 27 | protected transient Object payloadRaw; 28 | protected transient Object response; 29 | protected Throwable error; 30 | 31 | public static final String EVENT_ORIGINAL_CHANNEL_ID = "app_original_event_channel_id"; 32 | 33 | public static Event eventOf(final Context context, final int channelId) { 34 | return new Event(context).channelId(channelId); 35 | } 36 | 37 | public static Event asyncEventOf(final Context context, final int channelId) { 38 | return new Event(context).async(true).channelId(channelId); 39 | } 40 | 41 | /** 42 | * Constructs an instance of the Event class with specified type, context, payload, and response listener. 43 | * This event object can be used to trigger specific actions or responses based on the event type and payload. 44 | * 45 | * @param context The {@link Context} in which the event is created and processed. It provides environmental data and configurations. 46 | */ 47 | public Event(final Context context) { 48 | this.context = context; 49 | this.put("createdAt", System.currentTimeMillis()); 50 | } 51 | 52 | /** 53 | * Returns the event name based on the channel ID. 54 | * 55 | * @return The name of the event. 56 | */ 57 | public String channel() { 58 | return eventNameOf(channelId); 59 | } 60 | 61 | /** 62 | * Returns the event name based on the original channel ID. 63 | * 64 | * @return The name of the event. 65 | */ 66 | public String nameOrg() { 67 | return eventNameOf(channelIdOrg()); 68 | } 69 | 70 | public Nano nano() { 71 | return context.nano(); 72 | } 73 | 74 | /** 75 | * @return The integer representing the type of the event. This typically corresponds to a specific kind of event. 76 | */ 77 | public int channelId() { 78 | return channelId; 79 | } 80 | 81 | /** 82 | * @return The integer representing the original type of the event. This typically corresponds to a specific kind of event. 83 | */ 84 | public int channelIdOrg() { 85 | return asIntOpt(EVENT_ORIGINAL_CHANNEL_ID).orElse(channelId); 86 | } 87 | 88 | /** 89 | * Sets the channel ID of the event. 90 | * 91 | * @param channelId The integer representing the type of the event. This typically corresponds to a specific kind of event. 92 | * @return self for chaining 93 | */ 94 | public Event channelId(final int channelId) { 95 | this.channelId = channelId; 96 | return this; 97 | } 98 | 99 | /** 100 | * Sets the event to asynchronous mode, allowing the response to be handled by a listener. 101 | * 102 | * @return self for chaining 103 | */ 104 | public Event async(final boolean async) { 105 | this.responseListener = async ? ignored -> {} : null; 106 | return this; 107 | } 108 | 109 | /** 110 | * Sets the event to asynchronous mode, allowing the response to be handled by a listener. 111 | * 112 | * @param responseListener A consumer that handles the response of the event processing. It can be used to execute actions based on the event's outcome or data. 113 | * @return self for chaining 114 | */ 115 | public Event async(final Consumer responseListener) { 116 | this.responseListener = responseListener; 117 | return this; 118 | } 119 | 120 | /** 121 | * Sets the payload of the event. 122 | * 123 | * @param payload The data or object that is associated with this event. This can be any relevant information that needs to be passed along with the event. 124 | * @return self for chaining 125 | */ 126 | public Event payload(final Supplier payload) { 127 | this.payload = payload; 128 | return this; 129 | } 130 | 131 | public Event ifPresent(final int channelId, final Consumer consumer) { 132 | if (this.channelId == channelId) { 133 | consumer.accept(this); 134 | } 135 | return this; 136 | } 137 | 138 | public Event ifPresentAck(final int channelId, final Consumer consumer) { 139 | if (this.channelId == channelId) { 140 | consumer.accept(this); 141 | acknowledge(); 142 | } 143 | return this; 144 | } 145 | 146 | public Event ifPresent(final int channelId, final Class clazz, final Consumer consumer) { 147 | if (this.channelId == channelId) { 148 | final T payloadObj = payload(clazz); 149 | if (payloadObj != null) 150 | consumer.accept(payloadObj); 151 | } 152 | return this; 153 | } 154 | 155 | public Event ifPresentAck(final int channelId, final Class clazz, final Function consumer) { 156 | if (this.channelId == channelId) { 157 | final T payloadObj = payload(clazz); 158 | if (payloadObj != null) { 159 | response(consumer.apply(payloadObj)); 160 | } 161 | } 162 | return this; 163 | } 164 | 165 | public Object payload() { 166 | if (payloadRaw == null) 167 | payloadRaw = payload == null ? null : payload.get(); 168 | return payloadRaw; 169 | } 170 | 171 | @SuppressWarnings({"BooleanMethodIsAlwaysInverted"}) 172 | public boolean isBroadcast() { 173 | return asBooleanOpt("isBroadcast").orElse(false); 174 | } 175 | 176 | public Event broadcast(final boolean broadcast) { 177 | return putR("isBroadcast", broadcast); 178 | } 179 | 180 | public boolean isAcknowledged() { 181 | return response != null; 182 | } 183 | 184 | public Optional payloadOpt() { 185 | return ofNullable(payload()); 186 | } 187 | 188 | public T payload(final Class type) { 189 | return convertObj(payload(), type); 190 | } 191 | 192 | public Optional payloadOpt(final Class type) { 193 | return ofNullable(convertObj(payload(), type)); 194 | } 195 | 196 | public Context context() { 197 | return context; 198 | } 199 | 200 | public boolean isAsync() { 201 | return responseListener != null; 202 | } 203 | 204 | public Consumer async() { 205 | return responseListener; 206 | } 207 | 208 | public Event acknowledge() { 209 | return acknowledge(null); 210 | } 211 | 212 | public Event acknowledge(final Runnable response) { 213 | if (response != null) 214 | response.run(); 215 | return response(true); 216 | } 217 | 218 | public Event response(final Object response) { 219 | if (responseListener != null) 220 | responseListener.accept(response); 221 | this.response = response; 222 | return this; 223 | } 224 | 225 | public T response(final Class type) { 226 | return convertObj(response, type); 227 | } 228 | 229 | public Optional responseOpt(final Class type) { 230 | return ofNullable(response(type)); 231 | } 232 | 233 | public Object response() { 234 | return response; 235 | } 236 | 237 | public Event peek(final Consumer peek) { 238 | if (peek != null) 239 | peek.accept(this); 240 | return this; 241 | } 242 | 243 | @Override 244 | public Event putR(Object key, Object value) { 245 | this.put(key, value); 246 | return this; 247 | } 248 | 249 | public Type filter(final Predicate predicate) { 250 | return new Type<>(predicate.test(this) ? this : null); 251 | } 252 | 253 | public Throwable error() { 254 | return error; 255 | } 256 | 257 | public Event error(final Throwable error) { 258 | this.error = error; 259 | return this; 260 | } 261 | 262 | /** 263 | * Sends the event to the Nano instance for processing. 264 | * 265 | * @return self for chaining 266 | */ 267 | public Event send() { 268 | nano().sendEvent(this); 269 | return this; 270 | } 271 | 272 | @Override 273 | public String toString() { 274 | return new LinkedTypeMap() 275 | .putR("channel", channel()) 276 | .putR("ack", response != null) 277 | .putR("listener", responseListener != null) 278 | .putR("payload", payload(String.class)) 279 | .putR("size", context.size() + this.size() 280 | + (payload == null ? 0 : 1) 281 | + (responseListener == null ? 0 : 1) 282 | + (response == null ? 0 : 1) 283 | + (error == null ? 0 : 1) 284 | ) 285 | .toJson(); 286 | } 287 | 288 | @Override 289 | public boolean equals(final Object o) { 290 | if (!(o instanceof Event event)) return false; 291 | if (!super.equals(o)) return false; 292 | return channelId == event.channelId && Objects.equals(context, event.context) && Objects.equals(responseListener, event.responseListener) && Objects.equals(payload, event.payload); 293 | } 294 | 295 | @Override 296 | public int hashCode() { 297 | return Objects.hash(super.hashCode(), channelId, context, responseListener, payload); 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/http/HttpClient.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.http; 2 | 3 | import berlin.yuna.typemap.model.LinkedTypeMap; 4 | import berlin.yuna.typemap.model.TypeMapI; 5 | import org.nanonative.nano.core.model.Service; 6 | import org.nanonative.nano.helper.event.model.Event; 7 | import org.nanonative.nano.services.http.model.HttpObject; 8 | 9 | import java.io.IOException; 10 | import java.net.http.HttpRequest; 11 | import java.net.http.HttpResponse; 12 | import java.time.Duration; 13 | import java.util.function.Consumer; 14 | 15 | import static java.net.http.HttpClient.Redirect.ALWAYS; 16 | import static java.net.http.HttpClient.Redirect.NEVER; 17 | import static java.net.http.HttpClient.Version.HTTP_2; 18 | import static org.nanonative.nano.core.model.NanoThread.GLOBAL_THREAD_POOL; 19 | import static org.nanonative.nano.helper.config.ConfigRegister.registerConfig; 20 | import static org.nanonative.nano.helper.event.EventChannelRegister.registerChannelId; 21 | 22 | public class HttpClient extends Service { 23 | 24 | public static final String CONFIG_HTTP_CLIENT_VERSION = registerConfig("app_service_http_version", "HTTP client version 1 or 2 (see " + HttpClient.class.getSimpleName() + ")"); 25 | public static final String CONFIG_HTTP_CLIENT_MAX_RETRIES = registerConfig("app_service_http_max_retries", "Maximum number of retries for the HTTP client (see " + HttpClient.class.getSimpleName() + ")"); 26 | public static final String CONFIG_HTTP_CLIENT_CON_TIMEOUT_MS = registerConfig("app_service_http_con_timeoutMs", "Connection timeout in milliseconds for the HTTP client (see " + HttpClient.class.getSimpleName() + ")"); 27 | public static final String CONFIG_HTTP_CLIENT_READ_TIMEOUT_MS = registerConfig("app_service_http_read_timeoutMs", "Read timeout in milliseconds for the HTTP client (see " + HttpClient.class.getSimpleName() + ")"); 28 | public static final String CONFIG_HTTP_CLIENT_FOLLOW_REDIRECTS = registerConfig("app_service_http_follow_redirects", "Follow redirects for the HTTP client (see " + HttpClient.class.getSimpleName() + ")"); 29 | 30 | public static final int EVENT_SEND_HTTP = registerChannelId("SEND_HTTP"); 31 | 32 | protected java.net.http.HttpClient client; 33 | protected int retries = 3; 34 | protected long readTimeoutMs = 10000; 35 | 36 | @Override 37 | public void start() { 38 | client = java.net.http.HttpClient.newBuilder() 39 | .connectTimeout(Duration.ofMillis(context.asLongOpt(CONFIG_HTTP_CLIENT_CON_TIMEOUT_MS).orElse(5000L))) 40 | .followRedirects(context.asBooleanOpt(CONFIG_HTTP_CLIENT_FOLLOW_REDIRECTS).orElse(true) ? ALWAYS : NEVER) 41 | .version(context.asOpt(java.net.http.HttpClient.Version.class, CONFIG_HTTP_CLIENT_VERSION).orElse(HTTP_2)) 42 | .executor(GLOBAL_THREAD_POOL) 43 | .build(); 44 | } 45 | 46 | @Override 47 | public void stop() { 48 | client.close(); 49 | client = null; 50 | } 51 | 52 | @Override 53 | public Object onFailure(final Event error) { 54 | return null; 55 | } 56 | 57 | @Override 58 | @SuppressWarnings("unchecked") 59 | public void onEvent(final Event event) { 60 | event.ifPresentAck(EVENT_SEND_HTTP, HttpObject.class, httpObject -> send(httpObject, event.as(Consumer.class, "callback"))); 61 | event.ifPresentAck(EVENT_SEND_HTTP, HttpRequest.class, httpRequest -> send(httpRequest, event.as(Consumer.class, "callback"))); 62 | } 63 | 64 | @Override 65 | public void configure(final TypeMapI changes, final TypeMapI merged) { 66 | changes.asIntOpt(CONFIG_HTTP_CLIENT_MAX_RETRIES).ifPresent(value -> retries = value); 67 | changes.asIntOpt(CONFIG_HTTP_CLIENT_READ_TIMEOUT_MS).ifPresent(value -> readTimeoutMs = value); 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return new LinkedTypeMap() 73 | .putR("version", version()) 74 | .putR("retries", retries) 75 | .putR("followRedirects", followRedirects()) 76 | .putR("readTimeoutMs", readTimeoutMs) 77 | .putR("connectionTimeoutMs", connectionTimeoutMs()) 78 | .toJson(); 79 | } 80 | 81 | /** 82 | * Sends an HTTP request using the provided {@link HttpObject} or {@link HttpRequest}. 83 | * For async processing, use the {@link HttpClient#send(HttpRequest, Consumer)} method. 84 | * 85 | * @param request the {@link HttpObject} or {@link HttpRequest} representing the HTTP request to send 86 | * @return the response as an {@link HttpObject} 87 | */ 88 | public HttpObject send(final HttpRequest request) { 89 | return send(request, null); 90 | } 91 | 92 | /** 93 | * Sends an HTTP request using the provided {@link HttpObject} or {@link HttpRequest}. 94 | * If a response listener is provided, it processes the response asynchronously. 95 | * 96 | * @param request the {@link HttpObject} or {@link HttpRequest} representing the HTTP request to send 97 | * @param callback an optional consumer to process the response asynchronously 98 | * @return the response as an {@link HttpObject} 99 | */ 100 | public HttpObject send(final HttpRequest request, final Consumer callback) { 101 | if (request instanceof final HttpObject httpObject) 102 | httpObject.timeout(readTimeoutMs); 103 | return request != null ? send(0, request, new HttpObject(), callback) : new HttpObject().failure(400, new IllegalArgumentException("Invalid request [null]")); 104 | } 105 | 106 | /** 107 | * Returns the number of retries configured for this {@link HttpClient}. 108 | * 109 | * @return the number of retries 110 | */ 111 | public int retries() { 112 | return retries; 113 | } 114 | 115 | /** 116 | * Returns whether this {@link HttpClient} follows redirects. 117 | * 118 | * @return {@code true} if redirects are followed, {@code false} otherwise 119 | */ 120 | public boolean followRedirects() { 121 | return ALWAYS.equals(client.followRedirects()); 122 | } 123 | 124 | /** 125 | * Returns the read timeout in milliseconds configured for this {@link HttpClient}. 126 | * 127 | * @return the read timeout in milliseconds 128 | */ 129 | public long readTimeoutMs() { 130 | return readTimeoutMs; 131 | } 132 | 133 | /** 134 | * Returns the connection timeout in milliseconds configured for this {@link HttpClient}. 135 | * 136 | * @return the connection timeout in milliseconds 137 | */ 138 | public long connectionTimeoutMs() { 139 | return client.connectTimeout().map(Duration::toMillis).orElse(-1L); 140 | } 141 | 142 | /** 143 | * Returns the {@link java.net.http.HttpClient.Version} used by this {@link HttpClient}. 144 | * 145 | * @return the {@link java.net.http.HttpClient.Version} 146 | */ 147 | public java.net.http.HttpClient.Version version() { 148 | return client.version(); 149 | } 150 | 151 | public java.net.http.HttpClient client() { 152 | return client; 153 | } 154 | 155 | protected HttpObject send(final int attempt, final HttpRequest request, final HttpObject response, final Consumer callback) { 156 | if (client == null) 157 | configure(context); 158 | try { 159 | if (callback == null) { 160 | return responseOf(client.send(request, HttpResponse.BodyHandlers.ofByteArray()), response); 161 | } else { 162 | client.sendAsync(request, HttpResponse.BodyHandlers.ofByteArray()).thenAccept(httpResponse -> responseOf(httpResponse, response)).thenRun(() -> callback.accept(response)); 163 | } 164 | } catch (final IOException e) { 165 | return circuitBreaker(attempt, request, response, callback, e); 166 | } catch (final InterruptedException e) { 167 | Thread.currentThread().interrupt(); 168 | } catch (final Exception e) { 169 | return response.path(request.uri().toString()).statusCode(400).failure(-1, e); 170 | } 171 | return response; 172 | } 173 | 174 | protected HttpObject responseOf(final HttpResponse httpResponse, final HttpObject response) { 175 | final HttpObject result = response 176 | .statusCode(httpResponse.statusCode()) 177 | .methodType(httpResponse.request().method()) 178 | .path(httpResponse.uri().getPath()) 179 | .headerMap(httpResponse.headers().map()); 180 | return result.body(httpResponse.body()); 181 | } 182 | 183 | /** 184 | * Implements a circuit breaker pattern to handle retries for HTTP requests in case of failures. 185 | * This method attempts to resend the request after a delay that increases exponentially with the number of attempts. 186 | * If the maximum number of retries is reached, it logs the failure and stops retrying. 187 | * 188 | * @param attempt The current retry attempt number. 189 | * @param request The {@link HttpObject} representing the original HTTP request. 190 | * @param response The {@link HttpObject} to populate with the response upon successful request completion. 191 | * @param throwable The {@link Throwable} that triggered the need for a retry. 192 | * @return A modified {@link HttpObject} containing the result of the retry attempts. If all retries are exhausted without success, 193 | * it returns the {@link HttpObject} populated with the failure information. 194 | */ 195 | protected HttpObject circuitBreaker(final int attempt, final HttpRequest request, final HttpObject response, final Consumer callback, final Throwable throwable) { 196 | if (attempt < retries) { 197 | try { 198 | Thread.sleep((long) Math.pow(2, attempt) * 256); 199 | return send(attempt + 1, request, response, callback); 200 | } catch (final InterruptedException ie) { 201 | Thread.currentThread().interrupt(); 202 | return response.path(request.uri().toString()).failure(-99, ie); 203 | } 204 | } 205 | return response.path(request.uri().toString()).failure(-1, throwable); 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/http/HttpServer.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.http; 2 | 3 | import berlin.yuna.typemap.model.LinkedTypeMap; 4 | import berlin.yuna.typemap.model.Type; 5 | import berlin.yuna.typemap.model.TypeMapI; 6 | import com.sun.net.httpserver.HttpExchange; 7 | import org.nanonative.nano.core.model.Context; 8 | import org.nanonative.nano.core.model.Service; 9 | import org.nanonative.nano.core.model.Unhandled; 10 | import org.nanonative.nano.helper.NanoUtils; 11 | import org.nanonative.nano.helper.event.model.Event; 12 | import org.nanonative.nano.services.http.model.ContentType; 13 | import org.nanonative.nano.services.http.model.HttpHeaders; 14 | import org.nanonative.nano.services.http.model.HttpObject; 15 | 16 | import java.io.IOException; 17 | import java.io.OutputStream; 18 | import java.net.InetSocketAddress; 19 | import java.net.Socket; 20 | import java.util.List; 21 | import java.util.Optional; 22 | import java.util.concurrent.atomic.AtomicBoolean; 23 | import java.util.concurrent.locks.Lock; 24 | import java.util.concurrent.locks.ReentrantLock; 25 | import java.util.function.Consumer; 26 | 27 | import static berlin.yuna.typemap.logic.TypeConverter.collectionOf; 28 | import static org.nanonative.nano.core.model.Context.EVENT_APP_UNHANDLED; 29 | import static org.nanonative.nano.core.model.NanoThread.GLOBAL_THREAD_POOL; 30 | import static org.nanonative.nano.helper.config.ConfigRegister.registerConfig; 31 | import static org.nanonative.nano.helper.event.EventChannelRegister.registerChannelId; 32 | 33 | public class HttpServer extends Service { 34 | protected com.sun.net.httpserver.HttpServer server; 35 | 36 | // Register configurations 37 | public static final String CONFIG_SERVICE_HTTP_PORT = registerConfig("app_service_http_port", "Default port for the HTTP service (see " + HttpServer.class.getSimpleName() + ")"); 38 | public static final String CONFIG_SERVICE_HTTP_CLIENT = registerConfig("app_service_http_client", "Boolean if " + HttpClient.class.getSimpleName() + " should start as well"); 39 | 40 | // Register event channels 41 | public static final int EVENT_HTTP_REQUEST = registerChannelId("HTTP_REQUEST"); 42 | public static final int EVENT_HTTP_REQUEST_UNHANDLED = registerChannelId("HTTP_REQUEST_UNHANDLED"); 43 | 44 | public InetSocketAddress address() { 45 | return server == null ? null : server.getAddress(); 46 | } 47 | 48 | public int port() { 49 | return server == null ? -1 : server.getAddress().getPort(); 50 | } 51 | 52 | public com.sun.net.httpserver.HttpServer server() { 53 | return server; 54 | } 55 | 56 | // important for port finding when using multiple HttpServers 57 | protected static final Lock STARTUP_LOCK = new ReentrantLock(); 58 | 59 | @Override 60 | public void stop() { 61 | server.stop(0); 62 | context.info(() -> "[{}] port [{}] stopped", name(), (server == null ? null : server.getAddress().getPort())); 63 | server = null; 64 | } 65 | 66 | @Override 67 | public void start() { 68 | STARTUP_LOCK.lock(); 69 | final int port = context.asIntOpt(CONFIG_SERVICE_HTTP_PORT).filter(p -> p > 0).orElseGet(() -> nextFreePort(8080)); 70 | context.put(CONFIG_SERVICE_HTTP_PORT, port); 71 | handleHttps(context); 72 | try { 73 | server = com.sun.net.httpserver.HttpServer.create(new InetSocketAddress(port), 0); 74 | server.setExecutor(GLOBAL_THREAD_POOL); 75 | server.createContext("/", exchange -> { 76 | final HttpObject request = new HttpObject(exchange); 77 | try { 78 | final AtomicBoolean internalError = new AtomicBoolean(false); 79 | context.sendEventR(EVENT_HTTP_REQUEST, () -> request).peek(setError(internalError)).responseOpt(HttpObject.class).ifPresentOrElse( 80 | response -> sendResponse(exchange, request, response), 81 | () -> context.sendEventR(EVENT_HTTP_REQUEST_UNHANDLED, () -> request).responseOpt(HttpObject.class).ifPresentOrElse( 82 | response -> sendResponse(exchange, request, response), 83 | () -> sendResponse(exchange, request, new HttpObject() 84 | .statusCode(internalError.get() ? 500 : 404) 85 | .bodyT(new LinkedTypeMap().putR("message", internalError.get() ? "Internal Server Error" : "Not Found").putR("timestamp", System.currentTimeMillis())) 86 | .contentType(ContentType.APPLICATION_PROBLEM_JSON)) 87 | ) 88 | ); 89 | } catch (final Exception e) { 90 | context.sendEventR(EVENT_APP_UNHANDLED, () -> new Unhandled(context, request, e)).responseOpt(HttpObject.class).ifPresentOrElse( 91 | response -> sendResponse(exchange, request, response), 92 | () -> new HttpObject().statusCode(500).body("Internal Server Error".getBytes()).contentType(ContentType.APPLICATION_PROBLEM_JSON) 93 | ); 94 | } 95 | }); 96 | server.start(); 97 | context.info(() -> "[{}] starting on port [{}]", name(), port); 98 | context.asBooleanOpt(CONFIG_SERVICE_HTTP_CLIENT).ifPresent(start -> context.runAwait(new HttpClient())); 99 | } catch (final IOException e) { 100 | context.error(e, () -> "[{}] failed to start with port [{}]", name(), port); 101 | } finally { 102 | STARTUP_LOCK.unlock(); 103 | } 104 | } 105 | 106 | @Override 107 | public void onEvent(final Event event) { 108 | } 109 | 110 | @Override 111 | public void configure(final TypeMapI configs, final TypeMapI merged) { 112 | 113 | } 114 | 115 | private static void handleHttps(final Context context) { 116 | //TODO: add option for HTTPS 117 | //TODO: handle certificates 118 | final Type crt = context.asStringOpt(String.class, "app.https.crt.path"); 119 | final Type key = context.asStringOpt(String.class, "app.https.key.path"); 120 | if (crt.isPresent() && key.isPresent()) { 121 | // // Load the certificate 122 | // CertificateFactory cf = CertificateFactory.getInstance("X.509"); 123 | // X509Certificate cert = (X509Certificate) cf.generateCertificate(new FileInputStream(crtFilePath)); 124 | // 125 | // // Load the private key 126 | // final byte[] keyBytes = Files.readAllBytes(Paths.get(keyFilePath)); 127 | // final PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes); 128 | // KeyFactory kf = KeyFactory.getInstance("RSA"); // TODO: TRY & ERROR loop for all Algorithm 129 | // final PrivateKey privateKey = kf.generatePrivate(spec); 130 | // 131 | // // Create a keystore 132 | // final KeyStore keyStore = KeyStore.getInstance("JKS"); 133 | // keyStore.load(null); 134 | // keyStore.setCertificateEntry("cert", cert); 135 | // keyStore.setKeyEntry("key", privateKey, "password".toCharArray(), new Certificate[]{cert}); 136 | // 137 | // // Initialize the SSL context 138 | // final KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); 139 | // kmf.init(keyStore, "password".toCharArray()); 140 | // final SSLContext sslContext = SSLContext.getInstance("TLS"); 141 | // sslContext.init(kmf.getKeyManagers(), null, null); 142 | // 143 | // // Set up the HTTPS server 144 | // HttpsServer httpsServer = HttpsServer.create(new InetSocketAddress(port), 0); 145 | // httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext)); 146 | // this.server = httpsServer; 147 | } 148 | } 149 | 150 | @Override 151 | public Object onFailure(final Event error) { 152 | return null; 153 | } 154 | 155 | protected void sendResponse(final HttpExchange exchange, final HttpObject request, final HttpObject response) { 156 | try { 157 | byte[] body = response.body(); 158 | final int statusCode = response.statusCode() > -1 && response.statusCode() < 600 ? response.statusCode() : 200; 159 | final Optional encoding = request.acceptEncodings().stream().filter(s -> s.equals("gzip") || s.equals("deflate")).findFirst(); 160 | response.headerMap().asMap(String.class, value -> collectionOf(value, String.class)).forEach((key, value) -> exchange.getResponseHeaders().put(key, value)); 161 | response.computedHeaders(false).forEach((key, value) -> exchange.getResponseHeaders().put(key, value)); 162 | 163 | if (encoding.isPresent()) 164 | body = encodeBody(body, encoding.get()); 165 | exchange.getResponseHeaders().put(HttpHeaders.CONTENT_ENCODING, List.of(encoding.orElse("identity"))); 166 | exchange.sendResponseHeaders(statusCode, body.length); 167 | try (final OutputStream os = exchange.getResponseBody()) { 168 | os.write(body); 169 | } 170 | } catch (final IOException ignored) { 171 | // Response was already sent 172 | } 173 | } 174 | 175 | protected byte[] encodeBody(final byte[] body, final String contentEncoding) { 176 | if ("gzip".equalsIgnoreCase(contentEncoding)) { 177 | return NanoUtils.encodeGzip(body); 178 | } else if ("deflate".equalsIgnoreCase(contentEncoding)) { 179 | return NanoUtils.encodeDeflate(body); 180 | } 181 | return body; 182 | } 183 | 184 | public static int nextFreePort(final int startPort) { 185 | for (int i = 0; i < 1024; i++) { 186 | final int port = i + (Math.max(startPort, 1)); 187 | if (!isPortInUse(port)) { 188 | return port; 189 | } 190 | } 191 | throw new IllegalStateException("Could not find any free port"); 192 | } 193 | 194 | public static boolean isPortInUse(final int portNumber) { 195 | try { 196 | new Socket("localhost", portNumber).close(); 197 | return true; 198 | } catch (final Exception e) { 199 | return false; 200 | } 201 | } 202 | 203 | public static Consumer setError(final AtomicBoolean internalError) { 204 | return event -> { 205 | if (event.error() != null) { 206 | internalError.set(true); 207 | } 208 | }; 209 | } 210 | } 211 | 212 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/http/model/ContentType.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.http.model; 2 | 3 | // TODO: autogenerate from org.apache.http.entity.ContentType 4 | @SuppressWarnings("unused") 5 | public enum ContentType { 6 | 7 | APPLICATION_ATOM_XML("application/atom+xml"), 8 | APPLICATION_FORM_URLENCODED("application/x-www-form-urlencoded"), 9 | APPLICATION_JSON("application/json"), 10 | APPLICATION_PDF("application/pdf"), 11 | APPLICATION_OCTET_STREAM("application/octet-stream"), 12 | APPLICATION_SOAP_XML("application/soap+xml"), 13 | APPLICATION_SVG_XML("application/svg+xml"), 14 | APPLICATION_XHTML_XML("application/xhtml+xml"), 15 | APPLICATION_XML("application/xml"), 16 | APPLICATION_PROBLEM_JSON("application/problem+json"), 17 | APPLICATION_PROBLEM_XML("application/problem+xml"), 18 | IMAGE_BMP("image/bmp"), 19 | IMAGE_GIF("image/gif"), 20 | IMAGE_JPEG("image/jpeg"), 21 | IMAGE_PNG("image/png"), 22 | IMAGE_SVG("image/svg+xml"), 23 | IMAGE_TIFF("image/tiff"), 24 | IMAGE_WEBP("image/webp"), 25 | MULTIPART_FORM_DATA("multipart/form-data"), 26 | TEXT_HTML("text/html"), 27 | TEXT_PLAIN("text/plain"), 28 | TEXT_XML("text/xml"), 29 | WILDCARD("*/*"), 30 | AUDIO_MPEG("audio/mpeg"), 31 | VIDEO_MP4("video/mp4"); 32 | 33 | private final String value; 34 | 35 | ContentType(final String value) { 36 | this.value = value; 37 | } 38 | 39 | public String value() { 40 | return value; 41 | } 42 | 43 | public static ContentType fromValue(final String value) { 44 | for (final ContentType type : values()) { 45 | if (type.value().equalsIgnoreCase(value) || type.name().equalsIgnoreCase(value)) { 46 | return type; 47 | } 48 | } 49 | return null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/http/model/HttpHeaders.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.http.model; 2 | 3 | /** 4 | * Common HTTP headers - used in requests and responses. 5 | * use always lowercase to avoid confusions and case sensitivity issues - makes lives easier. 6 | */ 7 | @SuppressWarnings("unused") 8 | public class HttpHeaders { 9 | 10 | private HttpHeaders() { 11 | } 12 | 13 | public static final String ACCEPT = "accept"; 14 | public static final String ACCEPT_CHARSET = "accept-charset"; 15 | public static final String ACCEPT_ENCODING = "accept-encoding"; 16 | public static final String ACCEPT_LANGUAGE = "accept-language"; 17 | public static final String ACCEPT_RANGES = "accept-ranges"; 18 | public static final String AGE = "age"; 19 | public static final String ALLOW = "allow"; 20 | public static final String AUTHORIZATION = "authorization"; 21 | public static final String CACHE_CONTROL = "cache-control"; 22 | public static final String CONNECTION = "connection"; 23 | public static final String CONTENT_ENCODING = "content-encoding"; 24 | public static final String CONTENT_LANGUAGE = "content-language"; 25 | public static final String CONTENT_LENGTH = "content-length"; 26 | public static final String CONTENT_LOCATION = "content-location"; 27 | public static final String CONTENT_MD5 = "content-md5"; 28 | public static final String CONTENT_RANGE = "content-range"; 29 | public static final String CONTENT_TYPE = "content-type"; 30 | public static final String DATE = "date"; 31 | public static final String DAV = "dav"; 32 | public static final String DEPTH = "depth"; 33 | public static final String DESTINATION = "destination"; 34 | public static final String ETAG = "etag"; 35 | public static final String EXPECT = "expect"; 36 | public static final String EXPIRES = "expires"; 37 | public static final String FROM = "from"; 38 | public static final String HOST = "host"; 39 | public static final String IF = "if"; 40 | public static final String IF_MATCH = "if-match"; 41 | public static final String IF_MODIFIED_SINCE = "if-modified-since"; 42 | public static final String IF_NONE_MATCH = "if-none-match"; 43 | public static final String IF_RANGE = "if-range"; 44 | public static final String IF_UNMODIFIED_SINCE = "if-unmodified-since"; 45 | public static final String LAST_MODIFIED = "last-modified"; 46 | public static final String LOCATION = "location"; 47 | public static final String LOCK_TOKEN = "lock-token"; 48 | public static final String MAX_FORWARDS = "max-forwards"; 49 | public static final String OVERWRITE = "overwrite"; 50 | public static final String PRAGMA = "pragma"; 51 | public static final String PROXY_AUTHENTICATE = "proxy-authenticate"; 52 | public static final String PROXY_AUTHORIZATION = "proxy-authorization"; 53 | public static final String RANGE = "range"; 54 | public static final String REFERER = "referer"; 55 | public static final String RETRY_AFTER = "retry-after"; 56 | public static final String SERVER = "server"; 57 | public static final String STATUS_URI = "status-uri"; 58 | public static final String TE = "te"; 59 | public static final String TIMEOUT = "timeout"; 60 | public static final String TRAILER = "trailer"; 61 | public static final String TRANSFER_ENCODING = "transfer-encoding"; 62 | public static final String UPGRADE = "upgrade"; 63 | public static final String USER_AGENT = "user-agent"; 64 | public static final String VARY = "vary"; 65 | public static final String VIA = "via"; 66 | public static final String WARNING = "warning"; 67 | public static final String WWW_AUTHENTICATE = "www-authenticate"; 68 | public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "access-control-allow-origin"; 69 | public static final String ACCESS_CONTROL_ALLOW_METHODS = "access-control-allow-methods"; 70 | public static final String ACCESS_CONTROL_ALLOW_HEADERS = "access-control-allow-headers"; 71 | public static final String ACCESS_CONTROL_MAX_AGE = "access-control-max-age"; 72 | public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "access-control-allow-credentials"; 73 | public static final String ACCESS_CONTROL_REQUEST_METHOD = "access-control-request-method"; 74 | public static final String ACCESS_CONTROL_REQUEST_HEADERS = "access-control-request-headers"; 75 | public static final String ORIGIN = "origin"; 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/http/model/HttpMethod.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.http.model; 2 | 3 | import java.util.Arrays; 4 | 5 | @SuppressWarnings("unused") 6 | public enum HttpMethod { 7 | GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; 8 | 9 | public static HttpMethod httpMethodOf(final String method) { 10 | return Arrays.stream(HttpMethod.values()) 11 | .filter(httpMethod -> httpMethod.name().equalsIgnoreCase(method)) 12 | .findFirst().orElse(null); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/logging/LogFormatRegister.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.logging; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | import java.util.function.Supplier; 6 | import java.util.logging.Formatter; 7 | 8 | @SuppressWarnings("unused") 9 | public class LogFormatRegister { 10 | 11 | @SuppressWarnings("java:S2386") 12 | public static final Map LOG_FORMATTERS = new ConcurrentHashMap<>(); 13 | 14 | public static void registerLogFormatter(final String id, final Formatter formatter) { 15 | LOG_FORMATTERS.put(id, formatter); 16 | } 17 | 18 | public static Formatter getLogFormatter(final String id) { 19 | if("console".equals(id)){ 20 | return getLogFormatter(id, LogFormatterConsole::new); 21 | } else if ("json".equals(id)){ 22 | return getLogFormatter(id, LogFormatterJson::new); 23 | } else { 24 | return getLogFormatter(id, LogFormatterConsole::new); 25 | } 26 | } 27 | 28 | public static Formatter getLogFormatter(final String id, final Supplier orRegister) { 29 | return LOG_FORMATTERS.computeIfAbsent(id, formatter -> orRegister.get()); 30 | } 31 | 32 | private LogFormatRegister() { 33 | // static util class 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/logging/LogFormatterConsole.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.logging; 2 | 3 | import org.nanonative.nano.helper.NanoUtils; 4 | import org.nanonative.nano.services.logging.model.LogLevel; 5 | 6 | import java.text.SimpleDateFormat; 7 | import java.util.Arrays; 8 | import java.util.Date; 9 | import java.util.logging.Formatter; 10 | import java.util.logging.LogRecord; 11 | 12 | import static berlin.yuna.typemap.logic.TypeConverter.convertObj; 13 | import static org.nanonative.nano.services.logging.LogService.MAX_LOG_NAME_LENGTH; 14 | import static org.nanonative.nano.services.logging.model.LogLevel.nanoLogLevelOf; 15 | 16 | /** 17 | * Formatter for logging messages to the console. 18 | *

19 | * This formatter provides a consistent log format comprising the timestamp, log level, logger name, and message. 20 | * The formatted log entries are easy to read and allow for quick scanning of log files. The log format is as follows: 21 | *

22 |  * [Timestamp] [Log Level] [Logger Name] - Message
23 |  * 
24 | *

25 | * The formatter supports parameterized messages, allowing insertion of values at runtime. To include dynamic content in your log messages, 26 | * use placeholders '{}' for automatic replacement or '%s' for manual specification. Each placeholder will be replaced with corresponding 27 | * parameters provided in the logging method call. 28 | *

29 | * Usage Example: 30 | *

31 |  * context.info(() -> throwable, "Processed records - success: [{}], failure: [%s], ignored; [{2}]", successCount, failureCount, ignoreCount);
32 |  * 
33 | * In this example, 'successCount' replaces the first '{}' placeholder, 'failureCount' replaces the '%s' placeholder and 'ignoreCount' replaces the last [{2}] placeholder. 34 | *

35 | *

36 | * Note: The formatter also handles exceptions by appending the stack trace to the log entry, should an exception be thrown during execution. 37 | *

38 | */ 39 | public class LogFormatterConsole extends Formatter { 40 | protected final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 41 | protected final int paddingLogLevel = Arrays.stream(LogLevel.values()).map(LogLevel::toString).mapToInt(String::length).max().orElse(5); 42 | 43 | /** 44 | * Format a LogRecord into a string representation. 45 | * 46 | * @param logRecord the log record to be formatted. 47 | * @return a formatted log string. 48 | */ 49 | @SuppressWarnings("java:S3457") 50 | @Override 51 | public String format(final LogRecord logRecord) { 52 | final StringBuilder formattedLog = new StringBuilder(); 53 | formattedLog.append("[") 54 | .append(dateFormat.format(new Date(logRecord.getMillis()))) 55 | .append("] [") 56 | .append(String.format("%-" + paddingLogLevel + "s", nanoLogLevelOf(logRecord.getLevel()))) 57 | .append("] [") 58 | .append(formatLoggerName(logRecord)) 59 | .append("] - ") 60 | .append(applyCustomFormat(formatMessage(logRecord), logRecord.getParameters())) 61 | .append(NanoUtils.LINE_SEPARATOR); 62 | if (logRecord.getThrown() != null) { 63 | formattedLog.append(convertObj(logRecord.getThrown(), String.class)).append(NanoUtils.LINE_SEPARATOR); 64 | } 65 | return formattedLog.toString(); 66 | } 67 | 68 | @SuppressWarnings("java:S3457") 69 | protected static String formatLoggerName(final LogRecord logRecord) { 70 | final int dot = logRecord.getLoggerName().lastIndexOf("."); 71 | return String.format("%-" + MAX_LOG_NAME_LENGTH.get() + "s", (dot != -1 ? logRecord.getLoggerName().substring(dot + 1) : logRecord.getLoggerName())); 72 | } 73 | 74 | /** 75 | * Replacing placeholders with parameters. 76 | * 77 | * @param message the message to be formatted. 78 | * @param params the parameters for the message. 79 | * @return a string with the formatted message. 80 | */ 81 | protected static String applyCustomFormat(final String message, final Object... params) { 82 | if (message != null && params != null && params.length > 0) { 83 | final String result = message.replace("{}", "%s"); 84 | return String.format(result, params); 85 | } 86 | return message; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/logging/LogFormatterJson.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.logging; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Date; 5 | import java.util.Map; 6 | import java.util.TreeMap; 7 | import java.util.logging.Formatter; 8 | import java.util.logging.LogRecord; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | import java.util.stream.Collectors; 12 | 13 | import static berlin.yuna.typemap.logic.TypeConverter.convertObj; 14 | import static org.nanonative.nano.helper.NanoUtils.LINE_SEPARATOR; 15 | import static org.nanonative.nano.services.logging.model.LogLevel.nanoLogLevelOf; 16 | 17 | /** 18 | * A log formatter that outputs log records in JSON format. 19 | *

20 | * This formatter structures log messages into JSON objects, which is beneficial for systems that ingest log data for analysis, 21 | * allowing for easy parsing and structured querying of log data. 22 | *

23 | * Usage Example: 24 | *

 25 |  * context.info(() -> throwable, "Processed records - success: [{}], failure: [%s], ignored; [{2}]", successCount, failureCount, ignoreCount, Map.of("username", "yuna"));
 26 |  * 
27 | * In this example, 'successCount' replaces the first '{}' placeholder, 'failureCount' replaces the '%s' placeholder and 'ignoreCount' replaces the last [{2}] placeholder. 28 | * The formatter will convert the log into a JSON line. 29 | * The map's keys and values becoming part of the JSON structure. 30 | * A key value map is not really needed as the keys and values from the messages itself becomes a part of the JSON structure as well. This makes it easier to switch between console and json logging. 31 | *

32 | *

33 | * Additionally, it supports automatic key-value extraction from the log message itself, enabling inline parameterization. 34 | * The extracted keys and values are also included in the JSON output. 35 | *

36 | *

37 | * The formatter handles exceptions by appending a "error" field with the exception message to the JSON log entry. 38 | *

39 | */ 40 | public class LogFormatterJson extends Formatter { 41 | protected final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 42 | protected static final Pattern MESSAGE_KEY_VALUE_PATTERN = Pattern.compile("(\\w+):?\\s*\\[\\{\\}]"); 43 | 44 | /** 45 | * Formats a log record into a JSON string. 46 | * 47 | * @param logRecord The log record to format. 48 | * @return The log record formatted as a JSON string. 49 | */ 50 | @Override 51 | public String format(final LogRecord logRecord) { 52 | final Object[] params = logRecord.getParameters(); 53 | final Map jsonMap = new TreeMap<>(); 54 | final String message = LogFormatterConsole.applyCustomFormat(logRecord.getMessage(), params); 55 | final int dot = logRecord.getLoggerName().lastIndexOf("."); 56 | extractKeyValuesFromMessage(jsonMap, logRecord.getMessage(), params); 57 | addJsonEntries(jsonMap, params); 58 | 59 | putEntry(jsonMap, "message", message); 60 | putEntry(jsonMap, "timestamp", dateFormat.format(new Date(logRecord.getMillis()))); 61 | putEntry(jsonMap, "level", nanoLogLevelOf(logRecord.getLevel())); 62 | putEntry(jsonMap, "package", dot != -1 ? logRecord.getLoggerName().substring(0, dot) : ""); 63 | putEntry(jsonMap, "logger", dot != -1 ? logRecord.getLoggerName().substring(dot + 1) : logRecord.getLoggerName()); 64 | if (logRecord.getThrown() != null) { 65 | putEntry(jsonMap, "error", jsonEscape(convertObj(logRecord.getThrown(), String.class))); 66 | } 67 | return "{" + jsonMap.entrySet().stream().map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"").collect(Collectors.joining(",")) + "}" + LINE_SEPARATOR; 68 | } 69 | 70 | /** 71 | * Extracts key-value pairs from the message and stores them in a map. 72 | * 73 | * @param jsonMap The map to store the key-value pairs. 74 | * @param message The log message. 75 | * @param params The parameters for the log message. 76 | */ 77 | protected void extractKeyValuesFromMessage(final Map jsonMap, final String message, final Object[] params) { 78 | final Matcher matcher = MESSAGE_KEY_VALUE_PATTERN.matcher(message); 79 | 80 | int index = 0; 81 | while (matcher.find() && params != null && index < params.length) { 82 | putEntry(jsonMap, matcher.group(1), params[index]); 83 | index++; 84 | } 85 | } 86 | 87 | /** 88 | * Adds additional key-value pairs to the map. 89 | * 90 | * @param jsonMap The map to store the key-value pairs. 91 | * @param params The parameters for the log message. 92 | */ 93 | protected void addJsonEntries(final Map jsonMap, final Object[] params) { 94 | if (params != null) { 95 | for (final Object param : params) { 96 | if (param instanceof final Map map) { 97 | for (final Map.Entry entry : map.entrySet()) { 98 | putEntry(jsonMap, entry.getKey(), entry.getValue()); 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | /** 106 | * Adds escaped and converted key-value pairs to the map. 107 | * 108 | * @param jsonMap The map to store the key-value pairs. 109 | * @param key key 110 | * @param value value 111 | */ 112 | protected void putEntry(final Map jsonMap, final Object key, final Object value) { 113 | jsonMap.put(jsonEscape(convertObj(key, String.class)), jsonEscape(convertObj(value, String.class))); 114 | } 115 | 116 | /** 117 | * Escapes special characters for JSON compatibility. 118 | * 119 | * @param value The object to escape. 120 | * @return The escaped string. 121 | */ 122 | protected String jsonEscape(final Object value) { 123 | if (value == null) return null; 124 | final String strValue = value.toString(); 125 | final StringBuilder sb = new StringBuilder(); 126 | for (int i = 0; i < strValue.length(); i++) { 127 | final char c = strValue.charAt(i); 128 | switch (c) { 129 | case '"': 130 | sb.append("\\\""); 131 | break; 132 | case '\\': 133 | sb.append("\\\\"); 134 | break; 135 | case '/': 136 | sb.append("\\/"); 137 | break; 138 | case '\b': 139 | sb.append("\\b"); 140 | break; 141 | case '\f': 142 | sb.append("\\f"); 143 | break; 144 | case '\n': 145 | sb.append("\\n"); 146 | break; 147 | case '\r': 148 | sb.append("\\r"); 149 | break; 150 | case '\t': 151 | sb.append("\\t"); 152 | break; 153 | default: 154 | sb.append(c); 155 | } 156 | } 157 | return sb.toString(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/logging/LogService.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.logging; 2 | 3 | import berlin.yuna.typemap.model.LinkedTypeMap; 4 | import berlin.yuna.typemap.model.TypeMapI; 5 | import org.nanonative.nano.core.model.Service; 6 | import org.nanonative.nano.helper.event.model.Event; 7 | import org.nanonative.nano.services.logging.model.LogLevel; 8 | 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | import java.util.function.Supplier; 13 | import java.util.logging.Formatter; 14 | import java.util.logging.Level; 15 | import java.util.logging.LogRecord; 16 | 17 | import static org.nanonative.nano.helper.config.ConfigRegister.registerConfig; 18 | import static org.nanonative.nano.helper.event.EventChannelRegister.registerChannelId; 19 | 20 | public class LogService extends Service { 21 | 22 | // CONFIG 23 | public static final String CONFIG_LOG_LEVEL = registerConfig("app_log_level", "Log level for the application (see " + LogLevel.class.getSimpleName() + ")"); 24 | public static final String CONFIG_LOG_FORMATTER = registerConfig("app_log_formatter", "Log formatter (see " + LogFormatRegister.class.getSimpleName() + ")"); 25 | public static final String CONFIG_LOG_EXCLUDE_PATTERNS = registerConfig("app_log_excludes", "Exclude patterns for logger names"); 26 | 27 | // CHANNEL 28 | public static final int EVENT_LOGGING = registerChannelId("EVENT_LOGGING"); 29 | 30 | public static final AtomicInteger MAX_LOG_NAME_LENGTH = new AtomicInteger(10); 31 | protected List excludePatterns; 32 | protected Formatter logFormatter = new LogFormatterConsole(); 33 | protected Level level = Level.FINE; 34 | 35 | @Override 36 | public void start() { 37 | // nothing to do 38 | } 39 | 40 | @Override 41 | public void stop() { 42 | // nothing to do 43 | } 44 | 45 | @Override 46 | public Object onFailure(final Event error) { 47 | return null; 48 | } 49 | 50 | @Override 51 | public void onEvent(final Event event) { 52 | event.filter(this::isLoggable).ifPresent( 53 | event1 -> context.run(() -> event1.ifPresentAck(EVENT_LOGGING, LogRecord.class, this::log)) 54 | ); 55 | } 56 | 57 | @Override 58 | public void configure(final TypeMapI configs, final TypeMapI merged) { 59 | merged.asOpt(LogLevel.class, CONFIG_LOG_LEVEL).map(LogLevel::toJavaLogLevel).ifPresent(this::level); 60 | merged.asOpt(Formatter.class, CONFIG_LOG_FORMATTER).ifPresent(this::formatter); 61 | excludePatterns = merged.asStringOpt(CONFIG_LOG_EXCLUDE_PATTERNS).map(patterns -> Arrays.stream(patterns.split(",")).map(String::trim).toList()).orElseGet(List::of); 62 | } 63 | 64 | public synchronized LogService level(final Level level) { 65 | this.level = level; 66 | return this; 67 | } 68 | 69 | public Level level() { 70 | return level; 71 | } 72 | 73 | public synchronized LogService formatter(final Formatter formatter) { 74 | this.logFormatter = formatter; 75 | return this; 76 | } 77 | 78 | public Formatter formatter() { 79 | return this.logFormatter; 80 | } 81 | 82 | public void log(final Supplier logRecord) { 83 | context.run(() -> log(logRecord.get())); 84 | } 85 | 86 | @SuppressWarnings("SameReturnValue") 87 | protected boolean log(final LogRecord logRecord) { 88 | context.run(() -> { 89 | final String formattedMessage = logFormatter.format(logRecord); 90 | if (logRecord.getLevel().intValue() < Level.WARNING.intValue()) { 91 | System.out.print(formattedMessage); 92 | } else { 93 | System.err.print(formattedMessage); 94 | } 95 | }); 96 | return true; 97 | } 98 | 99 | private boolean isLoggable(final Event event) { 100 | return event.channelId() == EVENT_LOGGING && event.asOpt(Level.class, "level") 101 | .filter(level -> level.intValue() > this.level.intValue()) 102 | .map(level -> event.asString("name")) 103 | .filter(name -> excludePatterns == null || excludePatterns.stream().noneMatch(name::contains)) 104 | .map(name -> event.acknowledge()) 105 | .isPresent(); 106 | } 107 | 108 | @Override 109 | public String toString() { 110 | return new LinkedTypeMap() 111 | .putR("name", this.getClass().getSimpleName()) 112 | .putR("level", level) 113 | .putR("isReady", isReady.get()) 114 | .putR("context", context.size()) 115 | .putR("excludePatterns", excludePatterns != null ? excludePatterns.size() : 0) 116 | .putR("logFormatter", logFormatter.getClass().getSimpleName()) 117 | .putR("MAX_LOG_NAME_LENGTH", MAX_LOG_NAME_LENGTH.get()) 118 | .toJson(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/logging/model/LogLevel.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.logging.model; 2 | 3 | import org.nanonative.nano.helper.NanoUtils; 4 | 5 | import java.util.Arrays; 6 | import java.util.logging.Level; 7 | 8 | public enum LogLevel { 9 | OFF(Level.OFF), 10 | FATAL(Level.SEVERE), 11 | ERROR(Level.SEVERE), 12 | WARN(Level.WARNING), 13 | INFO(Level.INFO), 14 | DEBUG(Level.FINE), 15 | TRACE(Level.FINER), 16 | ALL(Level.ALL); // OR FINEST? 17 | 18 | private final Level javaLogLevel; 19 | 20 | LogLevel(final Level javaLogLevel) { 21 | this.javaLogLevel = javaLogLevel; 22 | } 23 | 24 | public java.util.logging.Level toJavaLogLevel() { 25 | return javaLogLevel; 26 | } 27 | 28 | public static LogLevel nanoLogLevelOf(final Level level) { 29 | return Arrays.stream(LogLevel.values()).filter(simpleLogLevel -> simpleLogLevel.javaLogLevel == level).findFirst().orElse(OFF); 30 | 31 | } 32 | 33 | public static LogLevel nanoLogLevelOf(final String level) { 34 | if (NanoUtils.hasText(level)) { 35 | // Nano log level 36 | for (final LogLevel logLevel : LogLevel.values()) { 37 | if (logLevel.toString().equalsIgnoreCase(level)) { 38 | return logLevel; 39 | } 40 | } 41 | 42 | // Java log level 43 | for (final LogLevel logLevel : LogLevel.values()) { 44 | if (logLevel.javaLogLevel.toString().equalsIgnoreCase(level)) { 45 | return logLevel; 46 | } 47 | } 48 | } 49 | return ALL; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/metric/model/MetricCache.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.metric.model; 2 | 3 | import java.util.Map; 4 | import java.util.TreeMap; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.atomic.AtomicLong; 7 | import java.util.stream.Collectors; 8 | 9 | import static java.util.Collections.emptyMap; 10 | import static java.util.Optional.ofNullable; 11 | 12 | @SuppressWarnings({"UnusedReturnValue"}) 13 | public class MetricCache { 14 | 15 | private final ConcurrentHashMap> counters = new ConcurrentHashMap<>(); 16 | private final ConcurrentHashMap> gauges = new ConcurrentHashMap<>(); 17 | private final ConcurrentHashMap> timers = new ConcurrentHashMap<>(); 18 | 19 | public record Metric(T value, TreeMap tags, String metricName) { 20 | } 21 | 22 | public Map> counters() { 23 | return counters; 24 | } 25 | 26 | public Map> gauges() { 27 | return gauges; 28 | } 29 | 30 | public Map> timers() { 31 | return timers; 32 | } 33 | 34 | public Map> sorted() { 35 | final TreeMap> result = new TreeMap<>(); 36 | result.putAll(counters); 37 | result.putAll(gauges); 38 | result.putAll(timers); 39 | return result; 40 | } 41 | 42 | public MetricCache counterIncrement(final String name) { 43 | return counterIncrement(name, null); 44 | } 45 | 46 | public MetricCache counterIncrement(final String name, final Map tags) { 47 | if (name != null) { 48 | final String id = sanitizeMetricName(name); 49 | final TreeMap sortedTags = new TreeMap<>(tags != null ? tags : emptyMap()); 50 | counters.computeIfAbsent(tags == null ? id : generateUniqueKey(id, sortedTags), key -> new Metric<>(new AtomicLong(), sortedTags, id)).value.incrementAndGet(); 51 | } 52 | return this; 53 | } 54 | 55 | public long counter(final String name) { 56 | return counter(name, null); 57 | } 58 | 59 | public long counter(final String name, final Map tags) { 60 | final String id = sanitizeMetricName(name); 61 | return ofNullable(counters.get(tags == null ? id : generateUniqueKey(id, new TreeMap<>(tags)))).map(Metric::value).map(AtomicLong::get).orElse(-1L); 62 | } 63 | 64 | public MetricCache gaugeSet(final String name, final double value) { 65 | return gaugeSet(name, value, null); 66 | } 67 | 68 | public MetricCache gaugeSet(final String name, final double value, final Map tags) { 69 | if (name != null && value > -1) { 70 | final String id = sanitizeMetricName(name); 71 | final TreeMap sortedTags = new TreeMap<>(tags != null ? tags : emptyMap()); 72 | gauges.put(tags == null ? id : generateUniqueKey(id, sortedTags), new Metric<>(value, sortedTags, id)); 73 | } 74 | return this; 75 | } 76 | 77 | public double gauge(final String name) { 78 | return gauge(name, null); 79 | } 80 | 81 | public double gauge(final String name, final Map tags) { 82 | final String id = sanitizeMetricName(name); 83 | return ofNullable(gauges.get(tags == null ? id : generateUniqueKey(id, new TreeMap<>(tags)))).map(Metric::value).orElse(-1d); 84 | } 85 | 86 | public MetricCache timerStart(final String name) { 87 | return timerStart(name, null); 88 | } 89 | 90 | public MetricCache timerStart(final String name, final Map tags) { 91 | if (name != null) { 92 | final String id = sanitizeMetricName(name); 93 | final TreeMap sortedTags = new TreeMap<>(tags != null ? tags : emptyMap()); 94 | timers.put(tags == null ? id : generateUniqueKey(id, sortedTags), new Metric<>(System.currentTimeMillis(), sortedTags, id)); 95 | } 96 | return this; 97 | } 98 | 99 | public MetricCache timerStop(final String name) { 100 | return timerStop(name, null); 101 | } 102 | 103 | public MetricCache timerStop(final String name, final Map tags) { 104 | if (name != null) { 105 | final String id = sanitizeMetricName(name); 106 | final TreeMap sortedTags = new TreeMap<>(tags != null ? tags : emptyMap()); 107 | timers.computeIfPresent(tags == null ? id : generateUniqueKey(id, sortedTags), (key, metric) -> new Metric<>(System.currentTimeMillis() - metric.value, sortedTags, id)); 108 | } 109 | return this; 110 | } 111 | 112 | public long timer(final String name) { 113 | return timer(name, null); 114 | } 115 | 116 | public long timer(final String name, final Map tags) { 117 | final String id = sanitizeMetricName(name); 118 | return ofNullable(timers.get(tags == null ? id : generateUniqueKey(id, new TreeMap<>(tags)))).map(Metric::value).orElse(-1L); 119 | } 120 | 121 | // Adjustments for metric formatting methods to use metric.metricName instead of the unique key 122 | public String prometheus() { 123 | final StringBuilder result = new StringBuilder(); 124 | sorted().forEach((id, metric) -> result.append(formatPrometheusMetric(metric))); 125 | return result.toString(); 126 | } 127 | 128 | // Example adjustment for the InfluxDB format 129 | public String influx() { 130 | final StringBuilder sb = new StringBuilder(); 131 | sorted().forEach((id, metric) -> sb.append(metric.metricName()).append(formatInfluxTags(metric.tags())).append(" value=").append(metric.value() instanceof final AtomicLong val ? val.get() : metric.value()).append("\n")); 132 | return sb.toString(); 133 | } 134 | 135 | public String dynatrace() { 136 | final StringBuilder sb = new StringBuilder(); 137 | sorted().forEach((id, metric) -> sb.append(formatDynatraceMetric(metric))); 138 | return sb.toString(); 139 | } 140 | 141 | public String wavefront() { 142 | final StringBuilder sb = new StringBuilder(); 143 | sorted().forEach((id, metric) -> sb.append(formatWavefrontMetric(metric))); 144 | return sb.toString(); 145 | } 146 | 147 | // Utility method for generating unique keys remains unchanged 148 | public String generateUniqueKey(final String name, final Map tags) { 149 | final String tagString = tags.entrySet().stream() 150 | .sorted(Map.Entry.comparingByKey()) 151 | .map(entry -> entry.getKey() + "=" + entry.getValue()) 152 | .reduce((t1, t2) -> t1 + "&" + t2).orElse(""); 153 | return name + "{" + tagString + "}"; 154 | } 155 | 156 | public String sanitizeMetricName(final String name) { 157 | return name == null ? "UNKNOWN.METRIC" : name.replaceAll("[^a-zA-Z0-9.]", ".").replace("..", ".").replaceAll("^\\.|\\.$", ""); 158 | } 159 | 160 | // Adjusted formatting methods to utilize metric.metricName 161 | private String formatPrometheusMetric(final Metric metric) { 162 | final String tagsString = metric.tags.entrySet().stream() 163 | .map(entry -> entry.getKey() + "=\"" + entry.getValue() + "\"") 164 | .reduce((t1, t2) -> t1 + "," + t2) 165 | .map(tags -> "{" + tags + "}").orElse(""); 166 | return metric.metricName.replace(".", "_") + tagsString + " " + metric.value + "\n"; 167 | } 168 | 169 | private String formatInfluxTags(final Map tags) { 170 | final StringBuilder tagsBuilder = new StringBuilder(); 171 | tags.forEach((key, value) -> tagsBuilder.append(",").append(key).append("=").append(value)); 172 | return tagsBuilder.toString(); 173 | } 174 | 175 | private String formatDynatraceMetric(final Metric metric) { 176 | // Adjusting for correct tag formatting in Dynatrace metrics 177 | final String dimensions = metric.tags.entrySet().stream() 178 | .map(entry -> entry.getKey() + "=" + entry.getValue()) 179 | .collect(Collectors.joining(",")); 180 | return metric.metricName + "," + dimensions + " " + metric.value + "\n"; 181 | } 182 | 183 | private String formatWavefrontMetric(final Metric metric) { 184 | // Wavefront format corrected for tag placement 185 | final String tags = metric.tags.entrySet().stream() 186 | .map(entry -> entry.getKey() + "=" + entry.getValue()) 187 | .collect(Collectors.joining(" ")); 188 | return metric.metricName + " " + metric.value + " source=nano " + tags + "\n"; 189 | } 190 | 191 | @Override 192 | public String toString() { 193 | return this.getClass().getSimpleName() + "{" + 194 | "counters=" + counters.size() + 195 | ", gauges=" + gauges.size() + 196 | ", timers=" + timers.size() + 197 | '}'; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/metric/model/MetricType.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.metric.model; 2 | 3 | public enum MetricType { 4 | 5 | COUNTER, 6 | GAUGE, 7 | TIMER_START, 8 | TIMER_END, 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/nanonative/nano/services/metric/model/MetricUpdate.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.metric.model; 2 | 3 | import java.util.Map; 4 | 5 | public record MetricUpdate(MetricType type, String name, Number value, Map tags) { 6 | } 7 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/core/config/TestConfig.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.config; 2 | 3 | import org.nanonative.nano.services.logging.model.LogLevel; 4 | 5 | import java.util.concurrent.CountDownLatch; 6 | import java.util.concurrent.atomic.AtomicReference; 7 | import java.util.function.Supplier; 8 | 9 | import static java.util.Optional.ofNullable; 10 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 11 | import static org.nanonative.nano.helper.NanoUtils.tryExecute; 12 | 13 | public class TestConfig { 14 | 15 | /** 16 | * Defines the log level for testing purposes, allowing for easy adjustment during debugging. 17 | * This can be particularly useful when trying to isolate or identify specific issues within tests. 18 | */ 19 | public static final LogLevel TEST_LOG_LEVEL = LogLevel.WARN; 20 | 21 | /** 22 | * Specifies the number of times tests should be repeated to ensure concurrency reliability. This setting aims to strike a balance between thorough testing and practical execution times. 23 | * It's advised to maintain this value around 100 repeats. Higher values might affect the reliability of timing-sensitive assertions due to the varying capabilities of different testing environments. 24 | *

25 | * This concurrency configuration supports the following objectives: 26 | * - Thread Safety: Ensures that components behave correctly when accessed by multiple threads simultaneously. 27 | * - Validates Performance Under Load: Confirms that the system can handle high levels of concurrency without significant performance degradation. 28 | * - Guarantees Correct Event Handling: Verifies that events are processed accurately and in order even when handled concurrently. 29 | * - Ensures Robustness and Stability: Checks for the resilience of the system under concurrent usage, ensuring it remains stable and performs consistently. 30 | * - Prepares for Real-World Scenarios: Mimics real-world application usage to ensure the system can handle concurrent operations effectively. 31 | * - Promotes Confidence in Security: Helps identify potential security vulnerabilities that could be exploited through concurrent execution. 32 | */ 33 | public static final int TEST_REPEAT = 128; 34 | public static final int TEST_TIMEOUT = 1024 + (int) (Math.sqrt(TEST_REPEAT) * 50); 35 | 36 | public static boolean await(final CountDownLatch latch) throws InterruptedException { 37 | return latch.await(TEST_TIMEOUT, MILLISECONDS); 38 | } 39 | 40 | public static T waitForNonNull(final Supplier waitFor) { 41 | return waitForNonNull(waitFor, 2000); 42 | } 43 | 44 | public static T waitForNonNull(final Supplier waitFor, final long timeoutMs) { 45 | final long startTime = System.currentTimeMillis(); 46 | final AtomicReference result = new AtomicReference<>(null); 47 | while (result.get() == null && (System.currentTimeMillis() - startTime) < timeoutMs) { 48 | ofNullable(waitFor.get()).ifPresentOrElse(result::set, () -> tryExecute(null, () -> Thread.sleep(100))); 49 | } 50 | return result.get(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/core/model/ConfigTest.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.nanonative.nano.helper.config.ConfigRegister.configDescriptionOf; 6 | import static org.nanonative.nano.helper.config.ConfigRegister.registerConfig; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class ConfigTest { 10 | 11 | @Test 12 | void testNewConfig() { 13 | assertThat(registerConfig("AA:BB.CC-DD+ff", "ABC123")).isEqualTo("aa_bb_cc_dd_ff"); 14 | assertThat(configDescriptionOf("AA:BB.CC-DD+ff")).isEqualTo("ABC123"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/core/model/ContextTest.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import org.junit.jupiter.api.RepeatedTest; 4 | import org.junit.jupiter.api.parallel.Execution; 5 | import org.junit.jupiter.api.parallel.ExecutionMode; 6 | import org.nanonative.nano.core.Nano; 7 | import org.nanonative.nano.core.config.TestConfig; 8 | import org.nanonative.nano.helper.event.EventChannelRegister; 9 | import org.nanonative.nano.helper.event.model.Event; 10 | import org.nanonative.nano.model.TestService; 11 | 12 | import java.time.LocalTime; 13 | import java.util.Map; 14 | import java.util.concurrent.CountDownLatch; 15 | import java.util.function.Consumer; 16 | 17 | import static java.time.temporal.ChronoUnit.MILLIS; 18 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 21 | import static org.nanonative.nano.core.config.TestConfig.TEST_TIMEOUT; 22 | import static org.nanonative.nano.core.model.Context.CONTEXT_CLASS_KEY; 23 | import static org.nanonative.nano.core.model.Context.CONTEXT_NANO_KEY; 24 | import static org.nanonative.nano.core.model.Context.CONTEXT_TRACE_ID_KEY; 25 | import static org.nanonative.nano.core.model.Context.EVENT_APP_HEARTBEAT; 26 | import static org.nanonative.nano.helper.NanoUtils.waitForCondition; 27 | import static org.nanonative.nano.services.logging.LogService.CONFIG_LOG_LEVEL; 28 | 29 | @SuppressWarnings("java:S5778") 30 | @Execution(ExecutionMode.CONCURRENT) 31 | class ContextTest { 32 | 33 | private static final int TEST_CHANNEL_ID = EventChannelRegister.registerChannelId("TEST_EVENT"); 34 | 35 | @RepeatedTest(TestConfig.TEST_REPEAT) 36 | void testNewContext_withNano() throws InterruptedException { 37 | final Nano nano = new Nano(Map.of(CONFIG_LOG_LEVEL, TestConfig.TEST_LOG_LEVEL)); 38 | final Context context = new Context(null, nano.getClass()).put(CONTEXT_NANO_KEY, nano); 39 | final Consumer myListener = event -> {}; 40 | assertContextBehaviour(context); 41 | 42 | // Verify event listener 43 | assertThat(nano.listeners().get(EVENT_APP_HEARTBEAT)).hasSize(1); 44 | assertThat(context.subscribeEvent(EVENT_APP_HEARTBEAT, myListener)).isEqualTo(context); 45 | assertThat(nano.listeners().get(EVENT_APP_HEARTBEAT)).hasSize(2); 46 | assertThat(context.unsubscribeEvent(EVENT_APP_HEARTBEAT, myListener)).isEqualTo(context); 47 | assertThat(nano.listeners().get(EVENT_APP_HEARTBEAT)).hasSize(1); 48 | 49 | // Verify event sending 50 | final CountDownLatch eventLatch = new CountDownLatch(4); 51 | final int channelId = context.registerChannelId("TEST_EVENT"); 52 | context.subscribeEvent(channelId, event -> eventLatch.countDown()); 53 | context.sendEvent(channelId, () -> "AA"); 54 | final Event event = context.sendEventR(channelId, () -> "BB"); 55 | context.broadcastEvent(channelId, () -> "CC"); 56 | context.broadcastEventR(channelId, () -> "DD"); 57 | assertThat(event).isNotNull(); 58 | assertThat(event.payload()).isEqualTo("BB"); 59 | assertThat(event.channel()).isEqualTo("TEST_EVENT"); 60 | assertThat(event.channelId()).isEqualTo(channelId); 61 | assertThat(event.context()).isEqualTo(context); 62 | assertThat(event.isAcknowledged()).isFalse(); 63 | assertThat(eventLatch.await(TEST_TIMEOUT, MILLISECONDS)).isTrue(); 64 | assertThat(eventLatch.getCount()).isZero(); 65 | assertThat(channelId).isEqualTo(TEST_CHANNEL_ID); 66 | 67 | // Verify services 68 | final TestService testService = new TestService(); 69 | assertThat(context.run(testService)).isEqualTo(context); 70 | assertThat(waitForCondition(() -> context.services().contains(testService), TEST_TIMEOUT)).isTrue(); 71 | assertThat(context.service(testService.getClass())).isEqualTo(testService); 72 | assertThat(context.services(TestService.class)).containsExactly(testService); 73 | 74 | // Verify schedule once 75 | final CountDownLatch latch1 = new CountDownLatch(1); 76 | context.run(latch1::countDown, 16, MILLISECONDS); 77 | assertThat(latch1.await(TEST_TIMEOUT, MILLISECONDS)) 78 | .withFailMessage("latch1 \nExpected: 1 \n Actual: " + latch1.getCount()) 79 | .isTrue(); 80 | 81 | // Verify schedule multiple time with stop 82 | final CountDownLatch latch2 = new CountDownLatch(4); 83 | context.run(latch2::countDown, 0, 16, MILLISECONDS); 84 | assertThat(latch2.await(TEST_TIMEOUT, MILLISECONDS)) 85 | .withFailMessage("latch2 \nExpected: 4 \n Actual: " + latch2.getCount()) 86 | .isTrue(); 87 | 88 | final CountDownLatch latch3 = new CountDownLatch(1); 89 | context.run(latch3::countDown, LocalTime.now().plus(16, MILLIS)); 90 | assertThat(latch3.await(TEST_TIMEOUT, MILLISECONDS)) 91 | .withFailMessage("latch3 \nExpected: 1 \n Actual: " + latch3.getCount()) 92 | .isTrue(); 93 | 94 | assertThat(nano.stop(this.getClass()).waitForStop().isReady()).isFalse(); 95 | } 96 | 97 | @RepeatedTest(TestConfig.TEST_REPEAT) 98 | void testNewContext_withoutNano() { 99 | final Context context = Context.createRootContext(ContextTest.class); 100 | final Consumer myListener = event -> {}; 101 | assertContextBehaviour(context); 102 | assertThatThrownBy(() -> context.subscribeEvent(EVENT_APP_HEARTBEAT, myListener)).isInstanceOf(NullPointerException.class); 103 | assertThatThrownBy(() -> context.unsubscribeEvent(EVENT_APP_HEARTBEAT, myListener)).isInstanceOf(NullPointerException.class); 104 | } 105 | 106 | @RepeatedTest(TestConfig.TEST_REPEAT) 107 | void testNewEmptyContext_withoutClass_willCreateRootContext() { 108 | final Context context = Context.createRootContext(ContextTest.class); 109 | assertContextBehaviour(context); 110 | final Context subContext = context.newContext(null); 111 | assertThat(subContext.traceId()).startsWith(ContextTest.class.getSimpleName() + "/"); 112 | } 113 | 114 | @RepeatedTest(TestConfig.TEST_REPEAT) 115 | void testRunHandled_withException() throws InterruptedException { 116 | final Context context = Context.createRootContext(ContextTest.class); 117 | final CountDownLatch latch = new CountDownLatch(2); 118 | assertContextBehaviour(context); 119 | assertThat(context.runHandled(unhandled -> latch.countDown(), () -> { 120 | throw new RuntimeException("Nothing to see here, just a test exception"); 121 | })).isEqualTo(context); 122 | assertThat(context.runReturnHandled(unhandled -> latch.countDown(), () -> { 123 | throw new RuntimeException("Nothing to see here, just a test exception"); 124 | })).isNotNull(); 125 | assertThat(latch.await(1000, MILLISECONDS)).isTrue(); 126 | assertThat(latch.getCount()).isZero(); 127 | } 128 | 129 | 130 | @RepeatedTest(TestConfig.TEST_REPEAT) 131 | void testRunAwaitHandled_withException() throws InterruptedException { 132 | final Context context = Context.createRootContext(ContextTest.class); 133 | final CountDownLatch latch = new CountDownLatch(1); 134 | assertContextBehaviour(context); 135 | context.runAwaitHandled(unhandled -> latch.countDown(), () -> { 136 | throw new RuntimeException("Nothing to see here, just a test exception"); 137 | }); 138 | assertThat(latch.await(1000, MILLISECONDS)).isTrue(); 139 | assertThat(latch.getCount()).isZero(); 140 | } 141 | 142 | @RepeatedTest(TestConfig.TEST_REPEAT) 143 | void testRunAwait_withException() { 144 | final Context context = Context.createRootContext(ContextTest.class); 145 | assertContextBehaviour(context); 146 | context.runAwait(() -> { 147 | throw new RuntimeException("Nothing to see here, just a test exception"); 148 | }); 149 | //TODO: create an unhandled element and check if the error was unhandled 150 | } 151 | 152 | private void assertContextBehaviour(final Context context) { 153 | assertThat(context) 154 | .hasSize(3) 155 | .containsKeys(CONTEXT_NANO_KEY, CONTEXT_TRACE_ID_KEY, CONTEXT_CLASS_KEY); 156 | 157 | context.put("AA", "BB"); 158 | assertThat(context) 159 | .hasSize(4) 160 | .containsKeys(CONTEXT_NANO_KEY, CONTEXT_TRACE_ID_KEY, CONTEXT_CLASS_KEY, CONTEXT_TRACE_ID_KEY) 161 | .containsKey("AA"); 162 | 163 | assertThat(context.newContext(this.getClass())) 164 | .hasSize(5) 165 | .containsKeys(CONTEXT_NANO_KEY, CONTEXT_TRACE_ID_KEY, CONTEXT_CLASS_KEY, CONTEXT_TRACE_ID_KEY) 166 | .containsKey("AA"); 167 | 168 | assertThat(context.newContext(this.getClass())) 169 | .hasSize(5) 170 | .containsKeys(CONTEXT_NANO_KEY, CONTEXT_TRACE_ID_KEY, CONTEXT_CLASS_KEY, CONTEXT_TRACE_ID_KEY); 171 | 172 | //Verify trace id is shared between contexts 173 | assertThat(context.newContext(this.getClass()).asList(CONTEXT_TRACE_ID_KEY)).hasSize(1).doesNotContain(context.traceId()); 174 | final Context subContext = context.newContext(this.getClass()); 175 | assertThat(subContext.asList(CONTEXT_TRACE_ID_KEY)).hasSize(1).doesNotContain(context.traceId()); 176 | assertThat(subContext.traceId()).isNotEqualTo(context.traceId()); 177 | assertThat(subContext.traceId(0)).isEqualTo(subContext.traceId()).isNotEqualTo(context.traceId()); 178 | assertThat(subContext.traceId(1)).isNotEqualTo(subContext.traceId()).isEqualTo(context.traceId()); 179 | assertThat(subContext.traceId(99)).isEqualTo(subContext.traceId()).isNotEqualTo(context.traceId()); 180 | assertThat(subContext.traceIds()).containsExactlyInAnyOrder(context.traceId(), subContext.traceId()); 181 | } 182 | 183 | @RepeatedTest(TestConfig.TEST_REPEAT) 184 | void testToString() { 185 | final Context context = Context.createRootContext(ContextTest.class); 186 | assertThat(context).hasToString("{\"size\":" + context.size() + ",\"class\":\"" + this.getClass().getSimpleName() + "\"}"); 187 | 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/core/model/LockedBooleanTest.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import org.nanonative.nano.core.config.TestConfig; 4 | import org.nanonative.nano.helper.LockedBoolean; 5 | import org.assertj.core.api.Assertions; 6 | import org.junit.jupiter.api.RepeatedTest; 7 | import org.junit.jupiter.api.parallel.Execution; 8 | import org.junit.jupiter.api.parallel.ExecutionMode; 9 | 10 | import java.util.concurrent.CountDownLatch; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | import java.util.stream.IntStream; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @Execution(ExecutionMode.CONCURRENT) 17 | class LockedBooleanTest { 18 | 19 | @RepeatedTest(TestConfig.TEST_REPEAT) 20 | void setState_shouldBlock() throws InterruptedException { 21 | final CountDownLatch latch = new CountDownLatch(10); 22 | final LockedBoolean lockedBoolean = new LockedBoolean(); 23 | 24 | IntStream.range(0, 10).parallel().forEach(i -> NanoThreadTest.TEST_EXECUTOR.submit(() -> { 25 | lockedBoolean.set(i % 2 == 0); 26 | latch.countDown(); 27 | })); 28 | 29 | Assertions.assertThat(TestConfig.await(latch)).isTrue(); 30 | } 31 | 32 | @RepeatedTest(TestConfig.TEST_REPEAT) 33 | void setState_withConsumer_shouldBlock() throws InterruptedException { 34 | final CountDownLatch latch = new CountDownLatch(10); 35 | final LockedBoolean lockedBoolean = new LockedBoolean(false); 36 | final AtomicInteger trueSetCounter = new AtomicInteger(0); 37 | final AtomicInteger falseSetCounter = new AtomicInteger(0); 38 | 39 | IntStream.range(0, 10).parallel().forEach(i -> NanoThreadTest.TEST_EXECUTOR.submit(() -> { 40 | if (i % 2 == 0) { 41 | lockedBoolean.set(true, state -> trueSetCounter.incrementAndGet()); 42 | } else { 43 | lockedBoolean.set(false, state -> falseSetCounter.incrementAndGet()); 44 | } 45 | latch.countDown(); 46 | })); 47 | 48 | Assertions.assertThat(TestConfig.await(latch)).isTrue(); 49 | assertThat(trueSetCounter.get() + falseSetCounter.get()).isEqualTo(10); 50 | } 51 | 52 | @RepeatedTest(TestConfig.TEST_REPEAT) 53 | void run_withConditionAndConsumer_shouldBlock() throws InterruptedException { 54 | final CountDownLatch latch = new CountDownLatch(10); 55 | final LockedBoolean lockedBoolean = new LockedBoolean(false); 56 | 57 | IntStream.range(0, 10).parallel().forEach(i -> NanoThreadTest.TEST_EXECUTOR.submit(() -> lockedBoolean.run(state -> latch.countDown()))); 58 | 59 | Assertions.assertThat(TestConfig.await(latch)).isTrue(); 60 | assertThat(lockedBoolean.get()).isFalse(); 61 | } 62 | 63 | @RepeatedTest(TestConfig.TEST_REPEAT) 64 | void run_withConsumer_shouldBlock() { 65 | final LockedBoolean lockedBoolean = new LockedBoolean(false); 66 | final AtomicInteger runCounter = new AtomicInteger(0); 67 | 68 | for (int i = 0; i < 20; i++) { 69 | final boolean odd = i % 2 == 0; 70 | lockedBoolean.run(odd, state -> runCounter.incrementAndGet()); 71 | } 72 | 73 | assertThat(lockedBoolean.get()).isFalse(); 74 | assertThat(runCounter).hasValue(10); 75 | } 76 | 77 | @RepeatedTest(TestConfig.TEST_REPEAT) 78 | void setState_withCondition_shouldBlock() { 79 | final LockedBoolean lockedBoolean = new LockedBoolean(true); 80 | 81 | for (int i = 0; i < 10; i++) { 82 | final boolean odd = i % 2 == 0; 83 | lockedBoolean.set(odd, !odd); 84 | } 85 | 86 | assertThat(lockedBoolean.get()).isTrue(); 87 | } 88 | 89 | @RepeatedTest(TestConfig.TEST_REPEAT) 90 | void setState_withConditionAndConsumer_shouldBlock() throws InterruptedException { 91 | final CountDownLatch latch = new CountDownLatch(10); 92 | final LockedBoolean lockedBoolean = new LockedBoolean(true); 93 | 94 | for (int i = 0; i < 10; i++) { 95 | final boolean odd = i % 2 == 0; 96 | lockedBoolean.set(odd, !odd, state -> latch.countDown()); 97 | } 98 | 99 | Assertions.assertThat(TestConfig.await(latch)).isTrue(); 100 | } 101 | 102 | @RepeatedTest(TestConfig.TEST_REPEAT) 103 | void toString_shouldBlock() throws InterruptedException { 104 | final CountDownLatch latch = new CountDownLatch(10); 105 | final LockedBoolean lockedBoolean = new LockedBoolean(); 106 | 107 | IntStream.range(0, 10).parallel().forEach(i -> NanoThreadTest.TEST_EXECUTOR.submit(() -> { 108 | assertThat(lockedBoolean.toString()).contains(LockedBoolean.class.getSimpleName() + "{" + "state=false}"); 109 | latch.countDown(); 110 | })); 111 | 112 | Assertions.assertThat(TestConfig.await(latch)).isTrue(); 113 | assertThat(lockedBoolean.get()).isFalse(); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/core/model/NanoThreadTest.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import org.junit.jupiter.api.RepeatedTest; 4 | import org.junit.jupiter.api.parallel.Execution; 5 | import org.junit.jupiter.api.parallel.ExecutionMode; 6 | 7 | import java.util.Arrays; 8 | import java.util.concurrent.CountDownLatch; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.TimeUnit; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | import java.util.stream.IntStream; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | import static org.nanonative.nano.core.config.TestConfig.TEST_REPEAT; 16 | import static org.nanonative.nano.core.model.NanoThread.GLOBAL_THREAD_POOL; 17 | import static org.nanonative.nano.core.model.NanoThread.activeCarrierThreads; 18 | import static org.nanonative.nano.core.model.NanoThread.activeNanoThreads; 19 | 20 | @Execution(ExecutionMode.CONCURRENT) 21 | class NanoThreadTest { 22 | 23 | public static final ExecutorService TEST_EXECUTOR = GLOBAL_THREAD_POOL; 24 | 25 | @RepeatedTest(TEST_REPEAT) 26 | void waitForAll_shouldBlockAndWait() { 27 | final AtomicInteger doneThreads = new AtomicInteger(0); 28 | final NanoThread[] threads = startConcurrentThreads(doneThreads); 29 | 30 | NanoThread.waitFor(threads); 31 | assertThat(doneThreads.get()).isEqualTo(TEST_REPEAT); 32 | } 33 | 34 | @RepeatedTest(TEST_REPEAT) 35 | void waitForAll_shouldNotBlock() throws InterruptedException { 36 | final CountDownLatch latch = new CountDownLatch(1); 37 | final AtomicInteger doneThreads = new AtomicInteger(0); 38 | final Runnable onComplete = latch::countDown; 39 | 40 | NanoThread.waitFor(onComplete, startConcurrentThreads(doneThreads)); 41 | assertThat(latch.await(TEST_REPEAT, TimeUnit.SECONDS)).isTrue(); 42 | assertThat(doneThreads.get()).isEqualTo(TEST_REPEAT); 43 | } 44 | 45 | @RepeatedTest(TEST_REPEAT) 46 | void waitFor_shouldBlockAndWait() { 47 | final AtomicInteger doneThreads = new AtomicInteger(0); 48 | final NanoThread[] threads = startConcurrentThreads(doneThreads); 49 | 50 | Arrays.stream(threads).forEach(NanoThread::await); 51 | assertThat(doneThreads.get()).isEqualTo(TEST_REPEAT); 52 | assertThat(Arrays.stream(threads).parallel().allMatch(NanoThread::isComplete)).isTrue(); 53 | } 54 | 55 | @RepeatedTest(TEST_REPEAT) 56 | void waitFor_shouldNotBlockWait() throws InterruptedException { 57 | final CountDownLatch latch = new CountDownLatch(TEST_REPEAT); 58 | final AtomicInteger doneThreads = new AtomicInteger(0); 59 | final NanoThread[] threads = startConcurrentThreads(doneThreads); 60 | 61 | Arrays.stream(threads).forEach(thread -> thread.await(latch::countDown)); 62 | assertThat(doneThreads.get()).isLessThan(TEST_REPEAT); 63 | assertThat(latch.await(TEST_REPEAT, TimeUnit.SECONDS)).isTrue(); 64 | assertThat(doneThreads.get()).isEqualTo(TEST_REPEAT); 65 | Arrays.stream(threads).parallel().forEach(thread -> assertThat(thread.isComplete()).isTrue()); 66 | Arrays.stream(threads).parallel().forEach(thread -> assertThat(thread.toString()).contains(NanoThread.class.getSimpleName() + "{onCompleteCallbacks=", ", isComplete=true}")); 67 | } 68 | 69 | @RepeatedTest(TEST_REPEAT) 70 | void activeNanoThreadCount() { 71 | new NanoThread().run(() -> null, () -> { 72 | assertThat(activeNanoThreads()).isPositive(); 73 | assertThat(activeCarrierThreads()).isPositive(); 74 | }); 75 | } 76 | 77 | @SuppressWarnings("java:S2925") 78 | private static NanoThread[] startConcurrentThreads(final AtomicInteger doneThreads) { 79 | return IntStream.range(0, TEST_REPEAT).parallel().mapToObj(i -> { 80 | final NanoThread thread = new NanoThread(); 81 | thread.run(null, () -> { 82 | try { 83 | Thread.sleep((long) (Math.random() * 100)); 84 | doneThreads.incrementAndGet(); 85 | } catch (final InterruptedException e) { 86 | Thread.currentThread().interrupt(); 87 | } 88 | }); 89 | return thread; 90 | }).toArray(NanoThread[]::new); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/core/model/SchedulerTest.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import org.junit.jupiter.api.RepeatedTest; 4 | 5 | import static org.nanonative.nano.core.config.TestConfig.TEST_REPEAT; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | class SchedulerTest { 9 | 10 | @RepeatedTest(TEST_REPEAT) 11 | void testNewScheduler() { 12 | final Scheduler scheduler = new Scheduler("test"); 13 | assertThat(scheduler).isNotNull(); 14 | assertThat(scheduler.id()).isEqualTo("test"); 15 | assertThat(scheduler).hasToString("{\"id\":\"test\"}"); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/core/model/ServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import org.junit.jupiter.api.RepeatedTest; 4 | import org.junit.jupiter.api.parallel.Execution; 5 | import org.junit.jupiter.api.parallel.ExecutionMode; 6 | import org.nanonative.nano.core.Nano; 7 | import org.nanonative.nano.core.config.TestConfig; 8 | import org.nanonative.nano.helper.event.model.Event; 9 | import org.nanonative.nano.model.TestService; 10 | 11 | import java.util.Map; 12 | 13 | import static org.assertj.core.api.Assertions.assertThat; 14 | import static org.nanonative.nano.core.config.TestConfig.TEST_REPEAT; 15 | import static org.nanonative.nano.core.config.TestConfig.TEST_TIMEOUT; 16 | import static org.nanonative.nano.core.model.Context.EVENT_APP_UNHANDLED; 17 | import static org.nanonative.nano.helper.NanoUtils.waitForCondition; 18 | import static org.nanonative.nano.helper.event.model.Event.eventOf; 19 | import static org.nanonative.nano.services.logging.LogService.CONFIG_LOG_LEVEL; 20 | 21 | @Execution(ExecutionMode.CONCURRENT) 22 | class ServiceTest { 23 | 24 | @RepeatedTest(TEST_REPEAT) 25 | void testService() { 26 | final long startTime = System.currentTimeMillis() - 10; 27 | final Nano nano = new Nano(Map.of(CONFIG_LOG_LEVEL, TestConfig.TEST_LOG_LEVEL)); 28 | final Context context = nano.context(this.getClass()); 29 | final TestService service = new TestService(); 30 | final Event error = eventOf(context, 999).payload(() -> "TEST ERROR_AA").error(new RuntimeException("TEST ERROR_BB")); 31 | 32 | assertThat(service).isNotNull(); 33 | assertThat(service.createdAtMs()).isGreaterThan(startTime); 34 | assertThat(service.startCount()).isZero(); 35 | assertThat(service.stopCount()).isZero(); 36 | assertThat(service.events()).isEmpty(); 37 | assertThat(service.failures()).isEmpty(); 38 | 39 | service.start(); 40 | assertThat(service.startCount()).isEqualTo(1); 41 | 42 | service.stop(); 43 | assertThat(service.stopCount()).isEqualTo(1); 44 | 45 | service.onFailure(error); 46 | assertThat(service.failures()).hasSize(1).contains(error); 47 | 48 | final Event event = eventOf(context, EVENT_APP_UNHANDLED).payload(() -> error); 49 | service.onEvent(event); 50 | final Event receivedEvent = service.getEvent(EVENT_APP_UNHANDLED); 51 | assertThat(receivedEvent).isNotNull(); 52 | assertThat(receivedEvent.payload(Event.class)).isEqualTo(error); 53 | 54 | assertThat(nano.services()).hasSize(1); 55 | service.nanoThread(context).run(() -> context, () -> {}); 56 | assertThat(waitForCondition(() -> service.startCount() == 2, TEST_TIMEOUT)).isTrue(); 57 | waitForCondition(() -> nano.services().size() == 2, TEST_TIMEOUT); 58 | assertThat(service.startCount()).isEqualTo(2); 59 | assertThat(service.failures()).hasSize(1); 60 | assertThat(nano.services()).size().isEqualTo(2); 61 | 62 | assertThat(nano.stop(this.getClass()).waitForStop().isReady()).isFalse(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/core/model/UnhandledTest.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.core.model; 2 | 3 | import org.nanonative.nano.core.Nano; 4 | import org.junit.jupiter.api.RepeatedTest; 5 | import org.junit.jupiter.api.parallel.Execution; 6 | import org.junit.jupiter.api.parallel.ExecutionMode; 7 | 8 | import java.util.Map; 9 | 10 | import static org.nanonative.nano.core.config.TestConfig.*; 11 | import static org.assertj.core.api.Assertions.assertThat; 12 | import static org.nanonative.nano.services.logging.LogService.CONFIG_LOG_LEVEL; 13 | 14 | @Execution(ExecutionMode.CONCURRENT) 15 | class UnhandledTest { 16 | 17 | @RepeatedTest(TEST_REPEAT) 18 | void testConstructor() { 19 | final Nano nano = new Nano(Map.of(CONFIG_LOG_LEVEL, TEST_LOG_LEVEL)).stop(this.getClass()); 20 | final Context context = nano.context(this.getClass()); 21 | final Unhandled error = new Unhandled(context, 111, null); 22 | assertThat(error).isNotNull(); 23 | assertThat(error.nano()).isEqualTo(nano); 24 | assertThat(error.context()).isEqualTo(context); 25 | assertThat(error.payload()).isEqualTo(111); 26 | assertThat(error.payload(String.class)).isEqualTo("111"); 27 | assertThat(error.payload(Integer.class)).isEqualTo(111); 28 | assertThat(error.exception()).isNull(); 29 | assertThat(error).hasToString("Unhandled{payload=111, exception=null}"); 30 | assertThat(nano.waitForStop().isReady()).isFalse(); 31 | } 32 | 33 | @RepeatedTest(TEST_REPEAT) 34 | void testNullConstructor() { 35 | final Unhandled error = new Unhandled(null, null, null); 36 | assertThat(error).isNotNull(); 37 | assertThat(error.nano()).isNull(); 38 | assertThat(error.context()).isNull(); 39 | assertThat(error.payload()).isNull(); 40 | assertThat(error.payload(String.class)).isNull(); 41 | assertThat(error.exception()).isNull(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/examples/ErrorHandling.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.examples; 2 | 3 | import org.nanonative.nano.core.Nano; 4 | import org.nanonative.nano.core.model.Context; 5 | import org.junit.jupiter.api.Disabled; 6 | 7 | import static org.nanonative.nano.core.model.Context.EVENT_APP_UNHANDLED; 8 | 9 | @Disabled 10 | public class ErrorHandling { 11 | 12 | public static void main(final String[] args) { 13 | final Context context = new Nano(args).context(ErrorHandling.class); 14 | 15 | // Listen to exceptions 16 | context.subscribeEvent(EVENT_APP_UNHANDLED, event -> { 17 | // Print error message 18 | event.context().warn(() -> "Caught event [{}] with error [{}] ", event.nameOrg(), event.error().getMessage()); 19 | event.acknowledge(); // Set exception as handled (prevent further processing) 20 | }); 21 | 22 | // Throw an exception 23 | context.run(() -> { 24 | throw new RuntimeException("Test Exception"); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/examples/HttpReceive.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.examples; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.nanonative.nano.core.Nano; 5 | import org.nanonative.nano.helper.event.model.Event; 6 | import org.nanonative.nano.services.http.HttpServer; 7 | import org.nanonative.nano.services.http.model.HttpObject; 8 | 9 | import java.util.Map; 10 | 11 | import static org.nanonative.nano.core.model.Context.EVENT_APP_UNHANDLED; 12 | import static org.nanonative.nano.services.http.HttpServer.EVENT_HTTP_REQUEST; 13 | 14 | @Disabled 15 | public class HttpReceive { 16 | 17 | public static void main(final String[] args) { 18 | final Nano app = new Nano(args, new HttpServer()); 19 | 20 | // Authorization 21 | app.subscribeEvent(EVENT_HTTP_REQUEST, HttpReceive::authorize); 22 | 23 | // Response 24 | app.subscribeEvent(EVENT_HTTP_REQUEST, HttpReceive::helloWorldController); 25 | 26 | // Error handling 27 | app.subscribeEvent(EVENT_APP_UNHANDLED, HttpReceive::controllerAdvice); 28 | 29 | // CORS 30 | app.subscribeEvent(EVENT_HTTP_REQUEST, HttpReceive::cors); 31 | } 32 | 33 | private static void cors(final Event event) { 34 | event.payloadOpt(HttpObject.class) 35 | .filter(HttpObject::isMethodOptions) 36 | .ifPresent(request -> request.corsResponse().respond(event)); 37 | } 38 | 39 | private static void authorize(final Event event) { 40 | event.payloadOpt(HttpObject.class) 41 | .filter(request -> request.pathMatch("/hello/**")) 42 | .filter(request -> !"mySecretToken".equals(request.authToken())) 43 | .ifPresent(request -> request.response().body(Map.of("message", "You are unauthorized")).statusCode(401).respond(event)); 44 | } 45 | 46 | private static void helloWorldController(final Event event) { 47 | event.payloadOpt(HttpObject.class) 48 | .filter(HttpObject::isMethodGet) 49 | .filter(request -> request.pathMatch("/hello")) 50 | .ifPresent(request -> request.response().body(Map.of("Hello", System.getProperty("user.name"))).respond(event)); 51 | } 52 | 53 | private static void controllerAdvice(final Event event) { 54 | event.payloadOpt(HttpObject.class).ifPresent(request -> 55 | request.response().body("Internal Server Error [" + event.error().getMessage() + "]").statusCode(500).respond(event)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/examples/HttpSend.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.examples; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.nanonative.nano.core.Nano; 5 | import org.nanonative.nano.core.model.Context; 6 | import org.nanonative.nano.services.http.HttpClient; 7 | import org.nanonative.nano.services.http.HttpServer; 8 | import org.nanonative.nano.services.http.model.HttpObject; 9 | 10 | import static org.nanonative.nano.services.http.HttpClient.EVENT_SEND_HTTP; 11 | import static org.nanonative.nano.services.http.model.HttpMethod.GET; 12 | 13 | @Disabled 14 | @SuppressWarnings("java:S1854") // Suppress "dead code" warning 15 | public class HttpSend { 16 | 17 | public static void main(final String[] args) { 18 | final Context context = new Nano(args, new HttpServer()).context(HttpSend.class); 19 | 20 | // send request via event 21 | final HttpObject response1 = context.sendEventR(EVENT_SEND_HTTP, () -> new HttpObject() 22 | .methodType(GET) 23 | .path("http://localhost:8080/hello") 24 | .body("Hello World") 25 | ).response(HttpObject.class); 26 | 27 | // send request via context 28 | final HttpObject response2 = new HttpObject() 29 | .methodType(GET) 30 | .path("http://localhost:8080/hello") 31 | .body("Hello World") 32 | .send(context); 33 | 34 | // send request manually 35 | final HttpObject response3 = new HttpClient().send(new HttpObject() 36 | .methodType(GET) 37 | .path("http://localhost:8080/hello") 38 | .body("Hello World") 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/examples/Kazim.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.examples; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.nanonative.nano.core.Nano; 5 | import org.nanonative.nano.services.logging.model.LogLevel; 6 | import org.nanonative.nano.services.http.HttpServer; 7 | import org.nanonative.nano.services.metric.logic.MetricService; 8 | 9 | import java.util.Map; 10 | 11 | import static org.nanonative.nano.services.logging.LogService.CONFIG_LOG_FORMATTER; 12 | import static org.nanonative.nano.services.logging.LogService.CONFIG_LOG_LEVEL; 13 | 14 | @Disabled 15 | public class Kazim { 16 | 17 | public static void main(String[] args) { 18 | final Nano application = new Nano(Map.of( 19 | CONFIG_LOG_LEVEL, LogLevel.INFO, 20 | CONFIG_LOG_FORMATTER, "console" 21 | // CONFIG_METRIC_SERVICE_BASE_PATH, "/metrics", 22 | // CONFIG_METRIC_SERVICE_PROMETHEUS_PATH, "/influx" 23 | ), new MetricService(), new HttpServer()); 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/examples/MetricCreation.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.examples; 2 | 3 | import org.nanonative.nano.core.Nano; 4 | import org.nanonative.nano.core.model.Context; 5 | import org.nanonative.nano.services.metric.logic.MetricService; 6 | import org.nanonative.nano.services.metric.model.MetricUpdate; 7 | import org.junit.jupiter.api.Disabled; 8 | 9 | import java.util.Map; 10 | 11 | import static org.nanonative.nano.services.metric.logic.MetricService.EVENT_METRIC_UPDATE; 12 | import static org.nanonative.nano.services.metric.model.MetricType.GAUGE; 13 | import static org.nanonative.nano.services.metric.model.MetricType.TIMER_END; 14 | import static org.nanonative.nano.services.metric.model.MetricType.TIMER_START; 15 | 16 | @Disabled 17 | @SuppressWarnings("java:S1854") // Suppress "dead code" warning 18 | public class MetricCreation { 19 | 20 | public static void main(final String[] args) { 21 | final Context context = new Nano(args, new MetricService()).context(MetricCreation.class); 22 | final Map metricTags = Map.of("tag_key", "tag_value"); 23 | 24 | // create counter 25 | context.sendEvent(EVENT_METRIC_UPDATE, () -> new MetricUpdate(GAUGE, "my.counter.key", 130624, metricTags)); 26 | // create gauge 27 | context.sendEvent(EVENT_METRIC_UPDATE, () -> new MetricUpdate(GAUGE, "my.gauge.key", 200888, metricTags)); 28 | // start timer 29 | context.sendEvent(EVENT_METRIC_UPDATE, () -> new MetricUpdate(TIMER_START, "my.timer.key", null, metricTags)); 30 | // end timer 31 | context.sendEvent(EVENT_METRIC_UPDATE, () -> new MetricUpdate(TIMER_END, "my.timer.key", null, metricTags)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/examples/Yuna.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.examples; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.nanonative.nano.core.Nano; 5 | import org.nanonative.nano.core.model.Context; 6 | import org.nanonative.nano.services.http.HttpServer; 7 | import org.nanonative.nano.services.http.model.HttpObject; 8 | 9 | import java.util.Date; 10 | import java.util.Map; 11 | import java.util.Optional; 12 | import java.util.logging.Level; 13 | 14 | import static org.nanonative.nano.core.model.Context.EVENT_CONFIG_CHANGE; 15 | import static org.nanonative.nano.services.http.HttpServer.EVENT_HTTP_REQUEST; 16 | import static org.nanonative.nano.services.logging.LogService.CONFIG_LOG_LEVEL; 17 | 18 | @Disabled 19 | public class Yuna { 20 | 21 | public static void main(final String[] args) { 22 | final Nano nano = new Nano(new HttpServer()); 23 | 24 | final Date myConfigValue = nano.context().asDate("unix-timestamp"); 25 | nano.context().info(() -> "Config value converted to date [{}]", myConfigValue); 26 | 27 | nano.subscribeEvent(EVENT_HTTP_REQUEST, event -> event.payloadOpt(HttpObject.class) 28 | .filter(HttpObject::isMethodGet) 29 | .filter(req -> req.pathMatch("/hello")) 30 | .ifPresent(req -> req.corsResponse() 31 | .statusCode(200) 32 | .body(Map.of("Nano", "World")) 33 | .respond(event)) 34 | ); 35 | 36 | final Context context = nano.context(Yuna.class); 37 | context.info(() -> "Hello World 1"); 38 | context.sendEvent(EVENT_CONFIG_CHANGE, () -> Map.of(CONFIG_LOG_LEVEL, Level.OFF)); 39 | context.info(() -> "Hello World 2"); 40 | nano.stop(context); 41 | // 42 | // // Nano with configuration 43 | // final Nano nano = new Nano(Map.of(CONFIG_LOG_LEVEL, LogLevel.INFO)); 44 | // 45 | // // Nano with startup services 46 | // final Nano nano = new Nano(new HttpServer()); 47 | // 48 | // // Nano adding "Hello World" API 49 | // final Nano nano = new Nano(new HttpServer()) 50 | // .subscribeEvent(EVENT_HTTP_REQUEST, event -> event.payloadOpt(HttpObject.class) 51 | // .filter(HttpObject::isMethodGet) 52 | // .filter(request -> request.pathMatch("/hello")) 53 | // .ifPresent(request -> request.response().body(System.getProperty("user.name")).send(event)) 54 | // ); 55 | 56 | 57 | //TODO: Dynamic Queues to Services 58 | //TODO: Dynamic Messages to Services 59 | //TODO: support internationalization (logRecord.setResourceBundle(javaLogger.getResourceBundle());, logRecord.setResourceBundleName(javaLogger.getResourceBundleName())) 60 | // final Nano application = new Nano(Map.of( 61 | // CONFIG_LOG_LEVEL, LogLevel.INFO, 62 | // CONFIG_LOG_FORMATTER, "console" 63 | // ), new LogQueue(), new MetricService(), new HttpServer()); 64 | 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/model/EventChannelRegisterTest.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.model; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.nanonative.nano.helper.event.EventChannelRegister.*; 6 | import static org.assertj.core.api.Assertions.assertThat; 7 | 8 | class EventChannelRegisterTest { 9 | 10 | @Test 11 | void testChannelChannelIdRegistration() { 12 | final int channelId = registerChannelId(this.getClass().getSimpleName().toUpperCase()); 13 | assertThat(isChannelIdAvailable(channelId)).isTrue(); 14 | assertThat(eventNameOf(channelId)).isEqualTo(this.getClass().getSimpleName().toUpperCase()); 15 | 16 | // duplicated registration should not be possible 17 | final int channelId2 = registerChannelId(this.getClass().getSimpleName().toUpperCase()); 18 | assertThat(isChannelIdAvailable(channelId2)).isTrue(); 19 | assertThat(channelId).isEqualTo(channelId2); 20 | assertThat(eventNameOf(channelId)).isEqualTo(this.getClass().getSimpleName().toUpperCase()); 21 | 22 | // should not find non registered channelIds 23 | assertThat(isChannelIdAvailable(-99)).isFalse(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/model/TestService.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.model; 2 | 3 | import berlin.yuna.typemap.model.TypeMapI; 4 | import org.nanonative.nano.core.model.Context; 5 | import org.nanonative.nano.core.model.Service; 6 | import org.nanonative.nano.helper.event.EventChannelRegister; 7 | import org.nanonative.nano.helper.event.model.Event; 8 | 9 | import java.util.List; 10 | import java.util.concurrent.CopyOnWriteArrayList; 11 | import java.util.concurrent.atomic.AtomicInteger; 12 | import java.util.concurrent.atomic.AtomicReference; 13 | import java.util.function.Consumer; 14 | import java.util.function.Function; 15 | import java.util.stream.Collectors; 16 | 17 | import static java.lang.System.lineSeparator; 18 | import static java.util.Optional.ofNullable; 19 | import static org.nanonative.nano.core.config.TestConfig.TEST_TIMEOUT; 20 | import static org.nanonative.nano.core.model.Context.EVENT_APP_HEARTBEAT; 21 | import static org.nanonative.nano.helper.NanoUtils.waitForCondition; 22 | 23 | public class TestService extends Service { 24 | 25 | private final AtomicInteger startCount = new AtomicInteger(0); 26 | private final AtomicInteger stopCount = new AtomicInteger(0); 27 | private final List failures = new CopyOnWriteArrayList<>(); 28 | private final List events = new CopyOnWriteArrayList<>(); 29 | private final AtomicReference> doOnEvent = new AtomicReference<>(); 30 | private final AtomicReference> failureConsumer = new AtomicReference<>(); 31 | private final AtomicReference> startConsumer = new AtomicReference<>(); 32 | private final AtomicReference> stopConsumer = new AtomicReference<>(); 33 | private long startTime = System.currentTimeMillis(); 34 | public static int TEST_EVENT = EventChannelRegister.registerChannelId("TEST_EVENT"); 35 | 36 | public TestService resetEvents() { 37 | events.clear(); 38 | return this; 39 | } 40 | 41 | public List events(final int channelId) { 42 | getEvent(channelId); 43 | return events.stream().filter(event -> event.channelId() == channelId).toList(); 44 | } 45 | 46 | public Event getEvent(final int channelId) { 47 | return getEvent(channelId, null, 2000); 48 | } 49 | 50 | public Event getEvent(final int channelId, final Function condition) { 51 | return getEvent(channelId, condition, TEST_TIMEOUT); 52 | } 53 | 54 | public Event getEvent(final int channelId, final long timeoutMs) { 55 | return getEvent(channelId, null, timeoutMs); 56 | } 57 | 58 | public Event getEvent(final int channelId, final Function condition, final long timeoutMs) { 59 | final AtomicReference result = new AtomicReference<>(); 60 | waitForCondition( 61 | () -> { 62 | final Event event1 = events.stream() 63 | .filter(event -> event.channelId() == channelId) 64 | .filter(event -> condition != null ? condition.apply(event) : true) 65 | .findFirst() 66 | .orElse(null); 67 | if (event1 != null) 68 | result.set(event1); 69 | return event1 != null; 70 | } 71 | , timeoutMs 72 | ); 73 | if (result.get() == null) 74 | throw new AssertionError("Event not found" 75 | + " channel [" + EventChannelRegister.eventNameOf(channelId) + "]" 76 | + " events [" + lineSeparator() + events.stream().filter(event -> event.channelId() != EVENT_APP_HEARTBEAT).map(Event::toString).collect(Collectors.joining(lineSeparator())) + lineSeparator() + "]" 77 | ); 78 | return result.get(); 79 | } 80 | 81 | public int startCount() { 82 | return startCount.get(); 83 | } 84 | 85 | public int stopCount() { 86 | return stopCount.get(); 87 | } 88 | 89 | public List failures() { 90 | return failures; 91 | } 92 | 93 | public List events() { 94 | return events; 95 | } 96 | 97 | public Consumer doOnEvent() { 98 | return doOnEvent.get(); 99 | } 100 | 101 | public TestService doOnEvent(final Consumer onEvent) { 102 | this.doOnEvent.set(onEvent); 103 | return this; 104 | } 105 | 106 | public Consumer doOnFailure() { 107 | return failureConsumer.get(); 108 | } 109 | 110 | public TestService doOnFailure(final Consumer onFailure) { 111 | this.failureConsumer.set(onFailure); 112 | return this; 113 | } 114 | 115 | public Consumer doOnStart() { 116 | return startConsumer.get(); 117 | } 118 | 119 | public TestService doOnStart(final Consumer onStart) { 120 | this.startConsumer.set(onStart); 121 | return this; 122 | } 123 | 124 | public Consumer doOnStop() { 125 | return stopConsumer.get(); 126 | } 127 | 128 | public TestService doOnStop(final Consumer onStop) { 129 | this.stopConsumer.set(onStop); 130 | return this; 131 | } 132 | 133 | public long getStartTime() { 134 | return startTime; 135 | } 136 | 137 | // ########## DEFAULT METHODS ########## 138 | @Override 139 | public void start() { 140 | startTime = System.currentTimeMillis(); 141 | startCount.incrementAndGet(); 142 | if (startConsumer.get() != null) 143 | startConsumer.get().accept(context); 144 | } 145 | 146 | @Override 147 | public void stop() { 148 | stopCount.incrementAndGet(); 149 | if (stopConsumer.get() != null) 150 | stopConsumer.get().accept(context); 151 | } 152 | 153 | @Override 154 | public Object onFailure(final Event error) { 155 | failures.add(error); 156 | ofNullable(failureConsumer.get()).ifPresent(consumer -> consumer.accept(error)); 157 | return null; 158 | } 159 | 160 | @Override 161 | public void onEvent(final Event event) { 162 | events.add(event); 163 | ofNullable(doOnEvent.get()).ifPresent(consumer -> consumer.accept(event)); 164 | } 165 | 166 | @Override 167 | public void configure(final TypeMapI configs, final TypeMapI merged) { 168 | 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/services/metric/logic/MetricServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.metric.logic; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.nanonative.nano.core.Nano; 5 | import org.nanonative.nano.services.http.HttpClient; 6 | import org.nanonative.nano.services.http.HttpServer; 7 | import org.nanonative.nano.services.http.model.HttpMethod; 8 | import org.nanonative.nano.services.http.model.HttpObject; 9 | 10 | import java.util.Map; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | import static org.nanonative.nano.core.config.TestConfig.TEST_LOG_LEVEL; 14 | import static org.nanonative.nano.services.http.HttpServer.CONFIG_SERVICE_HTTP_CLIENT; 15 | import static org.nanonative.nano.services.logging.LogService.CONFIG_LOG_LEVEL; 16 | import static org.nanonative.nano.services.metric.logic.MetricService.CONFIG_METRIC_SERVICE_BASE_PATH; 17 | import static org.nanonative.nano.services.metric.logic.MetricService.CONFIG_METRIC_SERVICE_PROMETHEUS_PATH; 18 | 19 | class MetricServiceTest { 20 | 21 | protected static String serverUrl = "http://localhost:"; 22 | 23 | @Test 24 | void metricEndpointsWithoutBasePath() { 25 | final Nano nano = new Nano(Map.of(CONFIG_LOG_LEVEL, TEST_LOG_LEVEL, CONFIG_SERVICE_HTTP_CLIENT, true), new MetricService(), new HttpServer()); 26 | 27 | final HttpObject result = new HttpObject() 28 | .methodType(HttpMethod.GET) 29 | .path(serverUrl + nano.service(HttpServer.class).port() + "/metrics/prometheus") 30 | .send(nano.context(MetricServiceTest.class)); 31 | 32 | assertThat(result).isNotNull(); 33 | assertThat(result.bodyAsString()).contains("java_version "); 34 | assertThat(nano.stop(MetricServiceTest.class).waitForStop().isReady()).isFalse(); 35 | } 36 | 37 | @Test 38 | void metricEndpointsWithCustomBasePath() { 39 | final Nano nano = new Nano(Map.of(CONFIG_LOG_LEVEL, TEST_LOG_LEVEL, CONFIG_METRIC_SERVICE_BASE_PATH, "/custom-metrics"), new MetricService(), new HttpServer(), new HttpClient()); 40 | 41 | final HttpObject result = new HttpObject() 42 | .methodType(HttpMethod.GET) 43 | .path(serverUrl + nano.service(HttpServer.class).port() + "/custom-metrics/prometheus") 44 | .send(nano.context(MetricServiceTest.class)); 45 | 46 | assertThat(result).isNotNull(); 47 | assertThat(result.bodyAsString()).contains("java_version "); 48 | assertThat(nano.stop(MetricServiceTest.class).waitForStop().isReady()).isFalse(); 49 | } 50 | 51 | 52 | @Test 53 | void metricEndpointsWithPrometheus() { 54 | final Nano nano = new Nano(Map.of(CONFIG_LOG_LEVEL, TEST_LOG_LEVEL, CONFIG_METRIC_SERVICE_PROMETHEUS_PATH, "/prometheus"), new MetricService(), new HttpServer(), new HttpClient()); 55 | 56 | final HttpObject result = new HttpObject() 57 | .methodType(HttpMethod.GET) 58 | .path(serverUrl + nano.service(HttpServer.class).port() + "/prometheus") 59 | .send(nano.context(MetricServiceTest.class)); 60 | 61 | assertThat(result).isNotNull(); 62 | assertThat(result.statusCode()).isEqualTo(200); 63 | assertThat(nano.stop(MetricServiceTest.class).waitForStop().isReady()).isFalse(); 64 | } 65 | 66 | @Test 67 | void metricEndpointsWithBasePath() { 68 | final Nano nano = new Nano(Map.of(CONFIG_LOG_LEVEL, TEST_LOG_LEVEL, CONFIG_METRIC_SERVICE_BASE_PATH, "/stats"), new MetricService(), new HttpServer(), new HttpClient()); 69 | 70 | final HttpObject result = new HttpObject() 71 | .methodType(HttpMethod.GET) 72 | .path(serverUrl + nano.service(HttpServer.class).port() + "/stats/prometheus") 73 | .send(nano.context(MetricServiceTest.class)); 74 | 75 | assertThat(result).isNotNull(); 76 | assertThat(result.statusCode()).isEqualTo(200); 77 | assertThat(nano.stop(MetricServiceTest.class).waitForStop().isReady()).isFalse(); 78 | 79 | } 80 | 81 | @Test 82 | void withoutMetricService() { 83 | final Nano nano = new Nano(Map.of(CONFIG_LOG_LEVEL, TEST_LOG_LEVEL), new HttpServer(), new HttpClient()); 84 | 85 | final HttpObject result = new HttpObject() 86 | .methodType(HttpMethod.GET) 87 | .path(serverUrl + nano.service(HttpServer.class).port() + "/metrics/prometheus") 88 | .send(nano.context(MetricServiceTest.class)); 89 | 90 | assertThat(result).isNotNull(); 91 | assertThat(result.statusCode()).isEqualTo(404); 92 | assertThat(nano.stop(MetricServiceTest.class).waitForStop().isReady()).isFalse(); 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/test/java/org/nanonative/nano/services/metric/model/MetricCacheTest.java: -------------------------------------------------------------------------------- 1 | package org.nanonative.nano.services.metric.model; 2 | 3 | import org.junit.jupiter.api.RepeatedTest; 4 | import org.junit.jupiter.api.parallel.Execution; 5 | import org.junit.jupiter.api.parallel.ExecutionMode; 6 | 7 | import java.util.Map; 8 | 9 | import static org.nanonative.nano.core.config.TestConfig.TEST_REPEAT; 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | @Execution(ExecutionMode.CONCURRENT) 13 | class MetricCacheTest { 14 | 15 | 16 | @RepeatedTest(TEST_REPEAT) 17 | void generateMetricFormats() throws InterruptedException { 18 | final MetricCache metricCache = new MetricCache() 19 | .counterIncrement("my_counter") 20 | .counterIncrement("my/counter") 21 | .gaugeSet("my_gauge", 100) 22 | .gaugeSet("my$gauge", 9.99) 23 | .timerStart("my^timer"); 24 | Thread.sleep(15); 25 | metricCache.timerStop("my#timer"); 26 | 27 | assertThat(metricCache.counters()).hasSize(1); 28 | assertThat(metricCache.gauges()).hasSize(1); 29 | assertThat(metricCache.timers()).hasSize(1); 30 | 31 | final long timer = metricCache.timer("my%timer"); 32 | assertThat(metricCache.counter("my%counter")).isEqualTo(2); 33 | assertThat(metricCache.gauge("my%gauge")).isEqualTo(9.99); 34 | assertThat(timer).isBetween(15L, 60L); 35 | assertThat(metricCache.prometheus()).isEqualTo("my_counter 2\nmy_gauge 9.99\nmy_timer " + timer + "\n"); 36 | assertThat(metricCache.influx()).isEqualTo("my.counter value=2\nmy.gauge value=9.99\nmy.timer value=" + timer + "\n"); 37 | assertThat(metricCache.dynatrace()).isEqualTo("my.counter, 2\nmy.gauge, 9.99\nmy.timer, " + timer + "\n"); 38 | assertThat(metricCache.wavefront()).isEqualTo("my.counter 2 source=nano \nmy.gauge 9.99 source=nano \nmy.timer " + timer + " source=nano \n"); 39 | assertThat(metricCache).hasToString(MetricCache.class.getSimpleName() + "{counters=1, gauges=1, timers=1}"); 40 | } 41 | 42 | @RepeatedTest(TEST_REPEAT) 43 | void generateMetricFormatsWithTags() throws InterruptedException { 44 | final Map tags = Map.of("aa", "bb", "cc", "dd"); 45 | final MetricCache metricCache = new MetricCache() 46 | .counterIncrement("my_counter", tags) 47 | .counterIncrement("my/counter", tags) 48 | .gaugeSet("my_gauge", 100, tags) 49 | .gaugeSet("my$gauge", 9.99, tags) 50 | .timerStart("my^timer", tags); 51 | Thread.sleep(15); 52 | metricCache.timerStop("my#timer", tags); 53 | 54 | assertThat(metricCache.counters()).hasSize(1); 55 | assertThat(metricCache.gauges()).hasSize(1); 56 | assertThat(metricCache.timers()).hasSize(1); 57 | 58 | final long timer = metricCache.timer("my%timer", tags); 59 | assertThat(timer).isBetween(15L, 60L); 60 | assertThat(metricCache.counter("my%counter", tags)).isEqualTo(2); 61 | assertThat(metricCache.gauge("my%gauge", tags)).isEqualTo(9.99); 62 | assertThat(metricCache.prometheus()).isEqualTo("my_counter{aa=\"bb\",cc=\"dd\"} 2\nmy_gauge{aa=\"bb\",cc=\"dd\"} 9.99\nmy_timer{aa=\"bb\",cc=\"dd\"} " + timer + "\n"); 63 | assertThat(metricCache.influx()).isEqualTo("my.counter,aa=bb,cc=dd value=2\nmy.gauge,aa=bb,cc=dd value=9.99\nmy.timer,aa=bb,cc=dd value=" + timer + "\n"); 64 | assertThat(metricCache.dynatrace()).isEqualTo("my.counter,aa=bb,cc=dd 2\nmy.gauge,aa=bb,cc=dd 9.99\nmy.timer,aa=bb,cc=dd " + timer + "\n"); 65 | assertThat(metricCache.wavefront()).isEqualTo("my.counter 2 source=nano aa=bb cc=dd\nmy.gauge 9.99 source=nano aa=bb cc=dd\nmy.timer " + timer + " source=nano aa=bb cc=dd\n"); 66 | assertThat(metricCache).hasToString(MetricCache.class.getSimpleName() + "{counters=1, gauges=1, timers=1}"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/resources/Nano.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NanoNative/nano/7888a9a36ecb671c11426816db1531030899895d/src/test/resources/Nano.png -------------------------------------------------------------------------------- /src/test/resources/application-local.properties: -------------------------------------------------------------------------------- 1 | app.profiles=default, local, dev, prod 2 | resource.key2=CC 3 | -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | resource.key1=AA 2 | resource.key2=BB 3 | app.profile=local 4 | test.placeholder.fallback=${invalid_key:fallback should be used 1} 5 | test.placeholder.fallback.empty=${invalid_key:} 6 | test.placeholder.value=${placeholder_value:fallback should not be used} 7 | test.placeholder.key_empty=${:fallback should be used 2} 8 | placeholder_value=used placeholder value 9 | -------------------------------------------------------------------------------- /src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | junit.jupiter.execution.parallel.enabled = true 2 | junit.jupiter.execution.parallel.mode.default=concurrent 3 | junit.jupiter.execution.parallel.mode.classes.default=concurrent 4 | -------------------------------------------------------------------------------- /src/test/resources/tiny_java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NanoNative/nano/7888a9a36ecb671c11426816db1531030899895d/src/test/resources/tiny_java.png --------------------------------------------------------------------------------