├── .git-blame-ignore-revs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── release-drafter.yml └── workflows │ ├── build.yml │ ├── pull.yml │ └── release.yml ├── .gitignore ├── .mergify.yml ├── .sbtopts ├── .scala-steward.conf ├── .scalafix.conf ├── .scalafmt.conf ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app-monix └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── bundle │ ├── MonixResourceApp.scala │ └── MonixServerApp.scala ├── app-zio └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── bundle │ ├── ZioResourceApp.scala │ └── ZioServerApp.scala ├── build.sbt ├── cassandra-datastax-driver-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── datastax │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── datastax │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── cassandra-datastax-driver └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── datastax │ ├── CassandraDatastaxDriverModule.scala │ ├── DatastaxHelper.scala │ └── config │ ├── CassandraDatastaxDriverConfig.scala │ ├── advanced.scala │ ├── basic.scala │ ├── package.scala │ └── profile.scala ├── cats-effect └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── catseffect │ │ ├── TimeUtils.scala │ │ └── syntax │ │ ├── TimeSyntax.scala │ │ └── package.scala │ └── test │ └── scala │ └── com │ └── avast │ └── sst │ └── catseffect │ └── syntax │ └── FOpsTest.scala ├── doobie-hikari-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── doobie │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── doobie │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── doobie-hikari └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── doobie │ ├── DoobieHikariConfig.scala │ └── DoobieHikariModule.scala ├── example ├── README.md ├── docker-compose.yml └── src │ └── main │ ├── resources │ └── reference.conf │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── example │ │ └── config │ │ └── Configuration.scala │ ├── scala-3 │ └── com │ │ └── avast │ │ └── sst │ │ └── example │ │ └── config │ │ └── Configuration.scala │ └── scala │ └── com │ └── avast │ └── sst │ └── example │ ├── Main.scala │ ├── module │ └── Http4sRoutingModule.scala │ └── service │ └── RandomService.scala ├── flyway-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── flyway │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── flyway │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── flyway └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── flyway │ ├── FlywayConfig.scala │ └── FlywayModule.scala ├── fs2-kafka-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── fs2kafka │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── fs2kafka │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── fs2-kafka └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── fs2kafka │ │ ├── ConsumerConfig.scala │ │ ├── Fs2KafkaModule.scala │ │ └── ProducerConfig.scala │ └── test │ └── scala-2 │ └── com │ └── avast │ └── sst │ └── fs2kafka │ ├── Fs2KafkaModuleTest.scala │ └── KafkaConfigTest.scala ├── grpc-server-micrometer └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── grpc │ └── server │ └── micrometer │ └── MonitoringServerInterceptor.scala ├── grpc-server-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── grpc │ │ └── server │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── grpc │ └── server │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── grpc-server └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── grpc │ └── server │ ├── GrpcServerConfig.scala │ ├── GrpcServerModule.scala │ └── interceptor │ └── LoggingServerInterceptor.scala ├── http4s-client-blaze-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── http4s │ │ └── client │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── http4s │ └── client │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── http4s-client-blaze └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── http4s │ │ └── client │ │ ├── Http4sBlazeClientConfig.scala │ │ └── Http4sBlazeClientModule.scala │ └── test │ └── scala │ └── com │ └── avast │ └── sst │ └── http4s │ └── client │ └── Http4SBlazeClientTest.scala ├── http4s-client-ember-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── http4s │ │ └── client │ │ └── pureconfig │ │ └── ember │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── http4s │ └── client │ └── pureconfig │ └── ember │ ├── ConfigReaders.scala │ └── implicits.scala ├── http4s-client-ember └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── http4s │ │ └── client │ │ ├── Http4sEmberClientConfig.scala │ │ └── Http4sEmberClientModule.scala │ └── test │ └── scala │ └── com │ └── avast │ └── sst │ └── http4s │ └── client │ └── Http4SEmberClientTest.scala ├── http4s-client-monix-catnap └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── http4s │ └── client │ └── monix │ └── catnap │ ├── Http4sClientCircuitBreakerModule.scala │ └── HttpStatusClassifier.scala ├── http4s-server-blaze-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── http4s │ │ └── server │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── http4s │ └── server │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── http4s-server-blaze └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── http4s │ │ └── server │ │ ├── Http4sBlazeServerConfig.scala │ │ └── Http4sBlazeServerModule.scala │ └── test │ └── scala │ └── com │ └── avast │ └── sst │ └── http4s │ └── server │ └── Http4sBlazeServerModuleTest.scala ├── http4s-server-ember-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── http4s │ │ └── server │ │ └── pureconfig │ │ └── ember │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── http4s │ └── server │ └── pureconfig │ └── ember │ ├── ConfigReaders.scala │ └── implicits.scala ├── http4s-server-ember └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── http4s │ │ └── server │ │ ├── Http4sEmberServerConfig.scala │ │ └── Http4sEmberServerModule.scala │ └── test │ └── scala │ └── com │ └── avast │ └── sst │ └── http4s │ └── server │ └── Http4sEmberServerModuleTest.scala ├── http4s-server-micrometer └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── http4s │ │ └── server │ │ └── micrometer │ │ ├── MicrometerHttp4sMetricsOpsModule.scala │ │ ├── MicrometerHttp4sServerMetricsModule.scala │ │ └── RouteMetrics.scala │ └── test │ └── scala │ └── com │ └── avast │ └── sst │ └── http4s │ └── server │ └── micrometer │ ├── MicrometerHttp4sMetricsOpsModuleTest.scala │ └── RouteMetricsTest.scala ├── http4s-server └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── http4s │ │ └── server │ │ ├── Http4sRouting.scala │ │ └── middleware │ │ └── CorrelationIdMiddleware.scala │ └── test │ └── scala │ └── com │ └── avast │ └── sst │ └── http4s │ └── server │ └── middleware │ └── CorrelationIdMiddlewareTest.scala ├── jdk-http-client-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── jdk │ │ └── httpclient │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── jdk │ └── httpclient │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── jdk-http-client └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── jdk │ └── httpclient │ ├── JdkHttpClientConfig.scala │ └── JdkHttpClientModule.scala ├── jvm-micrometer └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── jvm │ └── micrometer │ └── MicrometerJvmModule.scala ├── jvm-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── jvm │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── jvm │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── jvm └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── jvm │ │ ├── execution │ │ ├── ConfigurableThreadFactory.scala │ │ ├── ExecutorModule.scala │ │ ├── ForkJoinPoolConfig.scala │ │ ├── LoggingUncaughtExceptionHandler.scala │ │ └── ThreadPoolExecutorConfig.scala │ │ └── system │ │ ├── console │ │ ├── Console.scala │ │ └── ConsoleModule.scala │ │ └── random │ │ ├── Random.scala │ │ └── RandomModule.scala │ └── test │ └── scala │ └── com │ └── avast │ └── sst │ └── jvm │ ├── execution │ └── ExecutorModuleTest.scala │ └── system │ ├── console │ └── ConsoleModuleTest.scala │ └── random │ └── RandomModuleTest.scala ├── lettuce-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── lettuce │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── lettuce │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── lettuce └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── lettuce │ ├── LettuceConfig.scala │ └── LettuceModule.scala ├── micrometer-jmx-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── micrometer │ │ └── jmx │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── micrometer │ └── jmx │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── micrometer-jmx └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── micrometer │ └── jmx │ ├── MicrometerJmxConfig.scala │ ├── MicrometerJmxModule.scala │ └── TypeScopeNameObjectNameFactory.scala ├── micrometer-prometheus-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── micrometer │ │ └── prometheus │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── micrometer │ └── prometheus │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── micrometer-prometheus └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── micrometer │ │ └── prometheus │ │ ├── MicrometerPrometheusConfig.scala │ │ └── MicrometerPrometheusModule.scala │ └── test │ └── scala │ └── com │ └── avast │ └── sst │ └── micrometer │ └── prometheus │ └── Http4sPrometheusCompatibilityTest.scala ├── micrometer-statsd-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── micrometer │ │ └── statsd │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── micrometer │ └── statsd │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── micrometer-statsd └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── micrometer │ └── statsd │ ├── MicrometerStatsDConfig.scala │ └── MicrometerStatsDModule.scala ├── micrometer └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── micrometer │ │ └── PrefixMeterFilter.scala │ └── test │ └── scala │ └── com │ └── avast │ └── sst │ └── micrometer │ └── PrefixMeterFilterTest.scala ├── monix-catnap-micrometer └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── monix │ └── catnap │ └── micrometer │ └── MicrometerCircuitBreakerMetricsModule.scala ├── monix-catnap-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── monix │ │ └── catnap │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── monix │ └── catnap │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── monix-catnap └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── monix │ └── catnap │ ├── CircuitBreakerConfig.scala │ ├── CircuitBreakerMetrics.scala │ └── CircuitBreakerModule.scala ├── project ├── BuildSettings.scala ├── Dependencies.scala ├── build.properties └── plugins.sbt ├── pureconfig └── src │ ├── main │ └── scala │ │ └── com │ │ └── avast │ │ └── sst │ │ └── pureconfig │ │ ├── PureConfigModule.scala │ │ ├── WithConfig.scala │ │ └── util │ │ └── Toggle.scala │ └── test │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── pureconfig │ │ ├── PureConfigModuleTest.scala │ │ └── ToggleTest.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── pureconfig │ ├── PureConfigModuleTest.scala │ └── ToggleTest.scala ├── sentry-pureconfig └── src │ └── main │ ├── scala-2 │ └── com │ │ └── avast │ │ └── sst │ │ └── sentry │ │ └── pureconfig │ │ ├── ConfigReaders.scala │ │ └── implicits.scala │ └── scala-3 │ └── com │ └── avast │ └── sst │ └── sentry │ └── pureconfig │ ├── ConfigReaders.scala │ └── implicits.scala ├── sentry └── src │ └── main │ └── scala │ └── com │ └── avast │ └── sst │ └── sentry │ ├── SentryConfig.scala │ └── SentryModule.scala ├── site ├── docs │ ├── bundles.md │ ├── getting-started.md │ ├── index.md │ ├── rationale.md │ ├── structure.md │ ├── subprojects.md │ └── subprojects │ │ ├── cassandra-datastax-driver.md │ │ ├── doobie.md │ │ ├── flyway.md │ │ ├── fs2-kafka.md │ │ ├── http4s.md │ │ ├── jvm.md │ │ ├── lettuce.md │ │ ├── micrometer.md │ │ ├── monix-catnap.md │ │ ├── pureconfig.md │ │ ├── sentry.md │ │ └── ssl-config.md └── menu.yml └── ssl-config └── src ├── main └── scala │ └── com │ └── avast │ └── sst │ └── ssl │ ├── Slf4jLogger.scala │ └── SslContextModule.scala └── test ├── resources ├── application.conf └── truststore.jks └── scala └── com └── avast └── sst └── ssl └── SslContextModuleTest.scala /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.8.4 2 | ee851aa4e36b33e8971b66b2b3aef8b4e28d6854 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the bug 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior: 15 | 16 | ## Expected behavior 17 | A clear and concise description of what you expected to happen. 18 | 19 | ## Additional context 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 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 | ## Additional context 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'feature' 7 | - title: '🐛 Bug Fixes' 8 | labels: 9 | - 'bug' 10 | - title: '🧰 Maintenance' 11 | labels: 12 | - 'build' 13 | - title: '🌱 Dependency Updates' 14 | labels: 15 | - 'dependency-update' 16 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 17 | template: | 18 | ## Changes 19 | 20 | $CHANGES 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up JDK 11 14 | uses: actions/setup-java@v1 15 | with: 16 | java-version: 11 17 | - uses: coursier/cache-action@v5 18 | with: 19 | extraKey: ${{ secrets.BUILD_CACHE_VERSION }} 20 | - name: Check/Compile/Test 21 | run: sbt check 22 | update_release_draft: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: release-drafter/release-drafter@v5 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/pull.yml: -------------------------------------------------------------------------------- 1 | name: PR Build 2 | on: [pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-20.04 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Set up JDK 11 10 | uses: actions/setup-java@v1 11 | with: 12 | java-version: 11 13 | - uses: coursier/cache-action@v5 14 | with: 15 | extraKey: pr-${GITHUB_HEAD_REF} 16 | - name: Check/Compile/Test 17 | run: sbt versionPolicyCheck check 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: ["v*"] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up JDK 11 12 | uses: actions/setup-java@v1 13 | with: 14 | java-version: 11 15 | - uses: coursier/cache-action@v5 16 | - name: Check/Compile/Test 17 | run: sbt versionCheck check 18 | - name: Release 19 | env: 20 | PGP_SECRET: ${{ secrets.PGP_SECRET_2024 }} 21 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE_2024 }} 22 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 23 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 24 | run: sbt ci-release 25 | - uses: actions/cache@v1 26 | with: 27 | path: vendor/bundle 28 | key: ${{ runner.os }}-gems 29 | restore-keys: | 30 | ${{ runner.os }}-gems 31 | - uses: actions/setup-ruby@v1 32 | with: 33 | ruby-version: '3.0' 34 | - name: Install jekyll 35 | run: | 36 | export GEM_HOME=${PWD}/vendor/bundle 37 | gem install jekyll -v 4.0.0 38 | - name: Publish Microsite 39 | env: 40 | DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }} 41 | GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=no" 42 | run: | 43 | eval "$(ssh-agent -s)" 44 | ssh-add - <<< "${DEPLOY_KEY}" 45 | git config --global user.email "janecek@avast.com" 46 | git config --global user.name "scala-server-toolkit bot" 47 | export PATH="${PWD}/vendor/bundle/bin:$PATH" 48 | export GEM_HOME=${PWD}/vendor/bundle 49 | sbt site/publishMicrosite 50 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: assign and label scala-steward's PRs 3 | conditions: 4 | - author=scala-steward 5 | actions: 6 | label: 7 | add: [dependency-update] 8 | - name: merge Scala Steward's PRs 9 | conditions: 10 | - base=master 11 | - author=scala-steward 12 | - status-success=build 13 | actions: 14 | merge: 15 | method: merge 16 | - name: automatic merge on CI success and review 17 | conditions: 18 | - base=master 19 | - "#review-requested=0" 20 | - "#changes-requested-reviews-by=0" 21 | - "#approved-reviews-by>=1" 22 | - status-success=build 23 | actions: 24 | merge: 25 | method: squash 26 | delete_head_branch: 27 | force: true 28 | -------------------------------------------------------------------------------- /.sbtopts: -------------------------------------------------------------------------------- 1 | -J-Xmx7g 2 | -J-XX:+UseG1GC 3 | -J-Xss16m 4 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | commits.message = "Update ${artifactName} from ${currentVersion} to ${nextVersion}" 2 | updates.pin = [ 3 | {groupId = "com.github.fd4s", artifactId = "fs2-kafka", version = "1."}, 4 | {groupId = "dev.zio", artifactId = "zio", version = "1."}, 5 | {groupId = "org.http4s", version = "0.22."}, 6 | {groupId = "org.typelevel", artifactId = "cats-effect", version = "2."}, 7 | ] 8 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.8.5" 2 | runner.dialect = "scala3" 3 | 4 | maxColumn = 140 5 | assumeStandardLibraryStripMargin = true 6 | onTestFailure = "To fix this, run 'sbt fix' from the project root directory." 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to Scala Server Toolkit. All contributions are welcome if they are consistent with the goals 4 | and guidelines of the project. 5 | 6 | It is better to open an issue in the project and discuss your intention with project maintainers before you actually start implementing 7 | something. They can also give you tips on how to go about implementing it. 8 | 9 | ## How to Contribute 10 | 11 | Please read the [First Contributions Guide](https://github.com/firstcontributions/first-contributions/blob/master/README.md) for general 12 | information about contribution to OSS projects. 13 | 14 | ## Build 15 | 16 | The project can be built and tested using simple SBT command: 17 | 18 | ```bash 19 | sbt test 20 | ``` 21 | 22 | There is also an extra command called `check` which you should use to check your code before you submit a PR: 23 | 24 | ```bash 25 | sbt check 26 | ``` 27 | 28 | Some of the reported problems can be automatically fixed by `fix`: 29 | 30 | ```bash 31 | sbt fix 32 | ``` 33 | 34 | ## Documentation 35 | 36 | The project contains [compiled documentation](https://scalameta.org/mdoc) which is located in [example/mdoc](example/mdoc). 37 | Please do update it with your changes and recompile it to check that everything is fine using the following command: 38 | 39 | ```bash 40 | sbt example/mdoc 41 | ``` 42 | 43 | You should definitely recompile [mdoc] documentation 44 | 45 | ## Conventional Commits 46 | 47 | The project uses [Conventional Commits](https://www.conventionalcommits.org) specification to have clear Git history and help with 48 | semantic versioning. Please read the specification and follow it (or look at already existing history for inspiration) if you commit into 49 | the project. 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Avast Software s.r.o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app-monix/src/main/scala/com/avast/sst/bundle/MonixResourceApp.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.bundle 2 | 3 | import cats.effect.{ExitCode, Resource} 4 | import monix.eval.{Task, TaskApp} 5 | import org.slf4j.LoggerFactory 6 | 7 | /** Extend this `trait` if you want to implement application using [[monix.eval.Task]] effect data type. 8 | * 9 | * Implement method `program` with initialization and business logic of your application. It will be automatically run and cleaned up. 10 | */ 11 | trait MonixResourceApp[A] extends TaskApp { 12 | 13 | private val logger = LoggerFactory.getLogger(this.getClass) 14 | 15 | def program: Resource[Task, A] 16 | 17 | override def run(args: List[String]): Task[ExitCode] = { 18 | program 19 | .use(_ => Task.unit) 20 | .redeem( 21 | ex => { 22 | logger.error("Application initialization failed!", ex) 23 | ExitCode.Error 24 | }, 25 | _ => ExitCode.Success 26 | ) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /app-monix/src/main/scala/com/avast/sst/bundle/MonixServerApp.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.bundle 2 | 3 | import cats.effect.{ExitCode, Resource} 4 | import monix.eval.{Task, TaskApp} 5 | import org.http4s.server.Server 6 | import org.slf4j.LoggerFactory 7 | 8 | /** Extend this `trait` if you want to implement server application using [[monix.eval.Task]] effect data type. 9 | * 10 | * Implement method `program` with initialization and business logic of your application. It will be automatically run until JVM is shut 11 | * down in which case all the resources are cleaned up because the whole `program` is a [[cats.effect.Resource]]. 12 | */ 13 | trait MonixServerApp extends TaskApp { 14 | 15 | private val logger = LoggerFactory.getLogger(this.getClass) 16 | 17 | def program: Resource[Task, Server] 18 | 19 | override def run(args: List[String]): Task[ExitCode] = { 20 | program 21 | .use { server => 22 | for { 23 | _ <- Task.delay(logger.info(s"Server started @ ${server.address.getHostString}:${server.address.getPort}")) 24 | _ <- Task.never[Unit] 25 | } yield server 26 | } 27 | .redeem( 28 | ex => { 29 | logger.error("Server initialization failed!", ex) 30 | ExitCode.Error 31 | }, 32 | _ => ExitCode.Success 33 | ) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app-zio/src/main/scala/com/avast/sst/bundle/ZioResourceApp.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.bundle 2 | 3 | import cats.effect.Resource 4 | import org.slf4j.LoggerFactory 5 | import zio.* 6 | import zio.interop.catz.* 7 | 8 | /** Extend this `trait` if you want to implement application using [[zio.ZIO]] effect data type. 9 | * 10 | * Implement method `program` with initialization and business logic of your application. It will be automatically run and cleaned up. 11 | */ 12 | trait ZioResourceApp[A] extends CatsApp { 13 | 14 | private val logger = LoggerFactory.getLogger(this.getClass) 15 | 16 | def program: Resource[Task, A] 17 | 18 | override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = { 19 | program 20 | .use(_ => Task.unit) 21 | .fold( 22 | ex => { 23 | logger.error("Application initialization failed!", ex) 24 | ExitCode.failure 25 | }, 26 | _ => ExitCode.success 27 | ) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app-zio/src/main/scala/com/avast/sst/bundle/ZioServerApp.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.bundle 2 | 3 | import cats.effect.Resource 4 | import org.http4s.server.Server 5 | import org.slf4j.LoggerFactory 6 | import zio.* 7 | import zio.interop.catz.* 8 | 9 | import scala.annotation.nowarn 10 | 11 | /** Extend this `trait` if you want to implement server application using [[zio.ZIO]] effect data type. 12 | * 13 | * Implement method `program` with initialization and business logic of your application. It will be automatically run until JVM is shut 14 | * down in which case all the resources are cleaned up because the whole `program` is a [[cats.effect.Resource]]. 15 | */ 16 | trait ZioServerApp extends CatsApp { 17 | 18 | private val logger = LoggerFactory.getLogger(this.getClass) 19 | 20 | def program: Resource[Task, Server] 21 | 22 | @nowarn("msg=dead code") 23 | override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = { 24 | program 25 | .use { server => 26 | for { 27 | _ <- UIO.effectTotal(logger.info(s"Server started @ ${server.address.getHostString}:${server.address.getPort}")) 28 | _ <- Task.never 29 | } yield server 30 | } 31 | .fold( 32 | ex => { 33 | logger.error("Server initialization failed!", ex) 34 | ExitCode.failure 35 | }, 36 | _ => ExitCode.success 37 | ) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /cassandra-datastax-driver-pureconfig/src/main/scala-2/com/avast/sst/datastax/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.datastax.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /cassandra-datastax-driver-pureconfig/src/main/scala-3/com/avast/sst/datastax/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.datastax.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /cassandra-datastax-driver/src/main/scala/com/avast/sst/datastax/DatastaxHelper.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.datastax 2 | 3 | import com.datastax.oss.driver.api.core.config.{DriverOption, ProgrammaticDriverConfigLoaderBuilder as DriverBuilder} 4 | 5 | import scala.concurrent.duration.Duration 6 | import scala.jdk.CollectionConverters.* 7 | 8 | /** Helper functions to construct Datastax session using Java builder. */ 9 | private[datastax] object DatastaxHelper { 10 | def stringProperty(opt: DriverOption)(value: String)(b: DriverBuilder): DriverBuilder = b.withString(opt, value) 11 | def intProperty(opt: DriverOption)(value: Int)(b: DriverBuilder): DriverBuilder = b.withInt(opt, value) 12 | def booleanProperty(opt: DriverOption)(value: Boolean)(b: DriverBuilder): DriverBuilder = b.withBoolean(opt, value) 13 | def durationProperty(opt: DriverOption)(value: Duration)(b: DriverBuilder): DriverBuilder = 14 | b.withDuration(opt, java.time.Duration.ofNanos(value.toNanos)) 15 | def stringListProperty(opt: DriverOption)(value: List[String])(b: DriverBuilder): DriverBuilder = b.withStringList(opt, value.asJava) 16 | def intListProperty(opt: DriverOption)(value: List[Int])(b: DriverBuilder): DriverBuilder = 17 | b.withIntList(opt, value.map(Integer.valueOf).asJava) 18 | def optional[T](f: T => DriverBuilder => DriverBuilder, value: Option[T])(b: DriverBuilder): DriverBuilder = 19 | value.map(f(_)(b)).getOrElse(b) 20 | } 21 | -------------------------------------------------------------------------------- /cassandra-datastax-driver/src/main/scala/com/avast/sst/datastax/config/CassandraDatastaxDriverConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.datastax.config 2 | 3 | /** Configuration for Cassandra Datastax Driver. */ 4 | final case class CassandraDatastaxDriverConfig( 5 | basic: BasicConfig, 6 | advanced: AdvancedConfig = AdvancedConfig.Default, 7 | profiles: List[ProfileConfig] = List.empty 8 | ) 9 | -------------------------------------------------------------------------------- /cassandra-datastax-driver/src/main/scala/com/avast/sst/datastax/config/package.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.datastax 2 | 3 | import scala.concurrent.duration.* 4 | 5 | package object config { 6 | val ConnectTimeout: Duration = 5.seconds 7 | val InitQueryTimeout: Duration = 5.seconds 8 | val RequestTimeout: Duration = 2.seconds 9 | val RequestPageSize: Int = 5000 10 | } 11 | -------------------------------------------------------------------------------- /cassandra-datastax-driver/src/main/scala/com/avast/sst/datastax/config/profile.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.datastax.config 2 | 3 | /** Profile configuration holding overridable properties. 4 | */ 5 | final case class ProfileConfig( 6 | name: String, 7 | basic: ProfileBasicConfig = ProfileBasicConfig.Default, 8 | advanced: ProfileAdvancedConfig = ProfileAdvancedConfig.Default 9 | ) 10 | 11 | final case class ProfileBasicConfig( 12 | request: BasicRequestConfig = ProfileBasicConfig.Default.request, 13 | loadBalancingPolicy: LoadBalancingPolicyConfig = ProfileBasicConfig.Default.loadBalancingPolicy 14 | ) 15 | 16 | object ProfileBasicConfig { 17 | val Default: ProfileBasicConfig = ProfileBasicConfig(BasicRequestConfig.Default, LoadBalancingPolicyConfig.Default) 18 | } 19 | 20 | final case class ProfileAdvancedConfig( 21 | request: ProfileAdvancedRequestConfig = ProfileAdvancedConfig.Default.request, 22 | retryPolicy: RetryPolicyConfig = ProfileAdvancedConfig.Default.retryPolicy, 23 | speculativeExecutionPolicy: SpeculativeExecutionPolicyConfig = ProfileAdvancedConfig.Default.speculativeExecutionPolicy, 24 | timestampGenerator: TimestampGeneratorConfig = ProfileAdvancedConfig.Default.timestampGenerator, 25 | preparedStatements: ProfilePreparedStatementsConfig = ProfileAdvancedConfig.Default.preparedStatements 26 | ) 27 | 28 | object ProfileAdvancedConfig { 29 | val Default: ProfileAdvancedConfig = ProfileAdvancedConfig( 30 | ProfileAdvancedRequestConfig.Default, 31 | RetryPolicyConfig.Default, 32 | SpeculativeExecutionPolicyConfig.Default, 33 | TimestampGeneratorConfig.Default, 34 | ProfilePreparedStatementsConfig.Default 35 | ) 36 | } 37 | 38 | final case class ProfileAdvancedRequestConfig( 39 | trace: TraceConfig = ProfileAdvancedRequestConfig.Default.trace, 40 | logWarnings: Boolean = ProfileAdvancedRequestConfig.Default.logWarnings 41 | ) 42 | 43 | object ProfileAdvancedRequestConfig { 44 | val Default: ProfileAdvancedRequestConfig = ProfileAdvancedRequestConfig(TraceConfig.Default, true) 45 | } 46 | 47 | final case class ProfilePreparedStatementsConfig(prepareOnAllNodes: Boolean) 48 | 49 | object ProfilePreparedStatementsConfig { 50 | val Default: ProfilePreparedStatementsConfig = ProfilePreparedStatementsConfig(true) 51 | } 52 | -------------------------------------------------------------------------------- /cats-effect/src/main/scala/com/avast/sst/catseffect/TimeUtils.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.catseffect 2 | 3 | import cats.effect.syntax.bracket.* 4 | import cats.effect.{Bracket, Clock, ExitCase} 5 | import cats.syntax.flatMap.* 6 | import cats.syntax.functor.* 7 | 8 | import java.util.concurrent.TimeUnit 9 | import scala.concurrent.duration.Duration 10 | 11 | object TimeUtils { 12 | 13 | private final val unit = TimeUnit.NANOSECONDS 14 | 15 | /** Measures the time it takes the effect to finish and records it using the provided function. */ 16 | def time[F[_], A](f: F[A])(record: Duration => F[Unit])(implicit F: Bracket[F, Throwable], C: Clock[F]): F[A] = { 17 | for { 18 | start <- C.monotonic(unit) 19 | result <- f.guarantee { 20 | C.monotonic(unit).map(computeTime(start)).flatMap(record) 21 | } 22 | } yield result 23 | } 24 | 25 | /** Measures the time it takes the effect to finish and records it using the provided function. It distinguishes between successful and 26 | * failure state. Please note, that in case of the effect cancellation the `record` is not invoked at all. 27 | */ 28 | def timeCase[F[_], A](f: F[A])(record: Either[Duration, Duration] => F[Unit])(implicit F: Bracket[F, Throwable], C: Clock[F]): F[A] = { 29 | def calculateAndRecordAs(start: Long)(wrap: Duration => Either[Duration, Duration]): F[Unit] = { 30 | C.monotonic(unit).map(computeTime(start)).flatMap(d => record(wrap(d))) 31 | } 32 | 33 | F.bracketCase(C.monotonic(unit))(_ => f) { 34 | case (start, ExitCase.Completed) => calculateAndRecordAs(start)(Right(_)) 35 | case (start, ExitCase.Error(_)) => calculateAndRecordAs(start)(Left(_)) 36 | case _ => F.unit 37 | } 38 | } 39 | 40 | private def computeTime(start: Long)(end: Long) = Duration.fromNanos(end - start) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /cats-effect/src/main/scala/com/avast/sst/catseffect/syntax/TimeSyntax.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.catseffect.syntax 2 | 3 | import cats.effect.{Bracket, Clock} 4 | import com.avast.sst.catseffect.TimeUtils 5 | import com.avast.sst.catseffect.syntax.TimeSyntax.FOps 6 | 7 | import scala.concurrent.duration.Duration 8 | 9 | trait TimeSyntax { 10 | 11 | @SuppressWarnings(Array("scalafix:DisableSyntax.implicitConversion")) 12 | implicit def sstFOps[F[_], A](f: F[A]): FOps[F, A] = new FOps(f) 13 | 14 | } 15 | 16 | object TimeSyntax { 17 | 18 | final class FOps[F[_], A](private val f: F[A]) extends AnyVal { 19 | 20 | /** Measures the time it takes the effect to finish and records it using the provided function. */ 21 | def time(record: Duration => F[Unit])(implicit F: Bracket[F, Throwable], C: Clock[F]): F[A] = TimeUtils.time(f)(record) 22 | 23 | /** Measures the time it takes the effect to finish and records it using the provided function. It distinguishes between successful and 24 | * failure state. Please note, that in case of the effect cancellation the `record` is not invoked at all. 25 | */ 26 | def timeCase(record: Either[Duration, Duration] => F[Unit])(implicit F: Bracket[F, Throwable], C: Clock[F]): F[A] = { 27 | TimeUtils.timeCase(f)(record) 28 | } 29 | 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /cats-effect/src/main/scala/com/avast/sst/catseffect/syntax/package.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.catseffect 2 | 3 | package object syntax { 4 | 5 | object time extends TimeSyntax 6 | 7 | } 8 | -------------------------------------------------------------------------------- /doobie-hikari-pureconfig/src/main/scala-2/com/avast/sst/doobie/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.doobie.pureconfig 2 | 3 | import cats.syntax.either._ 4 | import com.avast.sst.doobie.DoobieHikariConfig 5 | import doobie.enumerated.TransactionIsolation 6 | import pureconfig.ConfigReader 7 | import pureconfig.error.CannotConvert 8 | import pureconfig.generic.ProductHint 9 | import pureconfig.generic.semiauto._ 10 | 11 | trait ConfigReaders { 12 | 13 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 14 | 15 | implicit val doobieTransactionIsolationReader: ConfigReader[TransactionIsolation] = ConfigReader[String].emap { 16 | case "TRANSACTION_NONE" => TransactionIsolation.TransactionNone.asRight 17 | case "TRANSACTION_READ_UNCOMMITTED" => TransactionIsolation.TransactionReadUncommitted.asRight 18 | case "TRANSACTION_READ_COMMITTED" => TransactionIsolation.TransactionReadCommitted.asRight 19 | case "TRANSACTION_REPEATABLE_READ" => TransactionIsolation.TransactionRepeatableRead.asRight 20 | case "TRANSACTION_SERIALIZABLE" => TransactionIsolation.TransactionSerializable.asRight 21 | case unknown => Left(CannotConvert(unknown, "TransactionIsolation", "unknown value")) 22 | } 23 | 24 | implicit val doobieDoobieHikariConfigReader: ConfigReader[DoobieHikariConfig] = deriveReader[DoobieHikariConfig] 25 | 26 | } 27 | -------------------------------------------------------------------------------- /doobie-hikari-pureconfig/src/main/scala-2/com/avast/sst/doobie/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.doobie.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | override implicit protected def hint[T]: ProductHint[T] = ProductHint.default 10 | 11 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 12 | * 13 | * This is alias for the default `implicits._` import. 14 | */ 15 | object KebabCase extends ConfigReaders { 16 | override implicit protected def hint[T]: ProductHint[T] = ProductHint.default 17 | } 18 | 19 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 20 | object CamelCase extends ConfigReaders { 21 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /doobie-hikari-pureconfig/src/main/scala-3/com/avast/sst/doobie/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.doobie.pureconfig 2 | 3 | import cats.syntax.either.* 4 | import com.avast.sst.doobie.DoobieHikariConfig 5 | import doobie.enumerated.TransactionIsolation 6 | import pureconfig.ConfigReader 7 | import pureconfig.error.CannotConvert 8 | import pureconfig.generic.derivation.default.* 9 | 10 | trait ConfigReaders { 11 | 12 | implicit val doobieTransactionIsolationReader: ConfigReader[TransactionIsolation] = ConfigReader[String].emap { 13 | case "TRANSACTION_NONE" => TransactionIsolation.TransactionNone.asRight 14 | case "TRANSACTION_READ_UNCOMMITTED" => TransactionIsolation.TransactionReadUncommitted.asRight 15 | case "TRANSACTION_READ_COMMITTED" => TransactionIsolation.TransactionReadCommitted.asRight 16 | case "TRANSACTION_REPEATABLE_READ" => TransactionIsolation.TransactionRepeatableRead.asRight 17 | case "TRANSACTION_SERIALIZABLE" => TransactionIsolation.TransactionSerializable.asRight 18 | case unknown => Left(CannotConvert(unknown, "TransactionIsolation", "unknown value")) 19 | } 20 | 21 | implicit val doobieDoobieHikariConfigReader: ConfigReader[DoobieHikariConfig] = ConfigReader.derived 22 | 23 | } 24 | -------------------------------------------------------------------------------- /doobie-hikari-pureconfig/src/main/scala-3/com/avast/sst/doobie/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.doobie.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /doobie-hikari/src/main/scala/com/avast/sst/doobie/DoobieHikariConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.doobie 2 | 3 | import doobie.enumerated.TransactionIsolation 4 | 5 | import java.util.concurrent.TimeUnit 6 | import scala.concurrent.duration.FiniteDuration 7 | 8 | final case class DoobieHikariConfig( 9 | driver: String, 10 | url: String, 11 | username: String, 12 | password: String, 13 | connectionTimeout: FiniteDuration = FiniteDuration(30, TimeUnit.SECONDS), 14 | idleTimeout: FiniteDuration = FiniteDuration(10, TimeUnit.MINUTES), 15 | maxLifeTime: FiniteDuration = FiniteDuration(30, TimeUnit.MINUTES), 16 | minimumIdle: Int = 10, 17 | maximumPoolSize: Int = 10, 18 | readOnly: Boolean = false, 19 | leakDetectionThreshold: Option[FiniteDuration] = None, 20 | allowPoolSuspension: Boolean = false, 21 | initializationFailTimeout: Option[FiniteDuration] = None, 22 | isolateInternalQueries: Boolean = false, 23 | poolName: Option[String] = None, 24 | registerMBeans: Boolean = false, 25 | validationTimeout: Option[FiniteDuration] = None, 26 | transactionIsolation: Option[TransactionIsolation] = None, 27 | dataSourceProperties: Map[String, String] = Map.empty, 28 | autoCommit: Boolean = false 29 | ) 30 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # Scala Server Toolkit 2 | 3 | This is an example that shows how Scala Server Toolkit can be used to properly initialize a server application. 4 | 5 | Some of the "business logic" parts are simplified so please do not take them as the best example of how server application should be structured. 6 | 7 | You need to run Docker Compose to start up the environment for the application (database, ...): 8 | 9 | ```bash 10 | docker-compose -f example/docker-compose.yml up 11 | ``` 12 | 13 | Then you can just run the [Main](src/main/scala/com/avast/sst/example/Main.scala) class. 14 | -------------------------------------------------------------------------------- /example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | postgres: 2 | image: postgres:12 3 | ports: 4 | - '5432:5432' 5 | environment: 6 | - POSTGRES_USER=user 7 | - POSTGRES_PASSWORD=pass 8 | - POSTGRES_DB=database 9 | -------------------------------------------------------------------------------- /example/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen-address = "0.0.0.0" 3 | listen-port = 8080 4 | } 5 | 6 | database { 7 | driver = "org.postgresql.Driver" 8 | url = "jdbc:postgresql://localhost:5432/database" 9 | username = "user" 10 | password = "pass" 11 | } 12 | 13 | bounded-connect-executor { 14 | core-size = 16 15 | max-size = 16 16 | } 17 | 18 | client { 19 | } 20 | 21 | circuit-breaker { 22 | max-failures = 3 23 | reset-timeout = 30 s 24 | } 25 | 26 | jmx { 27 | domain = "com.avast.sst.example" 28 | enable-type-scope-name-hierarchy = true 29 | } 30 | -------------------------------------------------------------------------------- /example/src/main/scala-2/com/avast/sst/example/config/Configuration.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.example.config 2 | 3 | import com.avast.sst.doobie.DoobieHikariConfig 4 | import com.avast.sst.doobie.pureconfig.implicits._ 5 | import com.avast.sst.http4s.client.Http4sBlazeClientConfig 6 | import com.avast.sst.http4s.client.pureconfig.implicits._ 7 | import com.avast.sst.http4s.server.Http4sBlazeServerConfig 8 | import com.avast.sst.http4s.server.pureconfig.implicits._ 9 | import com.avast.sst.jvm.execution.ThreadPoolExecutorConfig 10 | import com.avast.sst.jvm.pureconfig.implicits._ 11 | import com.avast.sst.micrometer.jmx.MicrometerJmxConfig 12 | import com.avast.sst.micrometer.jmx.pureconfig.implicits._ 13 | import com.avast.sst.monix.catnap.CircuitBreakerConfig 14 | import com.avast.sst.monix.catnap.pureconfig.implicits._ 15 | import pureconfig.ConfigReader 16 | import pureconfig.generic.semiauto._ 17 | 18 | final case class Configuration( 19 | server: Http4sBlazeServerConfig, 20 | database: DoobieHikariConfig, 21 | boundedConnectExecutor: ThreadPoolExecutorConfig, 22 | client: Http4sBlazeClientConfig, 23 | circuitBreaker: CircuitBreakerConfig, 24 | jmx: MicrometerJmxConfig 25 | ) 26 | 27 | object Configuration { 28 | 29 | implicit val reader: ConfigReader[Configuration] = deriveReader[Configuration] 30 | 31 | } 32 | -------------------------------------------------------------------------------- /example/src/main/scala-3/com/avast/sst/example/config/Configuration.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.example.config 2 | 3 | import com.avast.sst.doobie.DoobieHikariConfig 4 | import com.avast.sst.doobie.pureconfig.implicits.* 5 | import com.avast.sst.http4s.client.Http4sBlazeClientConfig 6 | import com.avast.sst.http4s.client.pureconfig.implicits.* 7 | import com.avast.sst.http4s.server.Http4sBlazeServerConfig 8 | import com.avast.sst.http4s.server.pureconfig.implicits.* 9 | import com.avast.sst.jvm.execution.ThreadPoolExecutorConfig 10 | import com.avast.sst.jvm.pureconfig.implicits.* 11 | import com.avast.sst.micrometer.jmx.MicrometerJmxConfig 12 | import com.avast.sst.micrometer.jmx.pureconfig.implicits.* 13 | import com.avast.sst.monix.catnap.CircuitBreakerConfig 14 | import com.avast.sst.monix.catnap.pureconfig.implicits.* 15 | import pureconfig.ConfigReader 16 | import pureconfig.generic.derivation.default.* 17 | 18 | final case class Configuration( 19 | server: Http4sBlazeServerConfig, 20 | database: DoobieHikariConfig, 21 | boundedConnectExecutor: ThreadPoolExecutorConfig, 22 | client: Http4sBlazeClientConfig, 23 | circuitBreaker: CircuitBreakerConfig, 24 | jmx: MicrometerJmxConfig 25 | ) 26 | 27 | object Configuration { 28 | 29 | implicit val reader: ConfigReader[Configuration] = ConfigReader.derived 30 | 31 | } 32 | -------------------------------------------------------------------------------- /example/src/main/scala/com/avast/sst/example/module/Http4sRoutingModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.example.module 2 | 3 | import cats.implicits.* 4 | import com.avast.sst.example.service.RandomService 5 | import com.avast.sst.http4s.server.Http4sRouting 6 | import com.avast.sst.http4s.server.micrometer.MicrometerHttp4sServerMetricsModule 7 | import org.http4s.client.Client 8 | import org.http4s.dsl.Http4sDsl 9 | import org.http4s.{HttpApp, HttpRoutes} 10 | import zio.Task 11 | import zio.interop.catz.* 12 | 13 | class Http4sRoutingModule( 14 | randomService: RandomService, 15 | client: Client[Task], 16 | serverMetricsModule: MicrometerHttp4sServerMetricsModule[Task] 17 | ) extends Http4sDsl[Task] { 18 | 19 | import serverMetricsModule.* 20 | 21 | private val helloWorldRoute = routeMetrics.wrap("hello")(Ok("Hello World!")) 22 | 23 | private val routes = HttpRoutes.of[Task] { 24 | case GET -> Root / "hello" => helloWorldRoute 25 | case GET -> Root / "random" => randomService.randomNumber.map(_.show).flatMap(Ok(_)) 26 | case GET -> Root / "circuit-breaker" => client.expect[String]("https://httpbin.org/status/500").flatMap(Ok(_)) 27 | } 28 | 29 | val router: HttpApp[Task] = Http4sRouting.make { 30 | serverMetrics { 31 | routes 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /example/src/main/scala/com/avast/sst/example/service/RandomService.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.example.service 2 | 3 | import doobie.Fragment 4 | import doobie.implicits.* 5 | import doobie.util.transactor.Transactor 6 | import zio.Task 7 | import zio.interop.catz.* 8 | 9 | trait RandomService { 10 | 11 | def randomNumber: Task[Double] 12 | 13 | } 14 | 15 | object RandomService { 16 | 17 | def apply(transactor: Transactor[Task]): RandomService = 18 | new RandomService { 19 | override def randomNumber: Task[Double] = { 20 | Fragment 21 | .const("select random()") 22 | .query[Double] 23 | .unique 24 | .transact(transactor) 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /flyway-pureconfig/src/main/scala-2/com/avast/sst/flyway/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.flyway.pureconfig 2 | 3 | import cats.syntax.either._ 4 | import com.avast.sst.flyway.FlywayConfig 5 | import org.flywaydb.core.api.MigrationVersion 6 | import pureconfig.ConfigReader 7 | import pureconfig.error.ExceptionThrown 8 | import pureconfig.generic.ProductHint 9 | import pureconfig.generic.semiauto._ 10 | 11 | import java.nio.charset.Charset 12 | 13 | trait ConfigReaders { 14 | 15 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 16 | 17 | implicit private[pureconfig] val flywayCharsetReader: ConfigReader[Charset] = ConfigReader[String].emap { value => 18 | Either.catchNonFatal(Charset.forName(value)).leftMap(ExceptionThrown.apply) 19 | } 20 | 21 | implicit val flywayMigrationVersionReader: ConfigReader[MigrationVersion] = ConfigReader[String].map(MigrationVersion.fromVersion) 22 | 23 | implicit val flywayFlywayConfigReader: ConfigReader[FlywayConfig] = deriveReader[FlywayConfig] 24 | 25 | } 26 | -------------------------------------------------------------------------------- /flyway-pureconfig/src/main/scala-2/com/avast/sst/flyway/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.flyway.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | override implicit protected def hint[T]: ProductHint[T] = ProductHint.default 10 | 11 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 12 | * 13 | * This is alias for the default `implicits._` import. 14 | */ 15 | object KebabCase extends ConfigReaders { 16 | override implicit protected def hint[T]: ProductHint[T] = ProductHint.default 17 | } 18 | 19 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 20 | object CamelCase extends ConfigReaders { 21 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /flyway-pureconfig/src/main/scala-3/com/avast/sst/flyway/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.flyway.pureconfig 2 | 3 | import cats.syntax.either.* 4 | import com.avast.sst.flyway.FlywayConfig 5 | import org.flywaydb.core.api.MigrationVersion 6 | import pureconfig.ConfigReader 7 | import pureconfig.error.ExceptionThrown 8 | import pureconfig.generic.derivation.default.* 9 | import pureconfig.generic.derivation.default.* 10 | 11 | import java.nio.charset.Charset 12 | 13 | trait ConfigReaders { 14 | 15 | implicit private[pureconfig] val flywayCharsetReader: ConfigReader[Charset] = ConfigReader[String].emap { value => 16 | Either.catchNonFatal(Charset.forName(value)).leftMap(ExceptionThrown.apply) 17 | } 18 | 19 | implicit val flywayMigrationVersionReader: ConfigReader[MigrationVersion] = ConfigReader[String].map(MigrationVersion.fromVersion) 20 | 21 | implicit val flywayFlywayConfigReader: ConfigReader[FlywayConfig] = ConfigReader.derived 22 | 23 | } 24 | -------------------------------------------------------------------------------- /flyway-pureconfig/src/main/scala-3/com/avast/sst/flyway/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.flyway.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /flyway/src/main/scala/com/avast/sst/flyway/FlywayConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.flyway 2 | 3 | import org.flywaydb.core.api.MigrationVersion 4 | 5 | import java.nio.charset.{Charset, StandardCharsets} 6 | 7 | final case class FlywayConfig( 8 | baselineOnMigrate: Boolean = false, 9 | baselineVersion: Option[MigrationVersion] = None, 10 | targetVersion: Option[MigrationVersion] = None, 11 | baselineDescription: Option[String] = None, 12 | cleanDisabled: Boolean = false, 13 | cleanOnValidationError: Boolean = false, 14 | connectRetries: Int = 0, 15 | encoding: Charset = StandardCharsets.UTF_8, 16 | group: Boolean = false, 17 | ignoreMigrationPatterns: List[String] = List.empty, 18 | installedBy: Option[String] = None, 19 | mixed: Boolean = false, 20 | locations: List[String] = List.empty, 21 | outOfOrder: Boolean = false, 22 | validateOnMigrate: Boolean = true, 23 | placeholderReplacement: Boolean = true, 24 | placeholders: Map[String, String] = Map.empty 25 | ) 26 | -------------------------------------------------------------------------------- /flyway/src/main/scala/com/avast/sst/flyway/FlywayModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.flyway 2 | 3 | import cats.effect.Sync 4 | import org.flywaydb.core.Flyway 5 | 6 | import javax.sql.DataSource 7 | import scala.jdk.CollectionConverters.* 8 | 9 | object FlywayModule { 10 | 11 | /** Makes [[org.flywaydb.core.Flyway]] from the given `javax.sql.DataSource` and config. */ 12 | def make[F[_]: Sync](dataSource: DataSource, config: FlywayConfig): F[Flyway] = { 13 | Sync[F].delay { 14 | val builder = Flyway.configure 15 | .dataSource(dataSource) 16 | .baselineOnMigrate(config.baselineOnMigrate) 17 | .cleanDisabled(config.cleanDisabled) 18 | .cleanOnValidationError(config.cleanOnValidationError) 19 | .connectRetries(config.connectRetries) 20 | .encoding(config.encoding) 21 | .group(config.group) 22 | .ignoreMigrationPatterns(config.ignoreMigrationPatterns*) 23 | .mixed(config.mixed) 24 | .outOfOrder(config.outOfOrder) 25 | .validateOnMigrate(config.validateOnMigrate) 26 | .placeholderReplacement(config.placeholderReplacement) 27 | .placeholders(config.placeholders.asJava) 28 | 29 | config.baselineVersion.foreach(builder.baselineVersion) 30 | config.targetVersion.foreach(builder.target) 31 | config.baselineDescription.foreach(builder.baselineDescription) 32 | config.installedBy.foreach(builder.installedBy) 33 | if (config.locations.nonEmpty) builder.locations(config.locations*) 34 | 35 | builder.load() 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /fs2-kafka-pureconfig/src/main/scala-2/com/avast/sst/fs2kafka/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.fs2kafka.pureconfig 2 | 3 | import cats.syntax.either._ 4 | import com.avast.sst.fs2kafka.{ConsumerConfig, ProducerConfig} 5 | import fs2.kafka.{Acks, AutoOffsetReset, CommitRecovery, IsolationLevel} 6 | import pureconfig.ConfigReader 7 | import pureconfig.error.CannotConvert 8 | import pureconfig.generic.ProductHint 9 | import pureconfig.generic.semiauto._ 10 | 11 | @SuppressWarnings(Array("scalafix:DisableSyntax.==")) 12 | trait ConfigReaders { 13 | 14 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 15 | 16 | implicit val fs2KafkaCommitRecoveryConfigReader: ConfigReader[CommitRecovery] = ConfigReader[String].emap { 17 | case s if s.toLowerCase() == "default" => CommitRecovery.Default.asRight 18 | case s if s.toLowerCase() == "none" => CommitRecovery.None.asRight 19 | case value => CannotConvert(value, "CommitRecovery", "default|none").asLeft 20 | } 21 | 22 | implicit val fs2KafkaAutoOffsetResetConfigReader: ConfigReader[AutoOffsetReset] = ConfigReader[String].emap { 23 | case s if s.toLowerCase() == "earliest" => AutoOffsetReset.Earliest.asRight 24 | case s if s.toLowerCase() == "latest" => AutoOffsetReset.Latest.asRight 25 | case s if s.toLowerCase() == "none" => AutoOffsetReset.None.asRight 26 | case value => CannotConvert(value, "AutoOffsetReset", "earliest|latest|none").asLeft 27 | } 28 | 29 | implicit val fs2KafkaIsolationLevelConfigReader: ConfigReader[IsolationLevel] = ConfigReader[String].emap { 30 | case s if s.toLowerCase() == "read_committed" => IsolationLevel.ReadCommitted.asRight 31 | case s if s.toLowerCase() == "read_uncommitted" => IsolationLevel.ReadUncommitted.asRight 32 | case value => CannotConvert(value, "IsolationLevel", "read_committed|read_uncommitted").asLeft 33 | } 34 | 35 | implicit val fs2KafkaAcksConfigReader: ConfigReader[Acks] = ConfigReader[String].emap { 36 | case s if s.toLowerCase() == "0" => Acks.Zero.asRight 37 | case s if s.toLowerCase() == "1" => Acks.One.asRight 38 | case s if s.toLowerCase() == "all" => Acks.All.asRight 39 | case value => CannotConvert(value, "Acks", "0|1|all").asLeft 40 | } 41 | 42 | implicit val fs2KafkaConsumerConfigReader: ConfigReader[ConsumerConfig] = deriveReader[ConsumerConfig] 43 | 44 | implicit val fs2KafkaProducerConfigReader: ConfigReader[ProducerConfig] = deriveReader[ProducerConfig] 45 | 46 | } 47 | -------------------------------------------------------------------------------- /fs2-kafka-pureconfig/src/main/scala-2/com/avast/sst/fs2kafka/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.fs2kafka.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /fs2-kafka-pureconfig/src/main/scala-3/com/avast/sst/fs2kafka/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.fs2kafka.pureconfig 2 | 3 | import cats.syntax.either.* 4 | import com.avast.sst.fs2kafka.{ConsumerConfig, ProducerConfig} 5 | import fs2.kafka.{Acks, AutoOffsetReset, CommitRecovery, IsolationLevel} 6 | import pureconfig.ConfigReader 7 | import pureconfig.error.CannotConvert 8 | import pureconfig.generic.derivation.default.* 9 | 10 | @SuppressWarnings(Array("scalafix:DisableSyntax.==")) 11 | trait ConfigReaders { 12 | 13 | implicit val fs2KafkaCommitRecoveryConfigReader: ConfigReader[CommitRecovery] = ConfigReader[String].emap { 14 | case s if s.toLowerCase() == "default" => CommitRecovery.Default.asRight 15 | case s if s.toLowerCase() == "none" => CommitRecovery.None.asRight 16 | case value => CannotConvert(value, "CommitRecovery", "default|none").asLeft 17 | } 18 | 19 | implicit val fs2KafkaAutoOffsetResetConfigReader: ConfigReader[AutoOffsetReset] = ConfigReader[String].emap { 20 | case s if s.toLowerCase() == "earliest" => AutoOffsetReset.Earliest.asRight 21 | case s if s.toLowerCase() == "latest" => AutoOffsetReset.Latest.asRight 22 | case s if s.toLowerCase() == "none" => AutoOffsetReset.None.asRight 23 | case value => CannotConvert(value, "AutoOffsetReset", "earliest|latest|none").asLeft 24 | } 25 | 26 | implicit val fs2KafkaIsolationLevelConfigReader: ConfigReader[IsolationLevel] = ConfigReader[String].emap { 27 | case s if s.toLowerCase() == "read_committed" => IsolationLevel.ReadCommitted.asRight 28 | case s if s.toLowerCase() == "read_uncommitted" => IsolationLevel.ReadUncommitted.asRight 29 | case value => CannotConvert(value, "IsolationLevel", "read_committed|read_uncommitted").asLeft 30 | } 31 | 32 | implicit val fs2KafkaAcksConfigReader: ConfigReader[Acks] = ConfigReader[String].emap { 33 | case s if s.toLowerCase() == "0" => Acks.Zero.asRight 34 | case s if s.toLowerCase() == "1" => Acks.One.asRight 35 | case s if s.toLowerCase() == "all" => Acks.All.asRight 36 | case value => CannotConvert(value, "Acks", "0|1|all").asLeft 37 | } 38 | 39 | implicit val fs2KafkaConsumerConfigReader: ConfigReader[ConsumerConfig] = ConfigReader.derived 40 | 41 | implicit val fs2KafkaProducerConfigReader: ConfigReader[ProducerConfig] = ConfigReader.derived 42 | 43 | } 44 | -------------------------------------------------------------------------------- /fs2-kafka-pureconfig/src/main/scala-3/com/avast/sst/fs2kafka/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.fs2kafka.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /fs2-kafka/src/main/scala/com/avast/sst/fs2kafka/ConsumerConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.fs2kafka 2 | 3 | import com.avast.sst.fs2kafka.ConsumerConfig.* 4 | import fs2.kafka.{AutoOffsetReset, CommitRecovery, IsolationLevel} 5 | import org.apache.kafka.clients.consumer.{ConsumerConfig as ApacheConsumerConfig} 6 | 7 | import java.util.concurrent.TimeUnit.{MILLISECONDS, SECONDS} 8 | import scala.annotation.nowarn 9 | import scala.concurrent.duration.FiniteDuration 10 | import scala.jdk.CollectionConverters.* 11 | 12 | @nowarn("msg=dead code") 13 | final case class ConsumerConfig( 14 | bootstrapServers: List[String], 15 | groupId: String, 16 | groupInstanceId: Option[String] = None, 17 | clientId: Option[String] = None, 18 | clientRack: Option[String] = None, 19 | autoOffsetReset: AutoOffsetReset = AutoOffsetReset.None, 20 | enableAutoCommit: Boolean = false, 21 | autoCommitInterval: FiniteDuration = defaultMillis(ApacheConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG), 22 | allowAutoCreateTopics: Boolean = default(ApacheConsumerConfig.ALLOW_AUTO_CREATE_TOPICS_CONFIG), 23 | closeTimeout: FiniteDuration = FiniteDuration(20, SECONDS), 24 | commitRecovery: CommitRecovery = CommitRecovery.Default, 25 | commitTimeout: FiniteDuration = FiniteDuration(15, SECONDS), 26 | defaultApiTimeout: FiniteDuration = defaultMillis(ApacheConsumerConfig.DEFAULT_API_TIMEOUT_MS_CONFIG), 27 | heartbeatInterval: FiniteDuration = defaultMillis(ApacheConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG), 28 | isolationLevel: IsolationLevel = defaultIsolationLevel, 29 | maxPrefetchBatches: Int = 2, 30 | pollInterval: FiniteDuration = FiniteDuration(50, MILLISECONDS), 31 | pollTimeout: FiniteDuration = FiniteDuration(50, MILLISECONDS), 32 | maxPollInterval: FiniteDuration = defaultMillis(ApacheConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG), 33 | maxPollRecords: Int = default(ApacheConsumerConfig.MAX_POLL_RECORDS_CONFIG), 34 | requestTimeout: FiniteDuration = defaultMillis(ApacheConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG), 35 | sessionTimeout: FiniteDuration = defaultMillis(ApacheConsumerConfig.SESSION_TIMEOUT_MS_CONFIG), 36 | properties: Map[String, String] = Map.empty 37 | ) 38 | 39 | object ConsumerConfig { 40 | 41 | private val officialDefaults = ApacheConsumerConfig.configDef().defaultValues().asScala 42 | 43 | @SuppressWarnings(Array("scalafix:DisableSyntax.asInstanceOf")) 44 | private def default[A](key: String): A = officialDefaults(key).asInstanceOf[A] 45 | 46 | private def defaultMillis(key: String): FiniteDuration = FiniteDuration(default[Int](key).toLong, MILLISECONDS) 47 | 48 | private val defaultIsolationLevel = default[String](ApacheConsumerConfig.ISOLATION_LEVEL_CONFIG) match { 49 | case "read_uncommitted" => IsolationLevel.ReadUncommitted 50 | case "read_committed" => IsolationLevel.ReadCommitted 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /fs2-kafka/src/main/scala/com/avast/sst/fs2kafka/ProducerConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.fs2kafka 2 | 3 | import com.avast.sst.fs2kafka.ProducerConfig.* 4 | import fs2.kafka.Acks 5 | import org.apache.kafka.clients.producer.{ProducerConfig as ApacheProducerConfig} 6 | 7 | import java.util.concurrent.TimeUnit.{MILLISECONDS, SECONDS} 8 | import scala.concurrent.duration.FiniteDuration 9 | import scala.jdk.CollectionConverters.* 10 | 11 | final case class ProducerConfig( 12 | bootstrapServers: List[String], 13 | clientId: Option[String] = None, 14 | acks: Acks = defaultAcks, 15 | batchSize: Int = default[Int](ApacheProducerConfig.BATCH_SIZE_CONFIG), 16 | closeTimeout: FiniteDuration = FiniteDuration(60, SECONDS), 17 | deliveryTimeout: FiniteDuration = defaultMillis(ApacheProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG), 18 | requestTimeout: FiniteDuration = defaultMillis(ApacheProducerConfig.REQUEST_TIMEOUT_MS_CONFIG), 19 | linger: FiniteDuration = defaultMillisLong(ApacheProducerConfig.LINGER_MS_CONFIG), 20 | enableIdempotence: Boolean = default[Boolean](ApacheProducerConfig.ENABLE_IDEMPOTENCE_CONFIG), 21 | maxInFlightRequestsPerConnection: Int = default[Int](ApacheProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION), 22 | parallelism: Int = 100, 23 | retries: Int = 0, 24 | properties: Map[String, String] = Map.empty 25 | ) 26 | 27 | object ProducerConfig { 28 | 29 | private val officialDefaults = ApacheProducerConfig.configDef().defaultValues().asScala 30 | 31 | @SuppressWarnings(Array("scalafix:DisableSyntax.asInstanceOf")) 32 | private def default[A](key: String): A = officialDefaults(key).asInstanceOf[A] 33 | 34 | private def defaultMillis(key: String): FiniteDuration = FiniteDuration(default[Int](key).toLong, MILLISECONDS) 35 | private def defaultMillisLong(key: String): FiniteDuration = FiniteDuration(default[Long](key), MILLISECONDS) 36 | 37 | private val defaultAcks = default[String](ApacheProducerConfig.ACKS_CONFIG) match { 38 | case "all" => Acks.All 39 | case "0" => Acks.Zero 40 | case "1" => Acks.One 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /fs2-kafka/src/test/scala-2/com/avast/sst/fs2kafka/Fs2KafkaModuleTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.fs2kafka 2 | 3 | import cats.effect.{ContextShift, IO, Resource, Timer} 4 | import cats.syntax.flatMap.* 5 | import com.dimafeng.testcontainers.{ForAllTestContainer, KafkaContainer} 6 | import fs2.kafka.{AutoOffsetReset, ProducerRecord, ProducerRecords} 7 | import org.scalatest.funsuite.AsyncFunSuite 8 | 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | 11 | class Fs2KafkaModuleTest extends AsyncFunSuite with ForAllTestContainer { 12 | 13 | override val container: KafkaContainer = KafkaContainer() 14 | 15 | implicit private val cs: ContextShift[IO] = IO.contextShift(global) 16 | implicit private val timer: Timer[IO] = IO.timer(global) 17 | 18 | test("producer") { 19 | val io = for { 20 | producer <- Fs2KafkaModule.makeProducer[IO, String, String](ProducerConfig(List(container.bootstrapServers))) 21 | consumer <- Fs2KafkaModule.makeConsumer[IO, String, String]( 22 | ConsumerConfig(List(container.bootstrapServers), groupId = "test", autoOffsetReset = AutoOffsetReset.Earliest) 23 | ) 24 | _ <- Resource.eval(consumer.subscribeTo("test")) 25 | _ <- Resource.eval(producer.produce(ProducerRecords.one(ProducerRecord("test", "key", "value"))).flatten) 26 | event <- Resource.eval(consumer.stream.head.compile.toList) 27 | } yield assert(event.head.record.key === "key" && event.head.record.value === "value") 28 | 29 | io.use(IO.pure).unsafeToFuture() 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /fs2-kafka/src/test/scala-2/com/avast/sst/fs2kafka/KafkaConfigTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.fs2kafka 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | import scala.annotation.nowarn 6 | 7 | @nowarn("msg=unused value") 8 | class KafkaConfigTest extends AnyFunSuite { 9 | 10 | test("verify ConsumerConfig defaults") { 11 | ConsumerConfig(List.empty, "group.id") 12 | succeed 13 | } 14 | 15 | test("verify ProducerConfig defaults") { 16 | ProducerConfig(List.empty) 17 | succeed 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /grpc-server-micrometer/src/main/scala/com/avast/sst/grpc/server/micrometer/MonitoringServerInterceptor.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.grpc.server.micrometer 2 | 3 | import io.grpc.ForwardingServerCall.SimpleForwardingServerCall 4 | import io.grpc.* 5 | import io.micrometer.core.instrument.{MeterRegistry, Timer} 6 | 7 | import java.util.concurrent.atomic.AtomicLong 8 | import java.util.concurrent.{ConcurrentHashMap, TimeUnit} 9 | 10 | /** Records important gRPC call metrics in [[io.micrometer.core.instrument.MeterRegistry]]. 11 | * 12 | * Metrics: 13 | * - grpc.`full-method-name`.current-calls 14 | * - grpc.`full-method-name`.successes 15 | * - grpc.`full-method-name`.failures 16 | */ 17 | class MonitoringServerInterceptor(meterRegistry: MeterRegistry) extends ServerInterceptor { 18 | 19 | private val gaugeCache = new ConcurrentHashMap[String, AtomicLong]() 20 | private val timerCache = new ConcurrentHashMap[String, Timer]() 21 | 22 | override def interceptCall[ReqT, RespT]( 23 | call: ServerCall[ReqT, RespT], 24 | headers: Metadata, 25 | next: ServerCallHandler[ReqT, RespT] 26 | ): ServerCall.Listener[ReqT] = { 27 | val prefix = s"grpc.${call.getMethodDescriptor.getFullMethodName.replace('/', '.')}" 28 | val currentCallsCounter = makeGauge(s"$prefix.current-calls") 29 | currentCallsCounter.incrementAndGet() 30 | val start = System.nanoTime 31 | val newCall = new CloseServerCall(prefix, start, currentCallsCounter, call) 32 | next.startCall(newCall, headers) 33 | } 34 | 35 | private class CloseServerCall[A, B](prefix: String, start: Long, currentCallsCounter: AtomicLong, delegate: ServerCall[A, B]) 36 | extends SimpleForwardingServerCall[A, B](delegate) { 37 | override def close(status: Status, trailers: Metadata): Unit = { 38 | currentCallsCounter.decrementAndGet() 39 | val durationNs = System.nanoTime - start 40 | if (status.isOk) { 41 | makeTimer(s"$prefix.successes").record(durationNs, TimeUnit.NANOSECONDS) 42 | } else { 43 | makeTimer(s"$prefix.failures").record(durationNs, TimeUnit.NANOSECONDS) 44 | } 45 | super.close(status, trailers) 46 | } 47 | } 48 | 49 | private def makeGauge(name: String): AtomicLong = { 50 | gaugeCache.computeIfAbsent( 51 | name, 52 | n => { 53 | val counter = new AtomicLong() 54 | meterRegistry.gauge(n, counter) 55 | counter 56 | } 57 | ) 58 | } 59 | 60 | private def makeTimer(name: String): Timer = timerCache.computeIfAbsent(name, meterRegistry.timer(_)) 61 | 62 | } 63 | -------------------------------------------------------------------------------- /grpc-server-pureconfig/src/main/scala-2/com/avast/sst/grpc/server/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.grpc.server.pureconfig 2 | 3 | import com.avast.sst.grpc.server.GrpcServerConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.ProductHint 6 | import pureconfig.generic.semiauto._ 7 | 8 | trait ConfigReaders { 9 | 10 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 11 | 12 | implicit val grpcServerGrpcServerConfigReader: ConfigReader[GrpcServerConfig] = deriveReader[GrpcServerConfig] 13 | 14 | } 15 | -------------------------------------------------------------------------------- /grpc-server-pureconfig/src/main/scala-2/com/avast/sst/grpc/server/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.grpc.server.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /grpc-server-pureconfig/src/main/scala-3/com/avast/sst/grpc/server/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.grpc.server.pureconfig 2 | 3 | import com.avast.sst.grpc.server.GrpcServerConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.derivation.default.* 6 | 7 | trait ConfigReaders { 8 | 9 | implicit val grpcServerGrpcServerConfigReader: ConfigReader[GrpcServerConfig] = ConfigReader.derived 10 | 11 | } 12 | -------------------------------------------------------------------------------- /grpc-server-pureconfig/src/main/scala-3/com/avast/sst/grpc/server/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.grpc.server.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /grpc-server/src/main/scala/com/avast/sst/grpc/server/GrpcServerConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.grpc.server 2 | 3 | import java.util.concurrent.TimeUnit 4 | import scala.concurrent.duration.Duration 5 | 6 | final case class GrpcServerConfig( 7 | port: Int, 8 | handshakeTimeout: Duration = Duration(120, TimeUnit.SECONDS), 9 | maxInboundMessageSize: Int = 4 * 1024 * 1024, 10 | maxInboundMetadataSize: Int = 8192, 11 | serverShutdownTimeout: Duration = Duration(10, TimeUnit.SECONDS) 12 | ) 13 | -------------------------------------------------------------------------------- /grpc-server/src/main/scala/com/avast/sst/grpc/server/GrpcServerModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.grpc.server 2 | 3 | import cats.effect.{Resource, Sync} 4 | import io.grpc.{Server, ServerBuilder, ServerInterceptor, ServerServiceDefinition} 5 | 6 | import java.util.concurrent.TimeUnit 7 | import scala.collection.immutable.Seq 8 | import scala.concurrent.ExecutionContext 9 | 10 | object GrpcServerModule { 11 | 12 | /** Makes [[io.grpc.Server]] (Netty) initialized with the given config, services and interceptors. 13 | * 14 | * @param services 15 | * service implementations to be added to the handler registry 16 | * @param executionContext 17 | * executor to be used for the server 18 | * @param interceptors 19 | * that are run for all the services 20 | */ 21 | def make[F[_]: Sync]( 22 | config: GrpcServerConfig, 23 | services: Seq[ServerServiceDefinition], 24 | executionContext: ExecutionContext, 25 | interceptors: Seq[ServerInterceptor] = List.empty 26 | ): Resource[F, Server] = 27 | Resource.make { 28 | Sync[F].delay { 29 | val builder = ServerBuilder 30 | .forPort(config.port) 31 | .handshakeTimeout(config.handshakeTimeout.toMillis, TimeUnit.MILLISECONDS) 32 | .maxInboundMessageSize(config.maxInboundMessageSize) 33 | .maxInboundMetadataSize(config.maxInboundMetadataSize) 34 | .executor(executionContext.execute(_)) 35 | 36 | services.foreach(builder.addService) 37 | interceptors.foreach(builder.intercept) 38 | 39 | builder.build.start() 40 | } 41 | } { s => 42 | Sync[F].delay { 43 | s.shutdown().awaitTermination(config.serverShutdownTimeout.toMillis, TimeUnit.MILLISECONDS) 44 | () 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /grpc-server/src/main/scala/com/avast/sst/grpc/server/interceptor/LoggingServerInterceptor.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.grpc.server.interceptor 2 | 3 | import io.grpc.ForwardingServerCall.SimpleForwardingServerCall 4 | import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener 5 | import io.grpc.* 6 | import org.slf4j.Logger 7 | 8 | import scala.annotation.nowarn 9 | 10 | /** Adds basic logging around each gRPC call. */ 11 | class LoggingServerInterceptor(logger: Logger) extends ServerInterceptor { 12 | 13 | override def interceptCall[ReqT, RespT]( 14 | call: ServerCall[ReqT, RespT], 15 | headers: Metadata, 16 | next: ServerCallHandler[ReqT, RespT] 17 | ): ServerCall.Listener[ReqT] = { 18 | val methodName = call.getMethodDescriptor.getFullMethodName 19 | val finalCall = new CloseServerCall(methodName, call) 20 | new OnMessageServerCallListener(methodName, next.startCall(finalCall, headers)) 21 | } 22 | 23 | @nowarn("msg=a type was inferred to be `Object`") 24 | private class CloseServerCall[A, B](methodName: String, delegate: ServerCall[A, B]) extends SimpleForwardingServerCall[A, B](delegate) { 25 | override def close(status: Status, trailers: Metadata): Unit = { 26 | import io.grpc.Status 27 | if ((status.getCode eq Status.Code.UNKNOWN) || (status.getCode eq Status.Code.INTERNAL)) { 28 | logger.error( 29 | String.format( 30 | "Error response from method %s: %s %s", 31 | methodName, 32 | status.getCode, 33 | status.getDescription 34 | ), 35 | status.getCause 36 | ) 37 | } else if (!status.isOk) { 38 | logger.warn( 39 | String.format( 40 | "Error response from method %s: %s %s", 41 | methodName, 42 | status.getCode, 43 | status.getDescription 44 | ), 45 | status.getCause 46 | ) 47 | } else { 48 | logger.debug("Successful response from method {}: {}", Array(methodName, status)*) 49 | } 50 | super.close(status, trailers) 51 | } 52 | } 53 | 54 | private class OnMessageServerCallListener[A](methodName: String, delegate: ServerCall.Listener[A]) 55 | extends SimpleForwardingServerCallListener[A](delegate) { 56 | override def onMessage(message: A): Unit = { 57 | logger.debug("Dispatching method {}", methodName) 58 | super.onMessage(message) 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /http4s-client-blaze-pureconfig/src/main/scala-2/com/avast/sst/http4s/client/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client.pureconfig 2 | 3 | import cats.syntax.either.* 4 | import com.avast.sst.http4s.client.Http4sBlazeClientConfig 5 | import com.avast.sst.http4s.client.Http4sBlazeClientConfig.SocketOptions 6 | import org.http4s.blaze.client.ParserMode 7 | import org.http4s.headers.`User-Agent` 8 | import pureconfig.ConfigReader 9 | import pureconfig.error.CannotConvert 10 | import pureconfig.generic.ProductHint 11 | import pureconfig.generic.semiauto.* 12 | 13 | trait ConfigReaders { 14 | 15 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 16 | 17 | implicit val http4sClientUserAgentReader: ConfigReader[`User-Agent`] = ConfigReader[String].emap { value => 18 | `User-Agent`.parse(value).leftMap { parseFailure => CannotConvert(value, "User-Agent HTTP header", parseFailure.message) } 19 | } 20 | 21 | implicit val http4sClientSocketOptionsReader: ConfigReader[SocketOptions] = deriveReader[SocketOptions] 22 | 23 | implicit val http4sClientParserModeReader: ConfigReader[ParserMode] = deriveEnumerationReader 24 | 25 | implicit val http4sClientHttp4sBlazeClientConfigReader: ConfigReader[Http4sBlazeClientConfig] = deriveReader[Http4sBlazeClientConfig] 26 | 27 | } 28 | -------------------------------------------------------------------------------- /http4s-client-blaze-pureconfig/src/main/scala-2/com/avast/sst/http4s/client/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /http4s-client-blaze-pureconfig/src/main/scala-3/com/avast/sst/http4s/client/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client.pureconfig 2 | 3 | import cats.syntax.either.* 4 | import com.avast.sst.http4s.client.Http4sBlazeClientConfig 5 | import com.avast.sst.http4s.client.Http4sBlazeClientConfig.SocketOptions 6 | import org.http4s.blaze.client.ParserMode 7 | import org.http4s.headers.`User-Agent` 8 | import pureconfig.ConfigReader 9 | import pureconfig.error.CannotConvert 10 | import pureconfig.generic.derivation.default.* 11 | 12 | trait ConfigReaders { 13 | 14 | implicit val http4sClientUserAgentReader: ConfigReader[`User-Agent`] = ConfigReader[String].emap { value => 15 | `User-Agent`.parse(value).leftMap { parseFailure => CannotConvert(value, "User-Agent HTTP header", parseFailure.message) } 16 | } 17 | 18 | implicit val http4sClientSocketOptionsReader: ConfigReader[SocketOptions] = ConfigReader.derived 19 | 20 | implicit val http4sClientParserModeReader: ConfigReader[ParserMode] = ConfigReader.derived 21 | 22 | implicit val http4sClientHttp4sBlazeClientConfigReader: ConfigReader[Http4sBlazeClientConfig] = 23 | ConfigReader.derived 24 | 25 | } 26 | -------------------------------------------------------------------------------- /http4s-client-blaze-pureconfig/src/main/scala-3/com/avast/sst/http4s/client/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /http4s-client-blaze/src/main/scala/com/avast/sst/http4s/client/Http4sBlazeClientConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client 2 | 3 | import com.avast.sst.http4s.client.Http4sBlazeClientConfig.SocketOptions 4 | import org.http4s.blaze.client.ParserMode 5 | import org.http4s.client.defaults 6 | import org.http4s.headers.`User-Agent` 7 | import org.http4s.{BuildInfo, ProductComment, ProductId} 8 | 9 | import java.util.concurrent.TimeUnit 10 | import scala.concurrent.duration.{Duration, FiniteDuration} 11 | 12 | final case class Http4sBlazeClientConfig( 13 | responseHeaderTimeout: Duration = Duration.Inf, 14 | idleTimeout: FiniteDuration = Duration(1, TimeUnit.MINUTES), 15 | requestTimeout: FiniteDuration = defaults.RequestTimeout, 16 | connectTimeout: FiniteDuration = defaults.ConnectTimeout, 17 | userAgent: `User-Agent` = `User-Agent`(ProductId("http4s-blaze-client", Some(BuildInfo.version)), List(ProductComment("Server"))), 18 | maxTotalConnections: Int = 10, 19 | maxWaitQueueLimit: Int = 256, 20 | maxConnectionsPerRequestkey: Int = 256, 21 | checkEndpointIdentification: Boolean = true, 22 | maxResponseLineSize: Int = 4 * 1024, 23 | maxHeaderLength: Int = 40 * 1024, 24 | maxChunkSize: Int = Int.MaxValue, 25 | chunkBufferMaxSize: Int = 1024 * 1024, 26 | parserMode: ParserMode = ParserMode.Strict, 27 | bufferSize: Int = 8192, 28 | socketOptions: Option[SocketOptions] = None 29 | ) 30 | 31 | object Http4sBlazeClientConfig { 32 | final case class SocketOptions( 33 | reuseAddress: Boolean = true, 34 | sendBufferSize: Int = 256 * 1024, 35 | receiveBufferSize: Int = 256 * 1024, 36 | keepAlive: Boolean = false, 37 | noDelay: Boolean = false 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /http4s-client-blaze/src/main/scala/com/avast/sst/http4s/client/Http4sBlazeClientModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client 2 | 3 | import cats.effect.{ConcurrentEffect, Resource} 4 | import com.avast.sst.http4s.client.Http4sBlazeClientConfig.SocketOptions 5 | import org.http4s.blaze.channel.{ChannelOptions, OptionValue} 6 | import org.http4s.blaze.client.BlazeClientBuilder 7 | import org.http4s.client.Client 8 | 9 | import java.net.StandardSocketOptions 10 | import javax.net.ssl.SSLContext 11 | import scala.concurrent.ExecutionContext 12 | object Http4sBlazeClientModule { 13 | 14 | /** Makes [[org.http4s.client.Client]] (Blaze) initialized with the given config. 15 | * 16 | * @param executionContext 17 | * callback handling [[scala.concurrent.ExecutionContext]] 18 | */ 19 | def make[F[_]: ConcurrentEffect]( 20 | config: Http4sBlazeClientConfig, 21 | executionContext: ExecutionContext, 22 | sslContext: Option[SSLContext] = None 23 | ): Resource[F, Client[F]] = { 24 | val builder = BlazeClientBuilder[F](executionContext) 25 | .withResponseHeaderTimeout(config.responseHeaderTimeout) 26 | .withIdleTimeout(config.idleTimeout) 27 | .withRequestTimeout(config.requestTimeout) 28 | .withConnectTimeout(config.connectTimeout) 29 | .withUserAgent(config.userAgent) 30 | .withMaxTotalConnections(config.maxTotalConnections) 31 | .withMaxWaitQueueLimit(config.maxWaitQueueLimit) 32 | .withMaxConnectionsPerRequestKey(Function.const(config.maxConnectionsPerRequestkey)) 33 | .withCheckEndpointAuthentication(config.checkEndpointIdentification) 34 | .withMaxResponseLineSize(config.maxResponseLineSize) 35 | .withMaxHeaderLength(config.maxHeaderLength) 36 | .withMaxChunkSize(config.maxChunkSize) 37 | .withChunkBufferMaxSize(config.chunkBufferMaxSize) 38 | .withParserMode(config.parserMode) 39 | .withBufferSize(config.bufferSize) 40 | 41 | val builderWithMaybeSocketOptions = config.socketOptions.fold(builder)(s => builder.withChannelOptions(channelOptions(s))) 42 | val builderWithMaybeTLS = sslContext.fold(builderWithMaybeSocketOptions)(builderWithMaybeSocketOptions.withSslContext) 43 | 44 | builderWithMaybeTLS.resource 45 | } 46 | 47 | def channelOptions(socketOptions: SocketOptions): ChannelOptions = 48 | ChannelOptions( 49 | Vector( 50 | OptionValue[java.lang.Boolean](StandardSocketOptions.SO_REUSEADDR, socketOptions.reuseAddress), 51 | OptionValue[java.lang.Integer](StandardSocketOptions.SO_SNDBUF, socketOptions.sendBufferSize), 52 | OptionValue[java.lang.Integer](StandardSocketOptions.SO_RCVBUF, socketOptions.receiveBufferSize), 53 | OptionValue[java.lang.Boolean](StandardSocketOptions.SO_KEEPALIVE, socketOptions.keepAlive), 54 | OptionValue[java.lang.Boolean](StandardSocketOptions.TCP_NODELAY, socketOptions.noDelay) 55 | ) 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /http4s-client-blaze/src/test/scala/com/avast/sst/http4s/client/Http4SBlazeClientTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client 2 | 3 | import cats.effect.* 4 | import org.http4s.headers.* 5 | import org.http4s.{ProductComment, ProductId} 6 | import org.scalatest.funsuite.AsyncFunSuite 7 | 8 | import scala.concurrent.ExecutionContext 9 | 10 | class Http4SBlazeClientTest extends AsyncFunSuite { 11 | 12 | implicit private val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) 13 | 14 | test("Initialization of HTTP client and simple GET") { 15 | val expected = """|{ 16 | | "user-agent": "http4s-client/1.2.3 (Test)" 17 | |} 18 | |""".stripMargin 19 | 20 | val test = for { 21 | client <- Http4sBlazeClientModule.make[IO]( 22 | Http4sBlazeClientConfig( 23 | userAgent = `User-Agent`(ProductId("http4s-client", Some("1.2.3")), List(ProductComment("Test"))) 24 | ), 25 | ExecutionContext.global 26 | ) 27 | response <- Resource.eval(client.expect[String]("https://httpbin.org/user-agent")) 28 | } yield assert(response === expected) 29 | 30 | test.use(IO.pure).unsafeToFuture() 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /http4s-client-ember-pureconfig/src/main/scala-2/com/avast/sst/http4s/client/pureconfig/ember/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client.pureconfig.ember 2 | 3 | import cats.syntax.either.* 4 | import com.avast.sst.http4s.client.Http4sEmberClientConfig 5 | import com.avast.sst.http4s.client.Http4sEmberClientConfig.SocketOptions 6 | import org.http4s.headers.`User-Agent` 7 | import pureconfig.ConfigReader 8 | import pureconfig.error.CannotConvert 9 | import pureconfig.generic.ProductHint 10 | import pureconfig.generic.semiauto.* 11 | 12 | trait ConfigReaders { 13 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 14 | 15 | implicit val http4sClientUserAgentReader: ConfigReader[`User-Agent`] = ConfigReader[String].emap { value => 16 | `User-Agent`.parse(value).leftMap { parseFailure => CannotConvert(value, "User-Agent HTTP header", parseFailure.message) } 17 | } 18 | 19 | implicit val http4sClientSocketOptionsReader: ConfigReader[SocketOptions] = deriveReader[SocketOptions] 20 | 21 | implicit val http4sClientHttp4sEmberClientConfigReader: ConfigReader[Http4sEmberClientConfig] = deriveReader[Http4sEmberClientConfig] 22 | } 23 | -------------------------------------------------------------------------------- /http4s-client-ember-pureconfig/src/main/scala-2/com/avast/sst/http4s/client/pureconfig/ember/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client.pureconfig.ember 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /http4s-client-ember-pureconfig/src/main/scala-3/com/avast/sst/http4s/client/pureconfig/ember/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client.pureconfig.ember 2 | 3 | import cats.syntax.either.* 4 | import com.avast.sst.http4s.client.Http4sEmberClientConfig 5 | import org.http4s.headers.`User-Agent` 6 | import pureconfig.ConfigReader 7 | import pureconfig.error.CannotConvert 8 | import pureconfig.generic.derivation.default.* 9 | 10 | trait ConfigReaders { 11 | 12 | implicit val http4sClientUserAgentReader: ConfigReader[`User-Agent`] = ConfigReader[String].emap { value => 13 | `User-Agent`.parse(value).leftMap { parseFailure => CannotConvert(value, "User-Agent HTTP header", parseFailure.message) } 14 | } 15 | 16 | implicit val http4sClientHttp4sEmberClientConfigReader: ConfigReader[Http4sEmberClientConfig] = 17 | ConfigReader.derived 18 | 19 | } 20 | -------------------------------------------------------------------------------- /http4s-client-ember-pureconfig/src/main/scala-3/com/avast/sst/http4s/client/pureconfig/ember/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client.pureconfig.ember 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /http4s-client-ember/src/main/scala/com/avast/sst/http4s/client/Http4sEmberClientConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client 2 | 3 | import com.avast.sst.http4s.client.Http4sEmberClientConfig.{Defaults, SocketOptions} 4 | import org.http4s.ProductId 5 | import org.http4s.client.defaults 6 | import org.http4s.headers.`User-Agent` 7 | 8 | import scala.concurrent.duration.{DurationInt, FiniteDuration} 9 | 10 | final case class Http4sEmberClientConfig( 11 | maxTotal: Int = Defaults.maxTotal, 12 | maxPerKey: Int = Defaults.maxPerKey, 13 | idleTimeInPool: FiniteDuration = Defaults.idleTimeInPool, 14 | chunkSize: Int = Defaults.chunkSize, 15 | maxResponseHeaderSize: Int = Defaults.maxResponseHeaderSize, 16 | idleConnectionTime: FiniteDuration = Defaults.idleConnectionTime, 17 | timeout: FiniteDuration = Defaults.timeout, 18 | socketOptions: SocketOptions = SocketOptions(), 19 | userAgent: `User-Agent` = Defaults.userAgent, 20 | checkEndpointIdentification: Boolean = Defaults.checkEndpointIdentification 21 | ) 22 | 23 | object Http4sEmberClientConfig { 24 | final case class SocketOptions( 25 | reuseAddress: Boolean = true, 26 | sendBufferSize: Int = 256 * 1024, 27 | receiveBufferSize: Int = 256 * 1024, 28 | keepAlive: Boolean = false, 29 | noDelay: Boolean = false 30 | ) 31 | 32 | object Defaults { 33 | val maxTotal = 100 34 | val maxPerKey = 100 35 | val idleTimeInPool: FiniteDuration = 30.seconds 36 | val chunkSize: Int = 32 * 1024 37 | val maxResponseHeaderSize: Int = 4096 38 | val idleConnectionTime: FiniteDuration = defaults.RequestTimeout 39 | val timeout: FiniteDuration = defaults.RequestTimeout 40 | val userAgent: `User-Agent` = `User-Agent`(ProductId("http4s-ember", Some(org.http4s.BuildInfo.version))) 41 | val checkEndpointIdentification = true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /http4s-client-ember/src/main/scala/com/avast/sst/http4s/client/Http4sEmberClientModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client 2 | 3 | import cats.effect.{Blocker, Concurrent, ContextShift, Resource, Timer} 4 | import com.avast.sst.http4s.client.Http4sEmberClientConfig.SocketOptions 5 | import fs2.io.tcp.SocketOptionMapping 6 | import fs2.io.tls.TLSContext 7 | import org.http4s.client.Client 8 | import org.http4s.ember.client.EmberClientBuilder 9 | 10 | import java.net.StandardSocketOptions 11 | 12 | object Http4sEmberClientModule { 13 | def make[F[_]: Concurrent: Timer: ContextShift]( 14 | config: Http4sEmberClientConfig, 15 | blocker: Option[Blocker] = None, 16 | tlsContext: Option[TLSContext] = None 17 | ): Resource[F, Client[F]] = { 18 | val builder = EmberClientBuilder 19 | .default[F] 20 | .withMaxTotal(config.maxTotal) 21 | .withMaxPerKey(Function.const(config.maxPerKey)) 22 | .withIdleTimeInPool(config.idleTimeInPool) 23 | .withChunkSize(config.chunkSize) 24 | .withMaxResponseHeaderSize(config.maxResponseHeaderSize) 25 | .withIdleConnectionTime(config.idleConnectionTime) 26 | .withTimeout(config.timeout) 27 | .withAdditionalSocketOptions(socketOptionMapping(config.socketOptions)) 28 | .withUserAgent(config.userAgent) 29 | .withCheckEndpointAuthentication(config.checkEndpointIdentification) 30 | 31 | val builderWithMaybeBlocker = blocker.fold(builder)(builder.withBlocker) 32 | val builderWithMaybeTSL = tlsContext.fold(builderWithMaybeBlocker)(builderWithMaybeBlocker.withTLSContext) 33 | 34 | builderWithMaybeTSL.build 35 | } 36 | 37 | def socketOptionMapping(socketOptions: SocketOptions) = 38 | List( 39 | SocketOptionMapping[java.lang.Boolean](StandardSocketOptions.SO_REUSEADDR, socketOptions.reuseAddress), 40 | SocketOptionMapping[java.lang.Integer](StandardSocketOptions.SO_SNDBUF, socketOptions.sendBufferSize), 41 | SocketOptionMapping[java.lang.Integer](StandardSocketOptions.SO_RCVBUF, socketOptions.receiveBufferSize), 42 | SocketOptionMapping[java.lang.Boolean](StandardSocketOptions.SO_KEEPALIVE, socketOptions.keepAlive), 43 | SocketOptionMapping[java.lang.Boolean](StandardSocketOptions.TCP_NODELAY, socketOptions.noDelay) 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /http4s-client-ember/src/test/scala/com/avast/sst/http4s/client/Http4SEmberClientTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client 2 | 3 | import cats.effect.* 4 | import org.http4s.headers.* 5 | import org.http4s.{ProductComment, ProductId} 6 | import org.scalatest.funsuite.AsyncFunSuite 7 | 8 | import java.util.concurrent.Executors 9 | import scala.concurrent.ExecutionContext 10 | 11 | class Http4SEmberClientTest extends AsyncFunSuite { 12 | 13 | implicit private val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) 14 | implicit private val timer: Timer[IO] = IO.timer(ExecutionContext.global) 15 | 16 | test("Initialization of HTTP client and simple GET") { 17 | val expected = """|{ 18 | | "user-agent": "http4s-client/1.2.3 (Test)" 19 | |} 20 | |""".stripMargin 21 | 22 | val test = for { 23 | client <- Http4sEmberClientModule.make[IO]( 24 | Http4sEmberClientConfig( 25 | userAgent = `User-Agent`(ProductId("http4s-client", Some("1.2.3")), List(ProductComment("Test"))) 26 | ), 27 | Some(Blocker.liftExecutionContext(ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor()))) 28 | ) 29 | response <- Resource.eval(client.expect[String]("https://httpbin.org/user-agent")) 30 | } yield assert(response === expected) 31 | 32 | test.use(IO.pure).unsafeToFuture() 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /http4s-client-monix-catnap/src/main/scala/com/avast/sst/http4s/client/monix/catnap/Http4sClientCircuitBreakerModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client.monix.catnap 2 | 3 | import cats.effect.{Resource, Sync} 4 | import cats.syntax.applicativeError.* 5 | import cats.syntax.flatMap.* 6 | import monix.catnap.CircuitBreaker 7 | import org.http4s.Response 8 | import org.http4s.client.Client 9 | 10 | object Http4sClientCircuitBreakerModule { 11 | 12 | /** Wraps [[org.http4s.client.Client]] with the given [[monix.catnap.CircuitBreaker]]. 13 | * 14 | * The circuit breaker is special in that it also catches any HTTP responses considered as server failures according to the 15 | * [[com.avast.sst.http4s.client.monix.catnap.HttpStatusClassifier]]. 16 | */ 17 | def make[F[_]: Sync]( 18 | client: Client[F], 19 | circuitBreaker: CircuitBreaker[F], 20 | httpStatusClassifier: HttpStatusClassifier = HttpStatusClassifier.default 21 | ): Client[F] = { 22 | val F = Sync[F] 23 | 24 | class ServerFailure(val response: Response[F], val close: F[Unit]) extends Exception 25 | 26 | Client[F] { request => 27 | val raisedInternal = client.run(request).allocated.flatMap { 28 | case tuple @ (response, _) if !httpStatusClassifier.isServerFailure(response.status) => F.pure(tuple) 29 | case (response, close) => F.raiseError[(Response[F], F[Unit])](new ServerFailure(response, close)) 30 | } 31 | val lifted = circuitBreaker.protect(raisedInternal).recover { case serverFailure: ServerFailure => 32 | (serverFailure.response, serverFailure.close) 33 | } 34 | Resource(lifted) 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /http4s-client-monix-catnap/src/main/scala/com/avast/sst/http4s/client/monix/catnap/HttpStatusClassifier.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.client.monix.catnap 2 | 3 | import org.http4s.Status 4 | 5 | /** Classifies HTTP status as failure or not - for the purpose of circuit breaking. */ 6 | trait HttpStatusClassifier { 7 | 8 | def isServerFailure(status: Status): Boolean 9 | 10 | } 11 | 12 | object HttpStatusClassifier { 13 | 14 | private val defaultFailureStatuses = fromSet(Set(Status.TooManyRequests)) 15 | 16 | lazy val default: HttpStatusClassifier = s => status5xx.isServerFailure(s) || defaultFailureStatuses.isServerFailure(s) 17 | 18 | lazy val status5xx: HttpStatusClassifier = _.code >= 500 19 | 20 | def fromSet(failureStatuses: Set[Status]): HttpStatusClassifier = failureStatuses(_) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /http4s-server-blaze-pureconfig/src/main/scala-2/com/avast/sst/http4s/server/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.pureconfig 2 | 3 | import com.avast.sst.http4s.server.Http4sBlazeServerConfig 4 | import com.avast.sst.http4s.server.Http4sBlazeServerConfig.SocketOptions 5 | import pureconfig.ConfigReader 6 | import pureconfig.generic.ProductHint 7 | import pureconfig.generic.semiauto._ 8 | 9 | trait ConfigReaders { 10 | 11 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 12 | 13 | implicit val http4sServerSocketOptionsReader: ConfigReader[SocketOptions] = deriveReader[SocketOptions] 14 | 15 | implicit val http4sServerHttp4sBlazeServerConfigReader: ConfigReader[Http4sBlazeServerConfig] = deriveReader[Http4sBlazeServerConfig] 16 | 17 | } 18 | -------------------------------------------------------------------------------- /http4s-server-blaze-pureconfig/src/main/scala-2/com/avast/sst/http4s/server/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /http4s-server-blaze-pureconfig/src/main/scala-3/com/avast/sst/http4s/server/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.pureconfig 2 | 3 | import com.avast.sst.http4s.server.Http4sBlazeServerConfig 4 | import com.avast.sst.http4s.server.Http4sBlazeServerConfig.SocketOptions 5 | import pureconfig.ConfigReader 6 | import pureconfig.generic.derivation.default.* 7 | 8 | trait ConfigReaders { 9 | 10 | implicit val http4sServerSocketOptionsReader: ConfigReader[SocketOptions] = ConfigReader.derived 11 | 12 | implicit val http4sServerHttp4sBlazeServerConfigReader: ConfigReader[Http4sBlazeServerConfig] = 13 | ConfigReader.derived 14 | 15 | } 16 | -------------------------------------------------------------------------------- /http4s-server-blaze-pureconfig/src/main/scala-3/com/avast/sst/http4s/server/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /http4s-server-blaze/src/main/scala/com/avast/sst/http4s/server/Http4sBlazeServerConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server 2 | 3 | import com.avast.sst.http4s.server.Http4sBlazeServerConfig.SocketOptions 4 | import org.http4s.blaze.channel 5 | import org.http4s.server.defaults 6 | 7 | import java.util.concurrent.TimeUnit 8 | import scala.concurrent.duration.{Duration, FiniteDuration} 9 | 10 | final case class Http4sBlazeServerConfig( 11 | listenAddress: String, 12 | listenPort: Int, 13 | webSocketsEnabled: Boolean = false, 14 | http2Enabled: Boolean = false, 15 | responseHeaderTimeout: FiniteDuration = Duration(defaults.ResponseTimeout.toNanos, TimeUnit.NANOSECONDS), 16 | idleTimeout: FiniteDuration = Duration(defaults.IdleTimeout.toNanos, TimeUnit.NANOSECONDS), 17 | bufferSize: Int = 64 * 1024, 18 | maxRequestLineLength: Int = 4 * 1024, 19 | maxHeadersLength: Int = defaults.MaxHeadersSize, 20 | chunkBufferMaxSize: Int = 1024 * 1024, 21 | connectorPoolSize: Int = channel.DefaultPoolSize, 22 | maxConnections: Int = defaults.MaxConnections, 23 | socketOptions: SocketOptions = SocketOptions() 24 | ) 25 | 26 | object Http4sBlazeServerConfig { 27 | 28 | def localhost8080: Http4sBlazeServerConfig = Http4sBlazeServerConfig("127.0.0.1", 8080) 29 | 30 | final case class SocketOptions( 31 | tcpNoDelay: Boolean = true, 32 | soKeepAlive: Option[Boolean] = None, 33 | soReuseAddr: Option[Boolean] = None, 34 | soReusePort: Option[Boolean] = None 35 | ) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /http4s-server-blaze/src/test/scala/com/avast/sst/http4s/server/Http4sBlazeServerModuleTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server 2 | 3 | import cats.effect.{ContextShift, IO, Timer} 4 | import com.avast.sst.http4s.client.{Http4sBlazeClientConfig, Http4sBlazeClientModule} 5 | import org.http4s.HttpRoutes 6 | import org.http4s.dsl.Http4sDsl 7 | import org.scalatest.funsuite.AsyncFunSuite 8 | 9 | import scala.concurrent.ExecutionContext 10 | 11 | class Http4sBlazeServerModuleTest extends AsyncFunSuite with Http4sDsl[IO] { 12 | 13 | implicit private val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) 14 | implicit private val timer: Timer[IO] = IO.timer(ExecutionContext.global) 15 | 16 | test("Simple HTTP server") { 17 | val routes = Http4sRouting.make(HttpRoutes.of[IO] { case GET -> Root / "test" => 18 | Ok("test") 19 | }) 20 | val test = for { 21 | server <- Http4sBlazeServerModule.make[IO](Http4sBlazeServerConfig("127.0.0.1", 0), routes, ExecutionContext.global) 22 | client <- Http4sBlazeClientModule.make[IO](Http4sBlazeClientConfig(), ExecutionContext.global) 23 | } yield (server, client) 24 | 25 | test 26 | .use { case (server, client) => 27 | client 28 | .expect[String](s"http://${server.address.getHostString}:${server.address.getPort}/test") 29 | .map(response => assert(response === "test")) 30 | } 31 | .unsafeToFuture() 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /http4s-server-ember-pureconfig/src/main/scala-2/com/avast/sst/http4s/server/pureconfig/ember/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.pureconfig.ember 2 | 3 | import com.avast.sst.http4s.server.Http4sEmberServerConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.ProductHint 6 | import pureconfig.generic.semiauto.deriveReader 7 | 8 | trait ConfigReaders { 9 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 10 | 11 | implicit val http4sServerHttp4sEmberServerConfigReader: ConfigReader[Http4sEmberServerConfig] = deriveReader[Http4sEmberServerConfig] 12 | 13 | } 14 | -------------------------------------------------------------------------------- /http4s-server-ember-pureconfig/src/main/scala-2/com/avast/sst/http4s/server/pureconfig/ember/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.pureconfig.ember 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /http4s-server-ember-pureconfig/src/main/scala-3/com/avast/sst/http4s/server/pureconfig/ember/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.pureconfig.ember 2 | 3 | import com.avast.sst.http4s.server.Http4sEmberServerConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.derivation.default.* 6 | 7 | trait ConfigReaders { 8 | 9 | implicit val http4sServerHttp4sEmberServerConfigReader: ConfigReader[Http4sEmberServerConfig] = 10 | ConfigReader.derived 11 | 12 | } 13 | -------------------------------------------------------------------------------- /http4s-server-ember-pureconfig/src/main/scala-3/com/avast/sst/http4s/server/pureconfig/ember/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.pureconfig.ember 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /http4s-server-ember/src/main/scala/com/avast/sst/http4s/server/Http4sEmberServerConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server 2 | 3 | import com.avast.sst.http4s.server.Http4sEmberServerConfig.Defaults 4 | import org.http4s.server.defaults 5 | 6 | import java.util.concurrent.TimeUnit 7 | import scala.concurrent.duration.* 8 | 9 | final case class Http4sEmberServerConfig( 10 | host: String = Defaults.host, 11 | port: Int = Defaults.port, 12 | maxConnections: Int = Defaults.maxConnections, 13 | receiveBufferSize: Int = Defaults.receiveBufferSize, 14 | maxHeaderSize: Int = Defaults.maxHeaderSize, 15 | requestHeaderReceiveTimeout: FiniteDuration = Defaults.requestHeaderReceiveTimeout, 16 | idleTimeout: FiniteDuration = Defaults.idleTimeout, 17 | shutdownTimeout: FiniteDuration = Defaults.shutdownTimeout 18 | ) 19 | 20 | object Http4sEmberServerConfig { 21 | object Defaults { 22 | val host: String = defaults.IPv4Host 23 | val port: Int = 8080 24 | val maxConnections: Int = defaults.MaxConnections 25 | val receiveBufferSize: Int = 256 * 1024 26 | val maxHeaderSize: Int = defaults.MaxHeadersSize 27 | val requestHeaderReceiveTimeout: FiniteDuration = 5.seconds 28 | val idleTimeout: FiniteDuration = Duration(defaults.IdleTimeout.toNanos, TimeUnit.NANOSECONDS) 29 | val shutdownTimeout: FiniteDuration = Duration(defaults.ShutdownTimeout.toNanos, TimeUnit.NANOSECONDS) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /http4s-server-ember/src/main/scala/com/avast/sst/http4s/server/Http4sEmberServerModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server 2 | 3 | import cats.effect.{Blocker, Concurrent, ContextShift, Resource, Timer} 4 | import fs2.io.tls.{TLSContext, TLSParameters} 5 | import org.http4s.HttpApp 6 | import org.http4s.ember.server.EmberServerBuilder 7 | import org.http4s.server.Server 8 | 9 | object Http4sEmberServerModule { 10 | def make[F[_]: Concurrent: Timer: ContextShift]( 11 | config: Http4sEmberServerConfig, 12 | httpApp: HttpApp[F], 13 | blocker: Option[Blocker] = None, 14 | tls: Option[(TLSContext, TLSParameters)] = None 15 | ): Resource[F, Server] = { 16 | val builder = EmberServerBuilder 17 | .default[F] 18 | .withHost(config.host) 19 | .withPort(config.port) 20 | .withHttpApp(httpApp) 21 | .withMaxConnections(config.maxConnections) 22 | .withReceiveBufferSize(config.receiveBufferSize) 23 | .withMaxHeaderSize(config.maxHeaderSize) 24 | .withRequestHeaderReceiveTimeout(config.requestHeaderReceiveTimeout) 25 | .withIdleTimeout(config.idleTimeout) 26 | .withShutdownTimeout(config.shutdownTimeout) 27 | 28 | val builderWithMaybeBlocker = blocker.fold(builder)(builder.withBlocker) 29 | val builderWithMaybeTLS = tls.fold(builderWithMaybeBlocker)(t => builderWithMaybeBlocker.withTLS(t._1, t._2)) 30 | 31 | builderWithMaybeTLS.build 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /http4s-server-ember/src/test/scala/com/avast/sst/http4s/server/Http4sEmberServerModuleTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server 2 | 3 | import cats.effect.{Blocker, ContextShift, IO, Timer} 4 | import com.avast.sst.http4s.client.{Http4sEmberClientConfig, Http4sEmberClientModule} 5 | import org.http4s.HttpRoutes 6 | import org.http4s.dsl.Http4sDsl 7 | import org.scalatest.funsuite.AsyncFunSuite 8 | 9 | import java.util.concurrent.Executors 10 | import scala.concurrent.ExecutionContext 11 | 12 | class Http4sEmberServerModuleTest extends AsyncFunSuite with Http4sDsl[IO] { 13 | 14 | implicit private val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) 15 | implicit private val timer: Timer[IO] = IO.timer(ExecutionContext.global) 16 | private val blocker = Blocker.liftExecutionContext(ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor())) 17 | 18 | test("Simple HTTP server") { 19 | val routes = Http4sRouting.make(HttpRoutes.of[IO] { case GET -> Root / "test" => 20 | Ok("test") 21 | }) 22 | val test = for { 23 | server <- Http4sEmberServerModule.make[IO](Http4sEmberServerConfig(), routes, Some(blocker)) 24 | client <- Http4sEmberClientModule.make[IO](Http4sEmberClientConfig(), Some(blocker)) 25 | } yield (server, client) 26 | 27 | test 28 | .use { case (server, client) => 29 | client 30 | .expect[String](s"http://${server.address.getHostString}:${server.address.getPort}/test") 31 | .map(response => assert(response === "test")) 32 | } 33 | .unsafeToFuture() 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sMetricsOpsModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.micrometer 2 | 3 | import cats.effect.concurrent.Ref 4 | import cats.effect.{Blocker, ContextShift, Effect, IO} 5 | import cats.syntax.functor.* 6 | import io.micrometer.core.instrument.MeterRegistry 7 | import org.http4s.metrics.{MetricsOps, TerminationType} 8 | import org.http4s.{Method, Status} 9 | 10 | import java.util.concurrent.TimeUnit 11 | 12 | object MicrometerHttp4sMetricsOpsModule { 13 | 14 | /** Makes [[org.http4s.metrics.MetricsOps]] to record the usual HTTP server metrics. */ 15 | def make[F[_]: Effect](meterRegistry: MeterRegistry, blocker: Blocker): F[MetricsOps[F]] = { 16 | val F = Effect[F] 17 | 18 | implicit val iocs: ContextShift[IO] = IO.contextShift(blocker.blockingContext) 19 | 20 | for { 21 | activeRequests <- Ref.of[F, Long](0L) 22 | } yield new MetricsOps[F] { 23 | 24 | private val prefix = "http.global" 25 | private val failureTime = meterRegistry.timer(s"$prefix.failure-time") 26 | 27 | meterRegistry.gauge( 28 | s"$prefix.active-requests", 29 | activeRequests, 30 | (_: Ref[F, Long]) => blocker.blockOn(Effect[F].toIO(activeRequests.get)).unsafeRunSync().toDouble 31 | ) 32 | 33 | override def increaseActiveRequests(classifier: Option[String]): F[Unit] = activeRequests.update(_ + 1) 34 | 35 | override def decreaseActiveRequests(classifier: Option[String]): F[Unit] = activeRequests.update(_ - 1) 36 | 37 | override def recordHeadersTime(method: Method, elapsed: Long, classifier: Option[String]): F[Unit] = { 38 | F.delay(meterRegistry.timer(s"$prefix.headers-time", "method", method.name).record(elapsed, TimeUnit.NANOSECONDS)) 39 | } 40 | 41 | override def recordTotalTime(method: Method, status: Status, elapsed: Long, classifier: Option[String]): F[Unit] = { 42 | F.delay( 43 | meterRegistry 44 | .timer(s"$prefix.total-time", "status", s"${status.code}", "status-class", s"${status.code / 100}xx") 45 | .record(elapsed, TimeUnit.NANOSECONDS) 46 | ) 47 | } 48 | 49 | override def recordAbnormalTermination(elapsed: Long, terminationType: TerminationType, classifier: Option[String]): F[Unit] = { 50 | F.delay(failureTime.record(elapsed, TimeUnit.NANOSECONDS)) 51 | } 52 | 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sServerMetricsModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.micrometer 2 | 3 | import cats.effect.{Blocker, Clock, Effect, Sync} 4 | import cats.syntax.flatMap.* 5 | import cats.syntax.functor.* 6 | import io.micrometer.core.instrument.MeterRegistry 7 | import org.http4s.HttpRoutes 8 | import org.http4s.server.middleware.Metrics 9 | 10 | class MicrometerHttp4sServerMetricsModule[F[_]](val serverMetrics: HttpRoutes[F] => HttpRoutes[F], val routeMetrics: RouteMetrics[F]) 11 | 12 | object MicrometerHttp4sServerMetricsModule { 13 | 14 | /** Makes [[com.avast.sst.http4s.server.micrometer.MicrometerHttp4sServerMetricsModule]] that can be used to setup monitoring of the whole 15 | * HTTP server and individual routes. 16 | */ 17 | def make[F[_]: Effect](meterRegistry: MeterRegistry, blocker: Blocker, clock: Clock[F]): F[MicrometerHttp4sServerMetricsModule[F]] = { 18 | implicit val c: Clock[F] = clock 19 | 20 | for { 21 | metricsOps <- MicrometerHttp4sMetricsOpsModule.make[F](meterRegistry, blocker) 22 | routeMetrics <- Sync[F].delay(new RouteMetrics[F](meterRegistry)) 23 | } yield new MicrometerHttp4sServerMetricsModule[F](Metrics(metricsOps), routeMetrics) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /http4s-server-micrometer/src/main/scala/com/avast/sst/http4s/server/micrometer/RouteMetrics.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.micrometer 2 | 3 | import cats.effect.Sync 4 | import cats.effect.syntax.bracket.* 5 | import cats.syntax.flatMap.* 6 | import cats.syntax.functor.* 7 | import io.micrometer.core.instrument.{MeterRegistry, Timer} 8 | import org.http4s.Response 9 | 10 | /** Provides the usual metrics for a single HTTP route. */ 11 | class RouteMetrics[F[_]: Sync](meterRegistry: MeterRegistry) { 12 | 13 | private val F = Sync[F] 14 | 15 | /** Wraps a single route with the usual metrics (count, times, HTTP status codes). 16 | * 17 | * @param name 18 | * will be used in metric name 19 | */ 20 | def wrap(name: String)(route: => F[Response[F]]): F[Response[F]] = { 21 | for { 22 | start <- F.delay(Timer.start(meterRegistry)) 23 | response <- route.bracket(F.pure) { response => 24 | F.delay( 25 | start.stop( 26 | meterRegistry 27 | .timer(s"http.$name", "status", s"${response.status.code}", "status-class", s"${response.status.code / 100}xx") 28 | ) 29 | ).as(()) 30 | } 31 | } yield response 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/MicrometerHttp4sMetricsOpsModuleTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.micrometer 2 | 3 | import cats.effect.{Blocker, IO} 4 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry 5 | import org.http4s.{Method, Status} 6 | import org.scalatest.funsuite.AnyFunSuite 7 | 8 | import java.util.concurrent.{Executors, TimeUnit} 9 | import scala.annotation.nowarn 10 | import scala.concurrent.ExecutionContext 11 | 12 | @nowarn("msg=unused value") 13 | class MicrometerHttp4sMetricsOpsModuleTest extends AnyFunSuite { 14 | 15 | test("http4s MetricsOps for Micrometer") { 16 | val registry = new SimpleMeterRegistry() 17 | val blocker = Blocker.liftExecutionContext(ExecutionContext.fromExecutor(Executors.newCachedThreadPool())) 18 | val metricsOps = MicrometerHttp4sMetricsOpsModule.make[IO](registry, blocker).unsafeRunSync() 19 | 20 | metricsOps.increaseActiveRequests(None).unsafeRunSync() 21 | metricsOps.recordTotalTime(Method.GET, Status.Ok, 2500, None).unsafeRunSync() 22 | 23 | assert(registry.get("http.global.active-requests").gauge().value() === 1) 24 | assert(registry.get("http.global.total-time").timer().count() === 1) 25 | assert(registry.get("http.global.total-time").timer().totalTime(TimeUnit.NANOSECONDS) > 2499) 26 | assert(registry.get("http.global.total-time").tags("status", "200").timer().count() === 1) 27 | assert(registry.get("http.global.total-time").tags("status-class", "2xx").timer().count() === 1) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /http4s-server-micrometer/src/test/scala/com/avast/sst/http4s/server/micrometer/RouteMetricsTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.micrometer 2 | 3 | import cats.effect.SyncIO 4 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry 5 | import org.http4s.Response 6 | import org.scalatest.funsuite.AnyFunSuite 7 | 8 | import java.util.concurrent.TimeUnit 9 | import scala.annotation.nowarn 10 | 11 | @nowarn("msg=unused value") 12 | class RouteMetricsTest extends AnyFunSuite { 13 | 14 | test("Single route metrics") { 15 | val registry = new SimpleMeterRegistry() 16 | val target = new RouteMetrics[SyncIO](registry) 17 | 18 | target.wrap("test")(SyncIO.pure(Response.notFound[SyncIO])).unsafeRunSync() 19 | assert(registry.get("http.test").timer().count() === 1) 20 | assert(registry.get("http.test").timer().totalTime(TimeUnit.MILLISECONDS) > 0) 21 | assert(registry.get("http.test").tags("status", "404").timer().count() === 1) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /http4s-server/src/main/scala/com/avast/sst/http4s/server/Http4sRouting.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server 2 | 3 | import cats.Monad 4 | import cats.syntax.all.* 5 | import org.http4s.{HttpApp, HttpRoutes} 6 | 7 | object Http4sRouting { 8 | 9 | /** Makes [[org.http4s.HttpApp]] from [[org.http4s.HttpRoutes]] */ 10 | def make[F[_]: Monad](routes: HttpRoutes[F], more: HttpRoutes[F]*): HttpApp[F] = { 11 | more 12 | .foldLeft[HttpRoutes[F]](routes)(_.combineK(_)) 13 | .orNotFound 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /http4s-server/src/main/scala/com/avast/sst/http4s/server/middleware/CorrelationIdMiddleware.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.middleware 2 | 3 | import cats.data.{Kleisli, NonEmptyList, OptionT} 4 | import cats.effect.Sync 5 | import cats.syntax.functor.* 6 | import com.avast.sst.http4s.server.middleware.CorrelationIdMiddleware.CorrelationId 7 | import org.http4s.{Header, HttpRoutes, Request, Response} 8 | import org.slf4j.LoggerFactory 9 | import org.typelevel.ci.CIString 10 | import org.typelevel.vault.Key 11 | 12 | import java.util.UUID 13 | 14 | /** Provides correlation ID functionality. Either generates new correlation ID for a request or takes the one sent in HTTP header and puts 15 | * it to [[org.http4s.Request]] attributes. It is also filled into HTTP response header. 16 | * 17 | * Use method `retrieveCorrelationId` to get the value from request attributes. 18 | */ 19 | class CorrelationIdMiddleware[F[_]: Sync]( 20 | correlationIdHeaderName: CIString, 21 | attributeKey: Key[CorrelationId], 22 | generator: () => String 23 | ) { 24 | 25 | private val logger = LoggerFactory.getLogger(this.getClass) 26 | 27 | private val F = Sync[F] 28 | 29 | def wrap(routes: HttpRoutes[F]): HttpRoutes[F] = 30 | Kleisli[OptionT[F, *], Request[F], Response[F]] { request => 31 | request.headers.get(correlationIdHeaderName) match { 32 | case Some(NonEmptyList(header, _)) => 33 | val requestWithAttribute = request.withAttribute(attributeKey, CorrelationId(header.value)) 34 | routes(requestWithAttribute).map(r => r.withHeaders(r.headers.put(header))) 35 | case None => 36 | for { 37 | newCorrelationId <- OptionT.liftF(F.delay(generator())) 38 | _ <- log(newCorrelationId) 39 | requestWithAttribute = request.withAttribute(attributeKey, CorrelationId(newCorrelationId)) 40 | response <- routes(requestWithAttribute) 41 | } yield response.withHeaders(response.headers.put(Header.Raw(correlationIdHeaderName, newCorrelationId))) 42 | } 43 | } 44 | 45 | def retrieveCorrelationId(request: Request[F]): Option[CorrelationId] = request.attributes.lookup(attributeKey) 46 | 47 | private def log(newCorrelationId: String) = { 48 | OptionT.liftF { 49 | F.delay { 50 | if (logger.isDebugEnabled()) { 51 | logger.debug(s"Generated new correlation ID: $newCorrelationId") 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | object CorrelationIdMiddleware { 59 | 60 | final case class CorrelationId(value: String) extends AnyVal 61 | 62 | @SuppressWarnings(Array("scalafix:Disable.toString")) 63 | def default[F[_]: Sync]: F[CorrelationIdMiddleware[F]] = { 64 | Key.newKey[F, CorrelationId].map { attributeKey => 65 | new CorrelationIdMiddleware(CIString("Correlation-ID"), attributeKey, () => UUID.randomUUID().toString) 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /http4s-server/src/test/scala/com/avast/sst/http4s/server/middleware/CorrelationIdMiddlewareTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.http4s.server.middleware 2 | 3 | import cats.effect.{ContextShift, IO, Resource, Timer} 4 | import com.avast.sst.http4s.server.Http4sRouting 5 | import org.http4s.blaze.client.BlazeClientBuilder 6 | import org.http4s.blaze.server.BlazeServerBuilder 7 | import org.http4s.dsl.Http4sDsl 8 | import org.http4s.{Header, HttpRoutes, Request, Uri} 9 | import org.scalatest.funsuite.AsyncFunSuite 10 | import org.typelevel.ci.CIString 11 | 12 | import java.net.InetSocketAddress 13 | import scala.annotation.nowarn 14 | import scala.concurrent.ExecutionContext 15 | 16 | @nowarn("msg=unused value") 17 | @SuppressWarnings(Array("scalafix:Disable.get", "scalafix:Disable.toString", "scalafix:Disable.createUnresolved")) 18 | class CorrelationIdMiddlewareTest extends AsyncFunSuite with Http4sDsl[IO] { 19 | 20 | implicit private val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global) 21 | implicit private val timer: Timer[IO] = IO.timer(ExecutionContext.global) 22 | 23 | test("CorrelationIdMiddleware fills Request attributes and HTTP response header") { 24 | val test = for { 25 | middleware <- Resource.eval(CorrelationIdMiddleware.default[IO]) 26 | routes = Http4sRouting.make { 27 | middleware.wrap { 28 | HttpRoutes.of[IO] { case req @ GET -> Root / "test" => 29 | val id = middleware.retrieveCorrelationId(req) 30 | Ok("test").map(_.withHeaders(Header.Raw(CIString("Attribute-Value"), id.toString))) 31 | } 32 | } 33 | } 34 | server <- BlazeServerBuilder[IO](ExecutionContext.global) 35 | .bindSocketAddress(InetSocketAddress.createUnresolved("127.0.0.1", 0)) 36 | .withHttpApp(routes) 37 | .resource 38 | client <- BlazeClientBuilder[IO](ExecutionContext.global).resource 39 | } yield (server, client) 40 | 41 | test 42 | .use { case (server, client) => 43 | client 44 | .run( 45 | Request[IO](uri = Uri.unsafeFromString(s"http://${server.address.getHostString}:${server.address.getPort}/test")) 46 | .withHeaders(Header.Raw(CIString("Correlation-Id"), "test-value")) 47 | ) 48 | .use { response => 49 | IO.delay { 50 | assert(response.headers.get(CIString("Correlation-Id")).get.head.value === "test-value") 51 | assert(response.headers.get(CIString("Attribute-Value")).get.head.value === "Some(CorrelationId(test-value))") 52 | } 53 | } 54 | } 55 | .unsafeToFuture() 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /jdk-http-client-pureconfig/src/main/scala-2/com/avast/sst/jdk/httpclient/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jdk.httpclient.pureconfig 2 | 3 | import com.avast.sst.jdk.httpclient.JdkHttpClientConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.ProductHint 6 | import pureconfig.generic.semiauto._ 7 | 8 | trait ConfigReaders { 9 | 10 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 11 | 12 | implicit val jdkHttpClientConfigReader: ConfigReader[JdkHttpClientConfig] = deriveReader[JdkHttpClientConfig] 13 | 14 | } 15 | -------------------------------------------------------------------------------- /jdk-http-client-pureconfig/src/main/scala-2/com/avast/sst/jdk/httpclient/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jdk.httpclient.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /jdk-http-client-pureconfig/src/main/scala-3/com/avast/sst/jdk/httpclient/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jdk.httpclient.pureconfig 2 | 3 | import pureconfig.ConfigReader 4 | import com.avast.sst.jdk.httpclient.JdkHttpClientConfig 5 | import pureconfig.generic.derivation.default.* 6 | 7 | trait ConfigReaders { 8 | 9 | implicit val jdkHttpClientConfigReader: ConfigReader[JdkHttpClientConfig] = ConfigReader.derived 10 | 11 | } 12 | -------------------------------------------------------------------------------- /jdk-http-client-pureconfig/src/main/scala-3/com/avast/sst/jdk/httpclient/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jdk.httpclient.pureconfig 2 | 3 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. 4 | */ 5 | object implicits extends ConfigReaders { 6 | 7 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 8 | * 9 | * This is alias for the default `implicits._` import. 10 | */ 11 | object KebabCase extends ConfigReaders 12 | 13 | } 14 | -------------------------------------------------------------------------------- /jdk-http-client/src/main/scala/com/avast/sst/jdk/httpclient/JdkHttpClientConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jdk.httpclient 2 | 3 | import java.net.http.HttpClient 4 | import scala.concurrent.duration.FiniteDuration 5 | 6 | final case class JdkHttpClientConfig( 7 | connectTimeout: Option[FiniteDuration] = None, 8 | followRedirects: Option[HttpClient.Redirect] = None, 9 | version: Option[HttpClient.Version] = None, 10 | priority: Option[Int] = None 11 | ) 12 | -------------------------------------------------------------------------------- /jdk-http-client/src/main/scala/com/avast/sst/jdk/httpclient/JdkHttpClientModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jdk.httpclient 2 | 3 | import java.net.http.HttpClient 4 | import java.net.{Authenticator, CookieHandler, ProxySelector} 5 | import java.time.{Duration as JDuration} 6 | import java.util.concurrent.Executor 7 | import javax.net.ssl.SSLContext 8 | 9 | object JdkHttpClientModule { 10 | 11 | /** Makes `java.net.http.HttpClient` initialized with the given config. */ 12 | def make( 13 | config: JdkHttpClientConfig, 14 | executor: Option[Executor] = None, 15 | sslContext: Option[SSLContext] = None, 16 | cookieHandler: Option[CookieHandler] = None, 17 | proxySelector: Option[ProxySelector] = None, 18 | authenticator: Option[Authenticator] = None 19 | ): HttpClient = { 20 | val builder = HttpClient.newBuilder() 21 | 22 | config.connectTimeout.map(d => JDuration.ofNanos(d.toNanos)).foreach(builder.connectTimeout) 23 | executor.foreach(builder.executor) 24 | sslContext.foreach(builder.sslContext) 25 | config.followRedirects.foreach(builder.followRedirects) 26 | config.priority.foreach(builder.priority) 27 | cookieHandler.foreach(builder.cookieHandler) 28 | proxySelector.foreach(builder.proxy) 29 | authenticator.foreach(builder.authenticator) 30 | 31 | builder.build() 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /jvm-micrometer/src/main/scala/com/avast/sst/jvm/micrometer/MicrometerJvmModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.micrometer 2 | 3 | import cats.effect.Sync 4 | import io.micrometer.core.instrument.MeterRegistry 5 | import io.micrometer.core.instrument.binder.jvm.{ClassLoaderMetrics, JvmGcMetrics, JvmMemoryMetrics, JvmThreadMetrics} 6 | import io.micrometer.core.instrument.binder.system.ProcessorMetrics 7 | 8 | object MicrometerJvmModule { 9 | 10 | /** Sets up publishing of JVM metrics (class loading, GC, memory, CPU, ...) into the given [[io.micrometer.core.instrument.MeterRegistry]] 11 | */ 12 | def make[F[_]: Sync](registry: MeterRegistry): F[Unit] = { 13 | Sync[F].delay { 14 | new ClassLoaderMetrics().bindTo(registry) 15 | new JvmMemoryMetrics().bindTo(registry) 16 | new JvmGcMetrics().bindTo(registry) 17 | new ProcessorMetrics().bindTo(registry) 18 | new JvmThreadMetrics().bindTo(registry) 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /jvm-pureconfig/src/main/scala-2/com/avast/sst/jvm/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.pureconfig 2 | 3 | import com.avast.sst.jvm.execution.ForkJoinPoolConfig.TaskPeekingMode 4 | import com.avast.sst.jvm.execution.{ForkJoinPoolConfig, ThreadPoolExecutorConfig} 5 | import pureconfig.ConfigReader 6 | import pureconfig.generic.ProductHint 7 | import pureconfig.generic.semiauto._ 8 | 9 | trait ConfigReaders { 10 | 11 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 12 | 13 | implicit val jvmThreadPoolExecutorConfigReader: ConfigReader[ThreadPoolExecutorConfig] = deriveReader[ThreadPoolExecutorConfig] 14 | 15 | implicit val jvmTaskPeekingModeReader: ConfigReader[TaskPeekingMode] = deriveEnumerationReader 16 | 17 | implicit val jvmForkJoinPoolConfigReader: ConfigReader[ForkJoinPoolConfig] = deriveReader[ForkJoinPoolConfig] 18 | 19 | } 20 | -------------------------------------------------------------------------------- /jvm-pureconfig/src/main/scala-2/com/avast/sst/jvm/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /jvm-pureconfig/src/main/scala-3/com/avast/sst/jvm/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.pureconfig 2 | 3 | import com.avast.sst.jvm.execution.ForkJoinPoolConfig.TaskPeekingMode 4 | import com.avast.sst.jvm.execution.{ForkJoinPoolConfig, ThreadPoolExecutorConfig} 5 | import pureconfig.ConfigReader 6 | import pureconfig.generic.derivation.EnumConfigReader 7 | import pureconfig.generic.derivation.default.* 8 | 9 | trait ConfigReaders { 10 | 11 | implicit val jvmThreadPoolExecutorConfigReader: ConfigReader[ThreadPoolExecutorConfig] = 12 | ConfigReader.derived 13 | 14 | implicit val jvmTaskPeekingModeReader: ConfigReader[TaskPeekingMode] = ConfigReader.derived 15 | 16 | implicit val jvmForkJoinPoolConfigReader: ConfigReader[ForkJoinPoolConfig] = ConfigReader.derived 17 | 18 | } 19 | -------------------------------------------------------------------------------- /jvm-pureconfig/src/main/scala-3/com/avast/sst/jvm/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /jvm/src/main/scala/com/avast/sst/jvm/execution/ConfigurableThreadFactory.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.execution 2 | 3 | import com.avast.sst.jvm.execution.ConfigurableThreadFactory.Config 4 | 5 | import java.lang.Thread.UncaughtExceptionHandler 6 | import java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory 7 | import java.util.concurrent.atomic.AtomicLong 8 | import java.util.concurrent.{ForkJoinPool, ForkJoinWorkerThread, ThreadFactory} 9 | 10 | /** Thread factory (both [[java.util.concurrent.ThreadFactory]] and [[java.util.concurrent.ForkJoinPool.ForkJoinWorkerThreadFactory]]) that 11 | * creates new threads according to the provided [[com.avast.sst.jvm.execution.ConfigurableThreadFactory.Config]]. 12 | */ 13 | class ConfigurableThreadFactory(config: Config) extends ThreadFactory with ForkJoinWorkerThreadFactory { 14 | 15 | private val counter = new AtomicLong(0L) 16 | 17 | override def newThread(r: Runnable): Thread = configure(new Thread(r)) 18 | 19 | override def newThread(pool: ForkJoinPool): ForkJoinWorkerThread = { 20 | configure(ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool)) 21 | } 22 | 23 | private def configure[A <: Thread](thread: A) = { 24 | config.nameFormat.map(_.format(counter.getAndIncrement())).foreach(thread.setName) 25 | thread.setDaemon(config.daemon) 26 | thread.setPriority(config.priority) 27 | thread.setUncaughtExceptionHandler(config.uncaughtExceptionHandler) 28 | thread 29 | } 30 | 31 | } 32 | 33 | object ConfigurableThreadFactory { 34 | 35 | /** @param nameFormat 36 | * Formatted with long number, e.g. my-thread-%02d 37 | */ 38 | final case class Config( 39 | nameFormat: Option[String] = None, 40 | daemon: Boolean = false, 41 | priority: Int = Thread.NORM_PRIORITY, 42 | uncaughtExceptionHandler: UncaughtExceptionHandler = LoggingUncaughtExceptionHandler 43 | ) 44 | 45 | } 46 | -------------------------------------------------------------------------------- /jvm/src/main/scala/com/avast/sst/jvm/execution/ForkJoinPoolConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.execution 2 | 3 | import com.avast.sst.jvm.execution.ForkJoinPoolConfig.TaskPeekingMode 4 | import com.avast.sst.jvm.execution.ForkJoinPoolConfig.TaskPeekingMode.{FIFO, LIFO} 5 | 6 | final case class ForkJoinPoolConfig( 7 | parallelismMin: Int = 8, 8 | parallelismFactor: Double = 1.0, 9 | parallelismMax: Int = 64, 10 | taskPeekingMode: TaskPeekingMode = FIFO 11 | ) { 12 | 13 | private[sst] def computeParallelism(numOfCpus: Int): Int = { 14 | math.min(math.max(math.ceil(numOfCpus * parallelismFactor).toInt, parallelismMin), parallelismMax) 15 | } 16 | 17 | private[sst] def computeAsyncMode: Boolean = 18 | taskPeekingMode match { 19 | case FIFO => true 20 | case LIFO => false 21 | } 22 | 23 | } 24 | 25 | object ForkJoinPoolConfig { 26 | 27 | val Default: ForkJoinPoolConfig = ForkJoinPoolConfig() 28 | 29 | sealed trait TaskPeekingMode 30 | 31 | object TaskPeekingMode { 32 | 33 | case object FIFO extends TaskPeekingMode 34 | 35 | case object LIFO extends TaskPeekingMode 36 | 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /jvm/src/main/scala/com/avast/sst/jvm/execution/LoggingUncaughtExceptionHandler.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.execution 2 | 3 | import org.slf4j.LoggerFactory 4 | 5 | import java.lang.Thread.UncaughtExceptionHandler 6 | 7 | object LoggingUncaughtExceptionHandler extends UncaughtExceptionHandler { 8 | 9 | private lazy val logger = LoggerFactory.getLogger("LoggingUncaughtExceptionHandler") 10 | 11 | override def uncaughtException(t: Thread, ex: Throwable): Unit = logger.error(s"Uncaught exception on thread ${t.getName}", ex) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /jvm/src/main/scala/com/avast/sst/jvm/execution/ThreadPoolExecutorConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.execution 2 | 3 | import java.util.concurrent.TimeUnit 4 | import scala.concurrent.duration.{Duration, FiniteDuration} 5 | 6 | final case class ThreadPoolExecutorConfig( 7 | coreSize: Int, 8 | maxSize: Int, 9 | keepAlive: FiniteDuration = Duration(60000L, TimeUnit.MILLISECONDS), 10 | allowCoreThreadTimeout: Boolean = false 11 | ) 12 | -------------------------------------------------------------------------------- /jvm/src/main/scala/com/avast/sst/jvm/system/console/Console.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.system.console 2 | 3 | import cats.effect.Sync 4 | 5 | import java.io.{OutputStream, Reader} 6 | import scala.io.StdIn 7 | import scala.{Console as SConsole} 8 | 9 | /** Pure console allowing to read and print lines. */ 10 | trait Console[F[_]] { 11 | 12 | def printLine(value: String): F[Unit] 13 | 14 | def printLineToError(value: String): F[Unit] 15 | 16 | def readLine: F[String] 17 | 18 | } 19 | 20 | object Console { 21 | 22 | def apply[F[_]: Sync](in: Reader, out: OutputStream, err: OutputStream): Console[F] = 23 | new Console[F] { 24 | 25 | private val F = Sync[F] 26 | 27 | override def printLine(value: String): F[Unit] = 28 | F.delay { 29 | SConsole.withOut(out)(SConsole.println(value)) 30 | } 31 | 32 | override def printLineToError(value: String): F[Unit] = 33 | F.delay { 34 | SConsole.withErr(err)(SConsole.err.println(value)) 35 | } 36 | 37 | override def readLine: F[String] = 38 | F.delay { 39 | SConsole.withIn(in)(StdIn.readLine()) 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /jvm/src/main/scala/com/avast/sst/jvm/system/console/ConsoleModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.system.console 2 | 3 | import cats.effect.Sync 4 | 5 | import scala.{Console as SConsole} 6 | 7 | /** Provides console - standard in/out/err. */ 8 | object ConsoleModule { 9 | 10 | /** Makes [[com.avast.sst.jvm.system.console.Console]] with standard in/out/err. */ 11 | def make[F[_]: Sync]: Console[F] = Console(SConsole.in, SConsole.out, SConsole.err) 12 | 13 | } 14 | -------------------------------------------------------------------------------- /jvm/src/main/scala/com/avast/sst/jvm/system/random/Random.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.system.random 2 | 3 | import cats.effect.Sync 4 | 5 | /** Pure pseudo-random number generator based on the JVM implementation. */ 6 | trait Random[F[_]] { 7 | 8 | def nextDouble: F[Double] 9 | def nextBoolean: F[Boolean] 10 | def nextFloat: F[Float] 11 | def nextInt: F[Int] 12 | def nextInt(n: Int): F[Int] 13 | def nextLong: F[Long] 14 | def nextPrintableChar: F[Char] 15 | def nextString(length: Int): F[String] 16 | 17 | } 18 | 19 | object Random { 20 | 21 | def apply[F[_]: Sync](rnd: scala.util.Random): Random[F] = 22 | new Random[F] { 23 | 24 | private val F = Sync[F] 25 | 26 | override def nextDouble: F[Double] = F.delay(rnd.nextDouble()) 27 | 28 | override def nextBoolean: F[Boolean] = F.delay(rnd.nextBoolean()) 29 | 30 | override def nextFloat: F[Float] = F.delay(rnd.nextFloat()) 31 | 32 | override def nextInt: F[Int] = F.delay(rnd.nextInt()) 33 | 34 | override def nextInt(n: Int): F[Int] = F.delay(rnd.nextInt(n)) 35 | 36 | override def nextLong: F[Long] = F.delay(rnd.nextLong()) 37 | 38 | override def nextPrintableChar: F[Char] = F.delay(rnd.nextPrintableChar()) 39 | 40 | override def nextString(length: Int): F[String] = F.delay(rnd.nextString(length)) 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /jvm/src/main/scala/com/avast/sst/jvm/system/random/RandomModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.system.random 2 | 3 | import cats.effect.Sync 4 | 5 | import java.security.SecureRandom 6 | 7 | /** Provides random number generators. */ 8 | object RandomModule { 9 | 10 | /** Makes [[com.avast.sst.jvm.system.random.Random]] with default random seed. */ 11 | def makeRandom[F[_]: Sync]: F[Random[F]] = Sync[F].delay(Random(new scala.util.Random())) 12 | 13 | /** Makes [[com.avast.sst.jvm.system.random.Random]] with the provided `seed`. */ 14 | def makeRandom[F[_]: Sync](seed: Long): F[Random[F]] = Sync[F].delay(Random(new scala.util.Random(seed))) 15 | 16 | /** Makes [[com.avast.sst.jvm.system.random.Random]] based on [[java.security.SecureRandom]] with default random seed. */ 17 | def makeSecureRandom[F[_]: Sync]: F[Random[F]] = Sync[F].delay(Random(new SecureRandom())) 18 | 19 | /** Makes [[com.avast.sst.jvm.system.random.Random]] based on [[java.security.SecureRandom]] with the provided `seed`. */ 20 | def makeSecureRandom[F[_]: Sync](seed: Array[Byte]): F[Random[F]] = Sync[F].delay(Random(new SecureRandom(seed))) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /jvm/src/test/scala/com/avast/sst/jvm/execution/ExecutorModuleTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.execution 2 | 3 | import cats.effect.SyncIO 4 | import org.scalatest.funsuite.AnyFunSuite 5 | 6 | class ExecutorModuleTest extends AnyFunSuite { 7 | 8 | test("ExecutorModule initializes properly and blocking executor differs from callback executor") { 9 | val executorModule = ExecutorModule.makeDefault[SyncIO].use(m => SyncIO.pure(m)).unsafeRunSync() 10 | assert(executorModule.executionContext !== executorModule.blocker.blockingContext) 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /jvm/src/test/scala/com/avast/sst/jvm/system/console/ConsoleModuleTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.system.console 2 | 3 | import cats.effect.SyncIO 4 | import org.scalatest.funsuite.AnyFunSuite 5 | 6 | import java.io.{ByteArrayInputStream, ByteArrayOutputStream} 7 | import scala.Console as SConsole 8 | 9 | class ConsoleModuleTest extends AnyFunSuite { 10 | 11 | test("Console input") { 12 | SConsole.withIn(new ByteArrayInputStream("test input\n".getBytes("UTF-8"))) { 13 | val test = for { 14 | line <- ConsoleModule.make[SyncIO].readLine 15 | } yield assert(line === "test input") 16 | 17 | test.unsafeRunSync() 18 | } 19 | } 20 | 21 | test("Console output") { 22 | val out = new ByteArrayOutputStream() 23 | SConsole.withOut(out) { 24 | val test = for { 25 | _ <- ConsoleModule.make[SyncIO].printLine("test output") 26 | } yield () 27 | 28 | test.unsafeRunSync() 29 | } 30 | 31 | assert(out.toString("UTF-8") === "test output\n") 32 | } 33 | 34 | test("Console error") { 35 | val out = new ByteArrayOutputStream() 36 | SConsole.withErr(out) { 37 | val test = for { 38 | _ <- ConsoleModule.make[SyncIO].printLineToError("test output") 39 | } yield () 40 | 41 | test.unsafeRunSync() 42 | } 43 | 44 | assert(out.toString("UTF-8") === "test output\n") 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /jvm/src/test/scala/com/avast/sst/jvm/system/random/RandomModuleTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.jvm.system.random 2 | 3 | import cats.effect.SyncIO 4 | import org.scalatest.funsuite.AnyFunSuite 5 | 6 | class RandomModuleTest extends AnyFunSuite { 7 | 8 | test("RandomModule initializes properly") { 9 | val test = for { 10 | random <- RandomModule.makeRandom[SyncIO](123L) 11 | number <- random.nextInt 12 | } yield assert(number === -1188957731L) 13 | 14 | test.unsafeRunSync() 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /lettuce-pureconfig/src/main/scala-2/com/avast/sst/lettuce/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.lettuce.pureconfig 2 | 3 | import cats.syntax.either._ 4 | import com.avast.sst.lettuce.LettuceConfig 5 | import com.avast.sst.lettuce.LettuceConfig.{SocketOptions, SslOptions, TimeoutOptions} 6 | import io.lettuce.core.ClientOptions.DisconnectedBehavior 7 | import io.lettuce.core.protocol.ProtocolVersion 8 | import pureconfig.ConfigReader 9 | import pureconfig.error.CannotConvert 10 | import pureconfig.generic.ProductHint 11 | import pureconfig.generic.semiauto._ 12 | 13 | import java.nio.charset.Charset 14 | 15 | trait ConfigReaders { 16 | 17 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 18 | 19 | implicit val lettuceDisconnectedBehaviorConfigReader: ConfigReader[DisconnectedBehavior] = ConfigReader.stringConfigReader.emap { 20 | case "DEFAULT" => DisconnectedBehavior.DEFAULT.asRight 21 | case "ACCEPT_COMMANDS" => DisconnectedBehavior.ACCEPT_COMMANDS.asRight 22 | case "REJECT_COMMANDS" => DisconnectedBehavior.REJECT_COMMANDS.asRight 23 | case unknown => 24 | CannotConvert( 25 | unknown, 26 | "DisconnectedBehavior", 27 | s"Unknown enum value: ${DisconnectedBehavior.values().map(_.name()).mkString("|")}" 28 | ).asLeft 29 | } 30 | 31 | implicit val lettuceProtocolVersionConfigReader: ConfigReader[ProtocolVersion] = ConfigReader.stringConfigReader.emap { 32 | case "RESP2" => ProtocolVersion.RESP2.asRight 33 | case "RESP3" => ProtocolVersion.RESP3.asRight 34 | case unknown => 35 | CannotConvert( 36 | unknown, 37 | "ProtocolVersion", 38 | s"Unknown enum value: ${ProtocolVersion.values().map(_.name()).mkString("|")}" 39 | ).asLeft 40 | } 41 | 42 | implicit val lettuceCharsetConfigReader: ConfigReader[Charset] = ConfigReader.stringConfigReader.emap { charset => 43 | Either.catchNonFatal(Charset.forName(charset)).leftMap(ex => CannotConvert(charset, "java.nio.Charset", ex.getMessage)) 44 | } 45 | 46 | implicit val lettuceSocketOptionsReader: ConfigReader[SocketOptions] = deriveReader[SocketOptions] 47 | 48 | implicit val lettuceSslOptionsReader: ConfigReader[SslOptions] = deriveReader[SslOptions] 49 | 50 | implicit val lettuceTimeoutOptionsReader: ConfigReader[TimeoutOptions] = deriveReader[TimeoutOptions] 51 | 52 | implicit val lettuceConfigReader: ConfigReader[LettuceConfig] = deriveReader[LettuceConfig] 53 | 54 | } 55 | -------------------------------------------------------------------------------- /lettuce-pureconfig/src/main/scala-2/com/avast/sst/lettuce/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.lettuce.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /lettuce-pureconfig/src/main/scala-3/com/avast/sst/lettuce/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.lettuce.pureconfig 2 | 3 | import cats.syntax.either.* 4 | import com.avast.sst.lettuce.LettuceConfig 5 | import com.avast.sst.lettuce.LettuceConfig.{SocketOptions, SslOptions, TimeoutOptions} 6 | import io.lettuce.core.ClientOptions.DisconnectedBehavior 7 | import io.lettuce.core.protocol.ProtocolVersion 8 | import pureconfig.ConfigReader 9 | import pureconfig.error.CannotConvert 10 | import pureconfig.generic.derivation.default.* 11 | 12 | import java.nio.charset.Charset 13 | 14 | trait ConfigReaders { 15 | 16 | implicit val lettuceDisconnectedBehaviorConfigReader: ConfigReader[DisconnectedBehavior] = ConfigReader.stringConfigReader.emap { 17 | case "DEFAULT" => DisconnectedBehavior.DEFAULT.asRight 18 | case "ACCEPT_COMMANDS" => DisconnectedBehavior.ACCEPT_COMMANDS.asRight 19 | case "REJECT_COMMANDS" => DisconnectedBehavior.REJECT_COMMANDS.asRight 20 | case unknown => 21 | CannotConvert( 22 | unknown, 23 | "DisconnectedBehavior", 24 | s"Unknown enum value: ${DisconnectedBehavior.values().map(_.name()).mkString("|")}" 25 | ).asLeft 26 | } 27 | 28 | implicit val lettuceProtocolVersionConfigReader: ConfigReader[ProtocolVersion] = ConfigReader.stringConfigReader.emap { 29 | case "RESP2" => ProtocolVersion.RESP2.asRight 30 | case "RESP3" => ProtocolVersion.RESP3.asRight 31 | case unknown => 32 | CannotConvert( 33 | unknown, 34 | "ProtocolVersion", 35 | s"Unknown enum value: ${ProtocolVersion.values().map(_.name()).mkString("|")}" 36 | ).asLeft 37 | } 38 | 39 | implicit val lettuceCharsetConfigReader: ConfigReader[Charset] = ConfigReader.stringConfigReader.emap { charset => 40 | Either.catchNonFatal(Charset.forName(charset)).leftMap(ex => CannotConvert(charset, "java.nio.Charset", ex.getMessage)) 41 | } 42 | 43 | implicit val lettuceSocketOptionsReader: ConfigReader[SocketOptions] = ConfigReader.derived 44 | 45 | implicit val lettuceSslOptionsReader: ConfigReader[SslOptions] = ConfigReader.derived 46 | 47 | implicit val lettuceTimeoutOptionsReader: ConfigReader[TimeoutOptions] = ConfigReader.derived 48 | 49 | implicit val lettuceConfigReader: ConfigReader[LettuceConfig] = ConfigReader.derived 50 | 51 | } 52 | -------------------------------------------------------------------------------- /lettuce-pureconfig/src/main/scala-3/com/avast/sst/lettuce/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.lettuce.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /lettuce/src/main/scala/com/avast/sst/lettuce/LettuceConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.lettuce 2 | 3 | import com.avast.sst.lettuce.LettuceConfig.{SocketOptions, SslOptions, TimeoutOptions} 4 | import io.lettuce.core.ClientOptions.DisconnectedBehavior 5 | import io.lettuce.core.protocol.ProtocolVersion 6 | import io.lettuce.core.{ClientOptions, SocketOptions as LettuceSocketOptions, TimeoutOptions as LettuceTimeoutOptions} 7 | 8 | import java.nio.charset.Charset 9 | import scala.concurrent.duration.Duration 10 | 11 | final case class LettuceConfig( 12 | uri: String, 13 | pingBeforeActivateConnection: Boolean = ClientOptions.DEFAULT_PING_BEFORE_ACTIVATE_CONNECTION, 14 | autoReconnect: Boolean = ClientOptions.DEFAULT_AUTO_RECONNECT, 15 | suspendReconnectOnProtocolFailure: Boolean = ClientOptions.DEFAULT_SUSPEND_RECONNECT_PROTO_FAIL, 16 | requestQueueSize: Int = ClientOptions.DEFAULT_REQUEST_QUEUE_SIZE, 17 | disconnectedBehavior: DisconnectedBehavior = DisconnectedBehavior.DEFAULT, 18 | protocolVersion: Option[ProtocolVersion] = None, 19 | scriptCharset: Charset = ClientOptions.DEFAULT_SCRIPT_CHARSET, 20 | publishOnScheduler: Boolean = ClientOptions.DEFAULT_SUSPEND_RECONNECT_PROTO_FAIL, 21 | socketOptions: SocketOptions = SocketOptions(), 22 | sslOptions: SslOptions = SslOptions(), 23 | timeoutOptions: TimeoutOptions = TimeoutOptions() 24 | ) 25 | 26 | object LettuceConfig { 27 | 28 | final case class SocketOptions( 29 | connectTimeout: Duration = Duration.fromNanos(LettuceSocketOptions.DEFAULT_CONNECT_TIMEOUT_DURATION.toNanos), 30 | keepAlive: Boolean = LettuceSocketOptions.DEFAULT_SO_KEEPALIVE, 31 | tcpNoDelay: Boolean = LettuceSocketOptions.DEFAULT_SO_NO_DELAY 32 | ) 33 | 34 | final case class SslOptions( 35 | keyStoreType: Option[String] = None, 36 | keyStorePath: Option[String] = None, 37 | keyStorePassword: Option[String] = None, 38 | trustStorePath: Option[String] = None, 39 | trustStorePassword: Option[String] = None 40 | ) 41 | 42 | final case class TimeoutOptions(timeoutCommands: Boolean = LettuceTimeoutOptions.DEFAULT_TIMEOUT_COMMANDS) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /micrometer-jmx-pureconfig/src/main/scala-2/com/avast/sst/micrometer/jmx/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.jmx.pureconfig 2 | 3 | import com.avast.sst.micrometer.jmx.MicrometerJmxConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.ProductHint 6 | import pureconfig.generic.semiauto._ 7 | 8 | trait ConfigReaders { 9 | 10 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 11 | 12 | implicit val micrometerMicrometerJmxConfigReader: ConfigReader[MicrometerJmxConfig] = deriveReader[MicrometerJmxConfig] 13 | 14 | } 15 | -------------------------------------------------------------------------------- /micrometer-jmx-pureconfig/src/main/scala-2/com/avast/sst/micrometer/jmx/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.jmx.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /micrometer-jmx-pureconfig/src/main/scala-3/com/avast/sst/micrometer/jmx/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.jmx.pureconfig 2 | 3 | import com.avast.sst.micrometer.jmx.MicrometerJmxConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.derivation.default.* 6 | 7 | trait ConfigReaders { 8 | 9 | implicit val micrometerMicrometerJmxConfigReader: ConfigReader[MicrometerJmxConfig] = ConfigReader.derived 10 | 11 | } 12 | -------------------------------------------------------------------------------- /micrometer-jmx-pureconfig/src/main/scala-3/com/avast/sst/micrometer/jmx/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.jmx.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/MicrometerJmxConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.jmx 2 | 3 | import java.util.concurrent.TimeUnit 4 | import scala.concurrent.duration.Duration 5 | 6 | final case class MicrometerJmxConfig( 7 | domain: String, 8 | enableTypeScopeNameHierarchy: Boolean = false, 9 | step: Duration = Duration(1, TimeUnit.MINUTES) 10 | ) 11 | -------------------------------------------------------------------------------- /micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/MicrometerJmxModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.jmx 2 | 3 | import cats.effect.{Resource, Sync} 4 | import com.codahale.metrics.MetricRegistry 5 | import com.codahale.metrics.jmx.JmxReporter 6 | import io.micrometer.core.instrument.Clock 7 | import io.micrometer.core.instrument.config.NamingConvention 8 | import io.micrometer.core.instrument.util.HierarchicalNameMapper 9 | import io.micrometer.jmx.{JmxConfig, JmxMeterRegistry} 10 | 11 | import java.time.Duration 12 | 13 | object MicrometerJmxModule { 14 | 15 | /** Makes configured [[io.micrometer.jmx.JmxMeterRegistry]]. */ 16 | def make[F[_]: Sync]( 17 | config: MicrometerJmxConfig, 18 | clock: Clock = Clock.SYSTEM, 19 | nameMapper: HierarchicalNameMapper = HierarchicalNameMapper.DEFAULT 20 | ): Resource[F, JmxMeterRegistry] = { 21 | Resource 22 | .make { 23 | Sync[F].delay { 24 | if (config.enableTypeScopeNameHierarchy) { 25 | val dropwizardRegistry = new MetricRegistry 26 | val registry = new JmxMeterRegistry( 27 | new CustomJmxConfig(config), 28 | clock, 29 | nameMapper, 30 | dropwizardRegistry, 31 | makeJmxReporter(dropwizardRegistry, config.domain) 32 | ) 33 | registry.config.namingConvention(NamingConvention.dot) 34 | registry 35 | } else { 36 | new JmxMeterRegistry(new CustomJmxConfig(config), clock, nameMapper) 37 | } 38 | } 39 | }(registry => Sync[F].delay(registry.close())) 40 | } 41 | 42 | private def makeJmxReporter(metricRegistry: MetricRegistry, domain: String) = { 43 | JmxReporter 44 | .forRegistry(metricRegistry) 45 | .inDomain(domain) 46 | .createsObjectNamesWith(new TypeScopeNameObjectNameFactory()) 47 | .build 48 | } 49 | 50 | private class CustomJmxConfig(c: MicrometerJmxConfig) extends JmxConfig { 51 | 52 | override val domain: String = c.domain 53 | override val step: Duration = Duration.ofMillis(c.step.toMillis) 54 | 55 | // the method is @Nullable and we don't need to implement it here 56 | @SuppressWarnings(Array("scalafix:DisableSyntax.null")) 57 | override def get(key: String): String = null 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /micrometer-jmx/src/main/scala/com/avast/sst/micrometer/jmx/TypeScopeNameObjectNameFactory.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.jmx 2 | 3 | import cats.syntax.either.* 4 | import com.codahale.metrics.jmx.{DefaultObjectNameFactory, ObjectNameFactory} 5 | 6 | import java.util 7 | import java.util.regex.Pattern 8 | import javax.management.ObjectName 9 | 10 | /** This is custom [[com.codahale.metrics.jmx.ObjectNameFactory]] which uses "type-scope-name" hierarchy of resulting 11 | * [[javax.management.ObjectName]] (levels 3-N are glued together). 12 | */ 13 | private[jmx] class TypeScopeNameObjectNameFactory(separator: String = ".") extends ObjectNameFactory { 14 | 15 | private val quotedSeparator = Pattern.quote(separator) 16 | 17 | private val defaultFactory = new DefaultObjectNameFactory() 18 | 19 | private val partNames = Vector("type", "scope", "name") 20 | 21 | override def createName(`type`: String, domain: String, name: String): ObjectName = { 22 | val parsedName = parseName(domain, name) 23 | parsedName.getOrElse(defaultFactory.createName(`type`, domain, name)) 24 | } 25 | 26 | private def parseName(domain: String, name: String) = 27 | Either.catchNonFatal { 28 | val parts = name.split(quotedSeparator, partNames.length) 29 | 30 | /* The following block of code is a little hack. The problem is that ObjectName requires HashTable as parameter but HashTable 31 | is unsorted and thus unusable for us. We hack it by raping the HashTable and in-fact using LinkedHashMap which is 32 | much more suitable for our needs. */ 33 | val map = new java.util.LinkedHashMap[String, String](parts.length) 34 | val properties = new java.util.Hashtable[String, String](parts.length) { 35 | override def entrySet(): util.Set[util.Map.Entry[String, String]] = map.entrySet() 36 | } 37 | 38 | parts.zip(partNames).foreach { case (part, partName) => 39 | val quoted = quote(part) 40 | properties.put(partName, quoted) 41 | map.put(partName, quoted) 42 | } 43 | 44 | new ObjectName(domain, properties) 45 | } 46 | 47 | private def quote(objectName: String) = objectName.replaceAll("[\\Q.?*\"\\E]", "_") 48 | 49 | } 50 | -------------------------------------------------------------------------------- /micrometer-prometheus-pureconfig/src/main/scala-2/com/avast/sst/micrometer/prometheus/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.prometheus.pureconfig 2 | 3 | import com.avast.sst.micrometer.prometheus.MicrometerPrometheusConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.ProductHint 6 | import pureconfig.generic.semiauto.deriveReader 7 | 8 | trait ConfigReaders { 9 | 10 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 11 | 12 | implicit val micrometerPrometheusConfigReader: ConfigReader[MicrometerPrometheusConfig] = deriveReader 13 | 14 | } 15 | -------------------------------------------------------------------------------- /micrometer-prometheus-pureconfig/src/main/scala-2/com/avast/sst/micrometer/prometheus/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.prometheus.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /micrometer-prometheus-pureconfig/src/main/scala-3/com/avast/sst/micrometer/prometheus/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.prometheus.pureconfig 2 | 3 | import com.avast.sst.micrometer.prometheus.MicrometerPrometheusConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.derivation.default.* 6 | 7 | trait ConfigReaders { 8 | 9 | implicit val micrometerPrometheusConfigReader: ConfigReader[MicrometerPrometheusConfig] = 10 | ConfigReader.derived 11 | 12 | } 13 | -------------------------------------------------------------------------------- /micrometer-prometheus-pureconfig/src/main/scala-3/com/avast/sst/micrometer/prometheus/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.prometheus.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /micrometer-prometheus/src/main/scala/com/avast/sst/micrometer/prometheus/MicrometerPrometheusConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.prometheus 2 | 3 | import io.micrometer.prometheus.HistogramFlavor 4 | 5 | import java.util.concurrent.TimeUnit 6 | import scala.concurrent.duration.Duration 7 | 8 | final case class MicrometerPrometheusConfig( 9 | step: Duration = Duration(1, TimeUnit.MINUTES), 10 | prefix: String = "", 11 | descriptions: Boolean = true, 12 | histogramFlavor: HistogramFlavor = HistogramFlavor.Prometheus, 13 | commonTags: Map[String, String] = Map.empty 14 | ) 15 | -------------------------------------------------------------------------------- /micrometer-prometheus/src/main/scala/com/avast/sst/micrometer/prometheus/MicrometerPrometheusModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.prometheus 2 | 3 | import cats.effect.{Blocker, ContextShift, Resource, Sync} 4 | import com.avast.sst.micrometer.PrefixMeterFilter 5 | import io.micrometer.core.instrument.config.{MeterFilter, NamingConvention} 6 | import io.micrometer.prometheus.{HistogramFlavor, PrometheusConfig, PrometheusMeterRegistry} 7 | 8 | import java.time.Duration 9 | 10 | object MicrometerPrometheusModule { 11 | 12 | /** Makes configured [[io.micrometer.prometheus.PrometheusMeterRegistry]]. */ 13 | def make[F[_]: Sync: ContextShift]( 14 | config: MicrometerPrometheusConfig, 15 | blocker: Blocker, 16 | namingConvention: Option[NamingConvention] = None, 17 | meterFilter: Option[MeterFilter] = None 18 | ): Resource[F, PrometheusMeterRegistry] = { 19 | Resource 20 | .make { 21 | Sync[F].delay { 22 | val registry = new PrometheusMeterRegistry(new CustomPrometheusConfig(config)) 23 | 24 | namingConvention.foreach(registry.config().namingConvention) 25 | 26 | if (config.prefix.nonEmpty) { 27 | registry.config().meterFilter(new PrefixMeterFilter(config.prefix)) 28 | } 29 | 30 | meterFilter.foreach(registry.config().meterFilter) 31 | 32 | val preprocessedTags = config.commonTags.foldRight(List.empty[String]) { case (tag, acc) => 33 | tag._1 :: tag._2 :: acc 34 | } 35 | registry.config().commonTags(preprocessedTags*) 36 | 37 | registry 38 | } 39 | }(registry => blocker.delay(registry.close())) 40 | } 41 | 42 | private class CustomPrometheusConfig(c: MicrometerPrometheusConfig) extends PrometheusConfig { 43 | 44 | override val step: Duration = java.time.Duration.ofMillis(c.step.toMillis) 45 | override val prefix: String = c.prefix 46 | override val descriptions: Boolean = c.descriptions 47 | override val histogramFlavor: HistogramFlavor = c.histogramFlavor 48 | 49 | // the method is @Nullable and we don't need to implement it here 50 | @SuppressWarnings(Array("scalafix:DisableSyntax.null")) 51 | override def get(key: String): String = null 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /micrometer-prometheus/src/test/scala/com/avast/sst/micrometer/prometheus/Http4sPrometheusCompatibilityTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.prometheus 2 | 3 | import cats.effect.* 4 | import monix.eval.Task 5 | import monix.eval.instances.CatsConcurrentEffectForTask 6 | import monix.execution.Scheduler 7 | import org.http4s.metrics.prometheus.PrometheusExportService 8 | import org.scalatest.funsuite.AnyFunSuite 9 | 10 | import java.util.concurrent.{SynchronousQueue, ThreadPoolExecutor, TimeUnit} 11 | import scala.concurrent.ExecutionContext 12 | 13 | class Http4sPrometheusCompatibilityTest extends AnyFunSuite { 14 | 15 | implicit def scheduler: Scheduler = Scheduler.global 16 | 17 | protected def options: Task.Options = Task.defaultOptions.withSchedulerFeatures(scheduler) 18 | 19 | protected implicit lazy val catsEffect: ConcurrentEffect[Task] = 20 | new CatsConcurrentEffectForTask()(scheduler, options) 21 | 22 | test("Http4s Prometheus compatibility test") { 23 | 24 | val config = MicrometerPrometheusConfig() 25 | 26 | val blockingExecutor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS, new SynchronousQueue()) 27 | val blocker = Blocker.liftExecutionContext(ExecutionContext.fromExecutor(blockingExecutor)) 28 | 29 | val test = for { 30 | 31 | prometheusMeterRegistry <- MicrometerPrometheusModule.make(config, blocker) 32 | _ = PrometheusExportService(prometheusMeterRegistry.getPrometheusRegistry) 33 | _ <- PrometheusExportService.addDefaults(prometheusMeterRegistry.getPrometheusRegistry) 34 | } yield () 35 | 36 | test.use(_ => Task.unit).runSyncUnsafe() 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /micrometer-statsd-pureconfig/src/main/scala-2/com/avast/sst/micrometer/statsd/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.statsd.pureconfig 2 | 3 | import com.avast.sst.micrometer.statsd.MicrometerStatsDConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.ProductHint 6 | import pureconfig.generic.semiauto._ 7 | 8 | trait ConfigReaders { 9 | 10 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 11 | 12 | implicit val micrometerMicrometerStatsDConfigReader: ConfigReader[MicrometerStatsDConfig] = deriveReader[MicrometerStatsDConfig] 13 | 14 | } 15 | -------------------------------------------------------------------------------- /micrometer-statsd-pureconfig/src/main/scala-2/com/avast/sst/micrometer/statsd/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.statsd.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /micrometer-statsd-pureconfig/src/main/scala-3/com/avast/sst/micrometer/statsd/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.statsd.pureconfig 2 | 3 | import com.avast.sst.micrometer.statsd.MicrometerStatsDConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.derivation.default.* 6 | 7 | trait ConfigReaders { 8 | 9 | implicit val micrometerMicrometerStatsDConfigReader: ConfigReader[MicrometerStatsDConfig] = 10 | ConfigReader.derived 11 | 12 | } 13 | -------------------------------------------------------------------------------- /micrometer-statsd-pureconfig/src/main/scala-3/com/avast/sst/micrometer/statsd/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.statsd.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /micrometer-statsd/src/main/scala/com/avast/sst/micrometer/statsd/MicrometerStatsDConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.statsd 2 | 3 | import io.micrometer.statsd.{StatsdFlavor, StatsdProtocol} 4 | 5 | import java.util.concurrent.TimeUnit 6 | import scala.concurrent.duration.Duration 7 | 8 | final case class MicrometerStatsDConfig( 9 | host: String, 10 | port: Int = 8125, 11 | flavor: StatsdFlavor = StatsdFlavor.ETSY, 12 | enabled: Boolean = true, 13 | protocol: StatsdProtocol = StatsdProtocol.UDP, 14 | maxPacketLength: Int = 1400, 15 | pollingFrequency: Duration = Duration(10, TimeUnit.SECONDS), 16 | step: Duration = Duration(1, TimeUnit.MINUTES), 17 | publishUnchangedMeters: Boolean = true, 18 | buffered: Boolean = true, 19 | prefix: String = "", 20 | commonTags: Map[String, String] = Map.empty 21 | ) 22 | -------------------------------------------------------------------------------- /micrometer-statsd/src/main/scala/com/avast/sst/micrometer/statsd/MicrometerStatsDModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer.statsd 2 | 3 | import cats.effect.{Blocker, ContextShift, Resource, Sync} 4 | import com.avast.sst.micrometer.PrefixMeterFilter 5 | import io.micrometer.core.instrument.Clock 6 | import io.micrometer.core.instrument.config.{MeterFilter, NamingConvention} 7 | import io.micrometer.core.instrument.util.HierarchicalNameMapper 8 | import io.micrometer.statsd.* 9 | 10 | import java.time.Duration 11 | 12 | object MicrometerStatsDModule { 13 | 14 | /** Makes configured [[io.micrometer.statsd.StatsdMeterRegistry]]. */ 15 | def make[F[_]: Sync: ContextShift]( 16 | config: MicrometerStatsDConfig, 17 | blocker: Blocker, 18 | clock: Clock = Clock.SYSTEM, 19 | nameMapper: HierarchicalNameMapper = HierarchicalNameMapper.DEFAULT, 20 | namingConvention: Option[NamingConvention] = None, 21 | meterFilter: Option[MeterFilter] = None 22 | ): Resource[F, StatsdMeterRegistry] = { 23 | Resource 24 | .make { 25 | Sync[F].delay { 26 | val registry = StatsdMeterRegistry 27 | .builder(new CustomStatsdConfig(config)) 28 | .clock(clock) 29 | .nameMapper(nameMapper) 30 | .build 31 | 32 | namingConvention.foreach(registry.config().namingConvention) 33 | 34 | if (config.prefix.nonEmpty) { 35 | registry.config().meterFilter(new PrefixMeterFilter(config.prefix)) 36 | } 37 | 38 | meterFilter.foreach(registry.config().meterFilter) 39 | 40 | val preprocessedTags = config.commonTags.foldRight(List.empty[String]) { case (tag, acc) => 41 | tag._1 :: tag._2 :: acc 42 | } 43 | registry.config().commonTags(preprocessedTags*) 44 | 45 | registry 46 | } 47 | }(registry => blocker.delay(registry.close())) 48 | } 49 | 50 | private class CustomStatsdConfig(c: MicrometerStatsDConfig) extends StatsdConfig { 51 | 52 | override val flavor: StatsdFlavor = c.flavor 53 | 54 | override val enabled: Boolean = c.enabled 55 | 56 | override val host: String = c.host 57 | 58 | override val port: Int = c.port 59 | 60 | override val protocol: StatsdProtocol = c.protocol 61 | 62 | override val maxPacketLength: Int = c.maxPacketLength 63 | 64 | override val pollingFrequency: Duration = java.time.Duration.ofMillis(c.pollingFrequency.toMillis) 65 | 66 | override val step: Duration = java.time.Duration.ofMillis(c.step.toMillis) 67 | 68 | override val publishUnchangedMeters: Boolean = c.publishUnchangedMeters 69 | 70 | override val buffered: Boolean = c.buffered 71 | 72 | // the method is @Nullable and we don't need to implement it here 73 | @SuppressWarnings(Array("scalafix:DisableSyntax.null")) 74 | override def get(key: String): String = null 75 | 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /micrometer/src/main/scala/com/avast/sst/micrometer/PrefixMeterFilter.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer 2 | 3 | import io.micrometer.core.instrument.Meter 4 | import io.micrometer.core.instrument.config.MeterFilter 5 | 6 | class PrefixMeterFilter(prefix: String) extends MeterFilter { 7 | 8 | override def map(id: Meter.Id): Meter.Id = id.withName(s"$prefix${id.getName}") 9 | 10 | } 11 | -------------------------------------------------------------------------------- /micrometer/src/test/scala/com/avast/sst/micrometer/PrefixMeterFilterTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.micrometer 2 | 3 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry 4 | import org.scalatest.funsuite.AnyFunSuite 5 | 6 | class PrefixMeterFilterTest extends AnyFunSuite { 7 | 8 | test("prefixing") { 9 | val registry = new SimpleMeterRegistry 10 | registry.config().meterFilter(new PrefixMeterFilter("this.is.prefix.")) 11 | val counter = registry.counter("test") 12 | 13 | assert(counter.getId.getName === "this.is.prefix.test") 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /monix-catnap-micrometer/src/main/scala/com/avast/sst/monix/catnap/micrometer/MicrometerCircuitBreakerMetricsModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.monix.catnap.micrometer 2 | 3 | import cats.effect.Sync 4 | import cats.syntax.functor.* 5 | import com.avast.sst.monix.catnap.CircuitBreakerMetrics 6 | import com.avast.sst.monix.catnap.CircuitBreakerMetrics.State 7 | import com.avast.sst.monix.catnap.CircuitBreakerMetrics.State.{Closed, HalfOpen, Open} 8 | import io.micrometer.core.instrument.MeterRegistry 9 | 10 | import java.util.concurrent.atomic.AtomicInteger 11 | 12 | object MicrometerCircuitBreakerMetricsModule { 13 | 14 | /** Makes `CircuitBreakerMetrics` from [[io.micrometer.core.instrument.MeterRegistry]]. */ 15 | def make[F[_]: Sync](name: String, meterRegistry: MeterRegistry): F[CircuitBreakerMetrics[F]] = { 16 | for { 17 | circuitBreakerState <- Sync[F].delay(new AtomicInteger(CircuitClosed)) 18 | } yield new MicrometerCircuitBreakerMetrics(name, meterRegistry, circuitBreakerState) 19 | } 20 | 21 | private class MicrometerCircuitBreakerMetrics[F[_]: Sync](name: String, meterRegistry: MeterRegistry, state: AtomicInteger) 22 | extends CircuitBreakerMetrics[F] { 23 | 24 | private val F = Sync[F] 25 | 26 | private val rejected = meterRegistry.counter(s"circuit-breaker.$name.rejected") 27 | private val circuitState = meterRegistry.gauge[AtomicInteger](s"circuit-breaker.$name.state", state) 28 | 29 | override def increaseRejected: F[Unit] = F.delay(rejected.increment()) 30 | 31 | override def setState(state: State): F[Unit] = { 32 | state match { 33 | case Closed => F.delay(circuitState.set(CircuitClosed)) 34 | case Open => F.delay(circuitState.set(CircuitOpened)) 35 | case HalfOpen => F.delay(circuitState.set(CircuitHalfOpened)) 36 | } 37 | } 38 | 39 | } 40 | 41 | private val CircuitOpened = -1 42 | private val CircuitHalfOpened = 0 43 | private val CircuitClosed = 1 44 | 45 | } 46 | -------------------------------------------------------------------------------- /monix-catnap-pureconfig/src/main/scala-2/com/avast/sst/monix/catnap/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.monix.catnap.pureconfig 2 | 3 | import com.avast.sst.monix.catnap.CircuitBreakerConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.ProductHint 6 | import pureconfig.generic.semiauto._ 7 | 8 | trait ConfigReaders { 9 | 10 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 11 | 12 | implicit val monixCatnapCircuitBreakerConfigReader: ConfigReader[CircuitBreakerConfig] = deriveReader[CircuitBreakerConfig] 13 | 14 | } 15 | -------------------------------------------------------------------------------- /monix-catnap-pureconfig/src/main/scala-2/com/avast/sst/monix/catnap/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.monix.catnap.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /monix-catnap-pureconfig/src/main/scala-3/com/avast/sst/monix/catnap/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.monix.catnap.pureconfig 2 | 3 | import com.avast.sst.monix.catnap.CircuitBreakerConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.derivation.default.* 6 | 7 | trait ConfigReaders { 8 | 9 | implicit val monixCatnapCircuitBreakerConfigReader: ConfigReader[CircuitBreakerConfig] = ConfigReader.derived 10 | 11 | } 12 | -------------------------------------------------------------------------------- /monix-catnap-pureconfig/src/main/scala-3/com/avast/sst/monix/catnap/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.monix.catnap.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /monix-catnap/src/main/scala/com/avast/sst/monix/catnap/CircuitBreakerConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.monix.catnap 2 | 3 | import scala.concurrent.duration.{Duration, FiniteDuration} 4 | 5 | final case class CircuitBreakerConfig( 6 | maxFailures: Int, 7 | resetTimeout: FiniteDuration, 8 | exponentialBackoffFactor: Double = 1.0, 9 | maxResetTimeout: Duration = Duration.Inf 10 | ) 11 | -------------------------------------------------------------------------------- /monix-catnap/src/main/scala/com/avast/sst/monix/catnap/CircuitBreakerMetrics.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.monix.catnap 2 | 3 | import cats.Applicative 4 | import com.avast.sst.monix.catnap.CircuitBreakerMetrics.State 5 | 6 | /** Implement this trait for your monitoring system if you want to get insight into your circuit breaker. */ 7 | trait CircuitBreakerMetrics[F[_]] { 8 | 9 | def increaseRejected: F[Unit] 10 | 11 | def setState(state: State): F[Unit] 12 | 13 | } 14 | 15 | object CircuitBreakerMetrics { 16 | 17 | sealed trait State 18 | 19 | object State { 20 | 21 | case object Closed extends State 22 | case object HalfOpen extends State 23 | case object Open extends State 24 | 25 | } 26 | 27 | def noop[F[_]](implicit F: Applicative[F]): CircuitBreakerMetrics[F] = 28 | new CircuitBreakerMetrics[F] { 29 | override def increaseRejected: F[Unit] = F.unit 30 | override def setState(state: State): F[Unit] = F.unit 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /monix-catnap/src/main/scala/com/avast/sst/monix/catnap/CircuitBreakerModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.monix.catnap 2 | 3 | import cats.effect.{Clock, Sync} 4 | import com.avast.sst.monix.catnap.CircuitBreakerMetrics.State.{Closed, HalfOpen, Open} 5 | import monix.catnap.CircuitBreaker 6 | import org.slf4j.LoggerFactory 7 | 8 | class CircuitBreakerModule[F[_]](implicit F: Sync[F]) { 9 | 10 | /** Makes [[monix.catnap.CircuitBreaker]] initialized with the given config and `cats.effect.Clock`. */ 11 | def make( 12 | config: CircuitBreakerConfig, 13 | clock: Clock[F], 14 | onRejected: F[Unit] = F.unit, 15 | onClosed: F[Unit] = F.unit, 16 | onHalfOpen: F[Unit] = F.unit, 17 | onOpen: F[Unit] = F.unit 18 | ): F[CircuitBreaker[F]] = { 19 | CircuitBreaker[F].of( 20 | config.maxFailures, 21 | config.resetTimeout, 22 | config.exponentialBackoffFactor, 23 | config.maxResetTimeout, 24 | onRejected, 25 | onClosed, 26 | onHalfOpen, 27 | onOpen 28 | )(clock) 29 | } 30 | 31 | } 32 | 33 | object CircuitBreakerModule { 34 | 35 | /** Creates [[com.avast.sst.monix.catnap.CircuitBreakerModule]] specialed for `F[_]: Sync`. */ 36 | def apply[F[_]: Sync]: CircuitBreakerModule[F] = new CircuitBreakerModule[F] 37 | 38 | /** Wraps [[monix.catnap.CircuitBreaker]] and makes it log important events (e.g. onClose, onOpen). */ 39 | def withLogging[F[_]: Sync](name: String, circuitBreaker: CircuitBreaker[F]): CircuitBreaker[F] = { 40 | val F = Sync[F] 41 | 42 | lazy val logger = LoggerFactory.getLogger(s"${this.getClass}.$name") 43 | 44 | val loggingOnRejected = F.delay(logger.trace(s"Circuit breaker for $name rejected request.")) 45 | val loggingOnClosed = F.delay(logger.trace(s"Circuit breaker for $name closed.")) 46 | val loggingOnHalfOpen = F.delay(logger.trace(s"Circuit breaker for $name half-opened.")) 47 | val loggingOnOpen = F.delay(logger.trace(s"Circuit breaker for $name opened.")) 48 | 49 | circuitBreaker 50 | .doOnRejectedTask(loggingOnRejected) 51 | .doOnClosed(loggingOnClosed) 52 | .doOnHalfOpen(loggingOnHalfOpen) 53 | .doOnOpen(loggingOnOpen) 54 | } 55 | 56 | /** Wraps [[monix.catnap.CircuitBreaker]] and adds monitoring metrics (e.g. number of rejected tasks). */ 57 | def withMetrics[F[_]](circuitBreakerMetrics: CircuitBreakerMetrics[F], circuitBreaker: CircuitBreaker[F]): CircuitBreaker[F] = { 58 | circuitBreaker 59 | .doOnRejectedTask(circuitBreakerMetrics.increaseRejected) 60 | .doOnClosed(circuitBreakerMetrics.setState(Closed)) 61 | .doOnHalfOpen(circuitBreakerMetrics.setState(HalfOpen)) 62 | .doOnOpen(circuitBreakerMetrics.setState(Open)) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.11 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.2") 2 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") 3 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") 4 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") 5 | addSbtPlugin("com.47deg" % "sbt-microsites" % "1.3.4") 6 | addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") 7 | addSbtPlugin("com.thoughtworks.sbt-api-mappings" % "sbt-api-mappings" % "3.0.2") 8 | addSbtPlugin("ch.epfl.scala" % "sbt-version-policy" % "2.1.3") 9 | -------------------------------------------------------------------------------- /pureconfig/src/main/scala/com/avast/sst/pureconfig/PureConfigModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.pureconfig 2 | 3 | import cats.data.NonEmptyList 4 | import cats.effect.Sync 5 | import cats.syntax.either.* 6 | import pureconfig.error.{ConfigReaderFailure, ConfigReaderFailures, ConvertFailure} 7 | import pureconfig.{ConfigReader, ConfigSource} 8 | 9 | import scala.reflect.ClassTag 10 | 11 | /** Provides loading of configuration into case class via PureConfig. */ 12 | object PureConfigModule { 13 | 14 | /** Loads the case class `A` using Lightbend Config's standard behavior. */ 15 | def make[F[_]: Sync, A: ConfigReader]: F[Either[NonEmptyList[String], A]] = make(ConfigSource.default) 16 | 17 | /** Loads the case class `A` using provided [[pureconfig.ConfigSource]]. */ 18 | def make[F[_]: Sync, A: ConfigReader](source: ConfigSource): F[Either[NonEmptyList[String], A]] = { 19 | Sync[F].delay(source.load[A].leftMap(convertFailures)) 20 | } 21 | 22 | /** Loads the case class `A` using Lightbend Config's standard behavior or raises an exception. */ 23 | def makeOrRaise[F[_]: Sync, A: ConfigReader: ClassTag]: F[A] = makeOrRaise(ConfigSource.default) 24 | 25 | /** Loads the case class `A` using provided [[pureconfig.ConfigSource]] or raises an exception. */ 26 | def makeOrRaise[F[_]: Sync, A: ConfigReader: ClassTag](source: ConfigSource): F[A] = Sync[F].delay(source.loadOrThrow[A]) 27 | 28 | private def convertFailures(failures: ConfigReaderFailures): NonEmptyList[String] = { 29 | NonEmptyList(failures.head, failures.tail.toList).map(formatFailure) 30 | } 31 | 32 | private def formatFailure(configReaderFailure: ConfigReaderFailure): String = { 33 | configReaderFailure match { 34 | case convertFailure: ConvertFailure => 35 | s"Invalid configuration ${convertFailure.path} @ ${convertFailure.origin.map(_.description).iterator.mkString}: ${convertFailure.description}" 36 | case configFailure => 37 | s"Invalid configuration @ ${configFailure.origin.map(_.description).iterator.mkString}: ${configFailure.description}" 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /pureconfig/src/main/scala/com/avast/sst/pureconfig/WithConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.pureconfig 2 | 3 | import com.typesafe.config.Config 4 | import pureconfig.ConfigReader 5 | 6 | /** Used to retrieve both parsed configuration object and underlying [[config]] instance. */ 7 | final case class WithConfig[T](value: T, config: Config) 8 | 9 | object WithConfig { 10 | implicit def configReader[T: ConfigReader]: ConfigReader[WithConfig[T]] = 11 | for { 12 | config <- ConfigReader[Config] 13 | value <- ConfigReader[T] 14 | } yield WithConfig(value, config) 15 | 16 | } 17 | -------------------------------------------------------------------------------- /pureconfig/src/test/scala-2/com/avast/sst/pureconfig/PureConfigModuleTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.pureconfig 2 | 3 | import cats.data.NonEmptyList 4 | import cats.effect.SyncIO 5 | import org.scalatest.funsuite.AnyFunSuite 6 | import pureconfig.error.ConfigReaderException 7 | import pureconfig.generic.semiauto.* 8 | import pureconfig.{ConfigReader, ConfigSource} 9 | 10 | import scala.annotation.nowarn 11 | 12 | @nowarn("msg=unused value") 13 | class PureConfigModuleTest extends AnyFunSuite { 14 | 15 | private val source = ConfigSource.string("""|number = 123 16 | |string = "test"""".stripMargin) 17 | 18 | private val sourceWithMissingField = ConfigSource.string("number = 123") 19 | 20 | private val sourceWithTypeError = ConfigSource.string("""|number = wrong_type 21 | |string = "test"""".stripMargin) 22 | 23 | private case class TestConfig(number: Int, string: String) 24 | 25 | implicit private val configReader: ConfigReader[TestConfig] = deriveReader[TestConfig] 26 | 27 | test("Simple configuration loading") { 28 | assert(PureConfigModule.make[SyncIO, TestConfig](source).unsafeRunSync() === Right(TestConfig(123, "test"))) 29 | assert( 30 | PureConfigModule.make[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync() === Left( 31 | NonEmptyList( 32 | "Invalid configuration @ empty config: Key not found: 'number'.", 33 | List("Invalid configuration @ empty config: Key not found: 'string'.") 34 | ) 35 | ) 36 | ) 37 | assert( 38 | PureConfigModule.make[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync() === Left( 39 | NonEmptyList( 40 | "Invalid configuration @ String: 1: Key not found: 'string'.", 41 | List.empty 42 | ) 43 | ) 44 | ) 45 | assert( 46 | PureConfigModule.make[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync() === Left( 47 | NonEmptyList( 48 | "Invalid configuration number @ String: 1: Expected type NUMBER. Found STRING instead.", 49 | List.empty 50 | ) 51 | ) 52 | ) 53 | } 54 | 55 | test("Configuration loading with exceptions") { 56 | assert(PureConfigModule.makeOrRaise[SyncIO, TestConfig](source).unsafeRunSync() === TestConfig(123, "test")) 57 | assertThrows[ConfigReaderException[TestConfig]] { 58 | PureConfigModule.makeOrRaise[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync() 59 | } 60 | assertThrows[ConfigReaderException[TestConfig]] { 61 | PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync() 62 | } 63 | assertThrows[ConfigReaderException[TestConfig]] { 64 | PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync() 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /pureconfig/src/test/scala-3/com/avast/sst/pureconfig/PureConfigModuleTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.pureconfig 2 | 3 | import cats.data.NonEmptyList 4 | import cats.effect.SyncIO 5 | import org.scalatest.funsuite.AnyFunSuite 6 | import pureconfig.error.ConfigReaderException 7 | import pureconfig.{ConfigReader, ConfigSource} 8 | import pureconfig.generic.derivation.default.* 9 | 10 | import scala.annotation.nowarn 11 | 12 | @nowarn("msg=unused value") 13 | class PureConfigModuleTest extends AnyFunSuite { 14 | 15 | private val source = ConfigSource.string("""|number = 123 16 | |string = "test"""".stripMargin) 17 | 18 | private val sourceWithMissingField = ConfigSource.string("number = 123") 19 | 20 | private val sourceWithTypeError = ConfigSource.string("""|number = wrong_type 21 | |string = "test"""".stripMargin) 22 | 23 | private case class TestConfig(number: Int, string: String) 24 | 25 | implicit private val configReader: ConfigReader[TestConfig] = ConfigReader.derived 26 | 27 | test("Simple configuration loading") { 28 | assert(PureConfigModule.make[SyncIO, TestConfig](source).unsafeRunSync() === Right(TestConfig(123, "test"))) 29 | assert( 30 | PureConfigModule.make[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync() === Left( 31 | NonEmptyList( 32 | "Invalid configuration number @ : Key not found: 'number'.", 33 | List("Invalid configuration string @ : Key not found: 'string'.") 34 | ) 35 | ) 36 | ) 37 | assert( 38 | PureConfigModule.make[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync() === Left( 39 | NonEmptyList( 40 | "Invalid configuration string @ : Key not found: 'string'.", 41 | List.empty 42 | ) 43 | ) 44 | ) 45 | assert( 46 | PureConfigModule.make[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync() === Left( 47 | NonEmptyList( 48 | "Invalid configuration number @ String: 1: Expected type NUMBER. Found STRING instead.", 49 | List.empty 50 | ) 51 | ) 52 | ) 53 | } 54 | 55 | test("Configuration loading with exceptions") { 56 | assert(PureConfigModule.makeOrRaise[SyncIO, TestConfig](source).unsafeRunSync() === TestConfig(123, "test")) 57 | assertThrows[ConfigReaderException[TestConfig]] { 58 | PureConfigModule.makeOrRaise[SyncIO, TestConfig](ConfigSource.empty).unsafeRunSync() 59 | } 60 | assertThrows[ConfigReaderException[TestConfig]] { 61 | PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithMissingField).unsafeRunSync() 62 | } 63 | assertThrows[ConfigReaderException[TestConfig]] { 64 | PureConfigModule.makeOrRaise[SyncIO, TestConfig](sourceWithTypeError).unsafeRunSync() 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /sentry-pureconfig/src/main/scala-2/com/avast/sst/sentry/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.sentry.pureconfig 2 | 3 | import com.avast.sst.sentry.SentryConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.ProductHint 6 | import pureconfig.generic.semiauto._ 7 | 8 | trait ConfigReaders { 9 | 10 | implicit protected def hint[T]: ProductHint[T] = ProductHint.default 11 | 12 | implicit val sentryConfigReader: ConfigReader[SentryConfig] = deriveReader[SentryConfig] 13 | 14 | } 15 | -------------------------------------------------------------------------------- /sentry-pureconfig/src/main/scala-2/com/avast/sst/sentry/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.sentry.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | import pureconfig.generic.ProductHint 5 | 6 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 7 | object implicits extends ConfigReaders { 8 | 9 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 10 | * 11 | * This is alias for the default `implicits._` import. 12 | */ 13 | object KebabCase extends ConfigReaders 14 | 15 | /** Contains [[pureconfig.ConfigReader]] instances with "camelCase" naming convention. */ 16 | object CamelCase extends ConfigReaders { 17 | implicit override protected def hint[T]: ProductHint[T] = ProductHint(ConfigFieldMapping(pureconfig.CamelCase, pureconfig.CamelCase)) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /sentry-pureconfig/src/main/scala-3/com/avast/sst/sentry/pureconfig/ConfigReaders.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.sentry.pureconfig 2 | 3 | import com.avast.sst.sentry.SentryConfig 4 | import pureconfig.ConfigReader 5 | import pureconfig.generic.derivation.default.* 6 | 7 | trait ConfigReaders { 8 | 9 | implicit val sentryConfigReader: ConfigReader[SentryConfig] = ConfigReader.derived 10 | 11 | } 12 | -------------------------------------------------------------------------------- /sentry-pureconfig/src/main/scala-3/com/avast/sst/sentry/pureconfig/implicits.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.sentry.pureconfig 2 | 3 | import pureconfig.ConfigFieldMapping 4 | 5 | /** Contains [[pureconfig.ConfigReader]] instances with default "kebab-case" naming convention. */ 6 | object implicits extends ConfigReaders { 7 | 8 | /** Contains [[pureconfig.ConfigReader]] instances with "kebab-case" naming convention. 9 | * 10 | * This is alias for the default `implicits._` import. 11 | */ 12 | object KebabCase extends ConfigReaders 13 | 14 | } 15 | -------------------------------------------------------------------------------- /sentry/src/main/scala/com/avast/sst/sentry/SentryConfig.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.sentry 2 | 3 | final case class SentryConfig( 4 | dsn: String, 5 | release: Option[String] = None, 6 | environment: Option[String] = None, 7 | distribution: Option[String] = None, 8 | serverName: Option[String] = None, 9 | inAppInclude: List[String] = List.empty 10 | ) 11 | -------------------------------------------------------------------------------- /sentry/src/main/scala/com/avast/sst/sentry/SentryModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.sentry 2 | 3 | import cats.effect.{Resource, Sync} 4 | import io.sentry.{Sentry, SentryOptions} 5 | 6 | import scala.reflect.ClassTag 7 | 8 | object SentryModule { 9 | 10 | /** Makes [[io.sentry.SentryClient]] initialized with the given config. */ 11 | def make[F[_]: Sync](config: SentryConfig): Resource[F, Unit] = { 12 | Resource.make { 13 | Sync[F].delay { 14 | Sentry.init((options: SentryOptions) => { 15 | options.setDsn(config.dsn) 16 | config.release.foreach(options.setRelease) 17 | config.environment.foreach(options.setEnvironment) 18 | config.distribution.foreach(options.setDist) 19 | config.serverName.foreach(options.setServerName) 20 | config.inAppInclude.foreach(options.addInAppInclude) 21 | }) 22 | } 23 | }(_ => Sync[F].delay(Sentry.close())) 24 | } 25 | 26 | /** Makes [[io.sentry.SentryClient]] initialized with the given config and overrides the `release` property with `Implementation-Title` @`Implementation-Version` 27 | * from the `MANIFEST.MF` file inside the same JAR (package) as the `Main` class. 28 | * 29 | * This format is recommended by Sentry ([[https://docs.sentry.io/workflow/releases]]) because releases are global and must be 30 | * differentiated. 31 | */ 32 | def makeWithReleaseFromPackage[F[_]: Sync, Main: ClassTag](config: SentryConfig): Resource[F, Unit] = { 33 | for { 34 | customizedConfig <- Resource.eval { 35 | Sync[F].delay { 36 | for { 37 | pkg <- Option(implicitly[ClassTag[Main]].runtimeClass.getPackage) 38 | title <- Option(pkg.getImplementationTitle) 39 | version <- Option(pkg.getImplementationVersion) 40 | } yield config.copy(release = Some(s"$title@$version")) 41 | } 42 | } 43 | sentryClient <- make[F](customizedConfig.getOrElse(config)) 44 | } yield sentryClient 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /site/docs/bundles.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Bundles" 4 | position: 3 5 | --- 6 | 7 | # Bundles 8 | 9 | Having many small and independent subprojects is great but in practice everyone wants to use certain combination of dependencies and does not 10 | want to worry about many small dependencies. There are "bundles" for such use case - either the ones provided by this project or custom 11 | ones created by the user. 12 | 13 | One of the main decisions (dependency-wise) is to choose the effect data type. This project does not force you into specific data type and 14 | supports both [ZIO](https://zio.dev) and [Monix](https://monix.io) out-of-the-box. So there are two main bundles one for each effect data 15 | type that also bring in http4s server/client (Blaze), PureConfig and Micrometer. 16 | 17 | Unless you have specific needs take one of these bundles and write your server application using them - it will be the simplest way. 18 | -------------------------------------------------------------------------------- /site/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Getting Started" 4 | position: 0 5 | --- 6 | 7 | # Getting Started 8 | 9 | Creating a simple HTTP server using [http4s](https://http4s.org) and [ZIO](https://zio.dev) is as easy as this: 10 | 11 | ### build.sbt 12 | 13 | `libraryDependencies += "com.avast" %% "sst-bundle-zio-http4s-blaze" % "@VERSION@"` 14 | 15 | ### Main 16 | 17 | ```scala mdoc:silent:reset-class 18 | import cats.effect.* 19 | import com.avast.sst.http4s.client.* 20 | import com.avast.sst.http4s.server.* 21 | import com.avast.sst.jvm.execution.ExecutorModule 22 | import com.avast.sst.jvm.system.console.ConsoleModule 23 | import org.http4s.dsl.Http4sDsl 24 | import org.http4s.HttpRoutes 25 | import zio.interop.catz.* 26 | import zio.interop.catz.implicits.* 27 | import zio.{Task, ZEnv, Runtime} 28 | 29 | implicit val runtime: Runtime[ZEnv] = zio.Runtime.default // this is just needed in example 30 | 31 | val dsl = Http4sDsl[Task] // this is just needed in example 32 | import dsl.* 33 | 34 | val routes = Http4sRouting.make { 35 | HttpRoutes.of[Task] { 36 | case GET -> Root / "hello" => Ok("Hello World!") 37 | } 38 | } 39 | 40 | val resource = for { 41 | executorModule <- ExecutorModule.makeDefault[Task] 42 | console = ConsoleModule.make[Task] 43 | server <- Http4sBlazeServerModule.make[Task](Http4sBlazeServerConfig("127.0.0.1", 0), routes, executorModule.executionContext) 44 | client <- Http4sBlazeClientModule.make[Task](Http4sBlazeClientConfig(), executorModule.executionContext) 45 | } yield (server, client, console) 46 | 47 | val program = resource 48 | .use { 49 | case (server, client, console) => 50 | client 51 | .expect[String](s"http://127.0.0.1:${server.address.getPort}/hello") 52 | .flatMap(console.printLine) 53 | } 54 | ``` 55 | 56 | ```scala mdoc 57 | runtime.unsafeRun(program) 58 | ``` 59 | 60 | Or you can use the [official Giter8 template](https://github.com/avast/sst-seed.g8): 61 | 62 | ```bash 63 | sbt new avast/sst-seed.g8 64 | ``` 65 | -------------------------------------------------------------------------------- /site/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | title: "Home" 4 | --- 5 | 6 | This project is a culmination of years of Scala development at Avast and tries to represent the best practices of Scala server development 7 | we have gained together with tools that allow us to be effective. It is a set of small, flexible and cohesive building blocks that fit 8 | together well and allow you to build reliable server applications. 9 | 10 | # Jump Right In 11 | 12 | You can use the [official Giter8 template](https://github.com/avast/sst-seed.g8) to get started: 13 | 14 | ```bash 15 | sbt new avast/sst-seed.g8 16 | ``` 17 | 18 | Or **[read documentation](getting-started.md)** or [deep dive into example code](https://github.com/avast/scala-server-toolkit/tree/master/example). 19 | 20 | # SBT Dependency 21 | 22 | ```sbt 23 | libraryDependencies += "com.avast" %% "sst-bundle-zio-http4s-blaze" % "@VERSION@" 24 | ``` 25 | 26 | # Resources 27 | 28 | ## Articles 29 | 30 | * [Introducing Scala Server Toolkit](https://engineering.avast.io/introducing-scala-server-toolkit) (Avast Engineering) 31 | * [SST - Creating HTTP Server](https://engineering.avast.io/scala-server-toolkit-creating-http-server) (Avast Engineering) 32 | 33 | ## Talks 34 | * [Intro to Scala Server Toolkit](https://www.youtube.com/watch?v=T4xKu2bFUv0) (Functional JVM Meetup, Prague, January 23, 2020) 35 | * [slides](https://speakerdeck.com/jakubjanecek/intro-to-scala-server-toolkit) 36 | -------------------------------------------------------------------------------- /site/docs/structure.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Structure" 4 | position: 2 5 | --- 6 | 7 | # Structure 8 | 9 | The project is split into many small subprojects based on dependencies. For example code related to loading of configuration files via 10 | [PureConfig](https://pureconfig.github.io) lives in subproject named `sst-pureconfig` and code related to http4s server implemented using 11 | [Blaze](https://github.com/http4s/blaze) lives in subproject named `sst-http4s-server-blaze`. 12 | 13 | There are also subprojects that implement interoperability between usually two dependencies. For example we want to configure our HTTP server 14 | using PureConfig so definition of `implicit` `ConfigReader` instances lives in subproject named `sst-http4s-server-blaze-pureconfig`. Or to give 15 | another example, monitoring of HTTP server using [Micrometer](https://micrometer.io) lives in subproject named `sst-http4s-server-micrometer`. 16 | Note that such subproject depends on APIs of both http4s server and Micrometer but it does not depend on concrete implementation which allows 17 | you to choose any http4s implementation (Blaze, ...) and any Micrometer implementation (JMX, StatsD, ...). 18 | -------------------------------------------------------------------------------- /site/docs/subprojects.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Subprojects" 4 | position: 4 5 | --- 6 | 7 | # Subprojects 8 | 9 | Scala Server Toolkit provides many subprojects for different well-known libraries. 10 | -------------------------------------------------------------------------------- /site/docs/subprojects/cassandra-datastax-driver.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Datastax Cassandra Driver" 4 | --- 5 | 6 | # Datastax Cassandra Driver 7 | 8 | This subproject initializes Datastax Cassandra driver's `Session`. 9 | 10 | `libraryDependencies += "com.avast" %% "sst-cassandra-datastax-driver" % "@VERSION@"` 11 | 12 | ```scala mdoc:silent 13 | import cats.effect.Resource 14 | import com.avast.sst.datastax.CassandraDatastaxDriverModule 15 | import com.avast.sst.datastax.config.CassandraDatastaxDriverConfig 16 | import com.avast.sst.datastax.pureconfig.implicits.* 17 | import com.avast.sst.pureconfig.PureConfigModule 18 | import zio.* 19 | import zio.interop.catz.* 20 | 21 | implicit val runtime: Runtime[ZEnv] = zio.Runtime.default // this is just needed in example 22 | 23 | for { 24 | configuration <- Resource.eval(PureConfigModule.makeOrRaise[Task, CassandraDatastaxDriverConfig]) 25 | db <- CassandraDatastaxDriverModule.make[Task](configuration) 26 | } yield db 27 | ``` 28 | 29 | ```HOCON 30 | basic { 31 | contact-points = ["localhost:9042"] 32 | 33 | load-balancing-policy { 34 | local-datacenter = "datacenter1" 35 | } 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /site/docs/subprojects/doobie.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Doobie" 4 | --- 5 | 6 | # Doobie 7 | 8 | `libraryDependencies += "com.avast" %% "sst-doobie-hikari" % "@VERSION@"` 9 | 10 | This subproject initializes a doobie `Transactor`: 11 | 12 | ```scala mdoc:silent 13 | import cats.effect.Resource 14 | import com.avast.sst.doobie.DoobieHikariModule 15 | import com.avast.sst.example.config.Configuration 16 | import com.avast.sst.jvm.execution.ConfigurableThreadFactory.Config 17 | import com.avast.sst.jvm.execution.{ConfigurableThreadFactory, ExecutorModule} 18 | import com.avast.sst.micrometer.jmx.MicrometerJmxModule 19 | import com.avast.sst.pureconfig.PureConfigModule 20 | import com.zaxxer.hikari.metrics.micrometer.MicrometerMetricsTrackerFactory 21 | import scala.concurrent.ExecutionContext 22 | import zio.* 23 | import zio.interop.catz.* 24 | 25 | implicit val runtime: Runtime[ZEnv] = zio.Runtime.default // this is just needed in example 26 | 27 | for { 28 | configuration <- Resource.eval(PureConfigModule.makeOrRaise[Task, Configuration]) 29 | executorModule <- ExecutorModule.makeFromExecutionContext[Task](runtime.platform.executor.asEC) 30 | meterRegistry <- MicrometerJmxModule.make[Task](configuration.jmx) 31 | boundedConnectExecutionContext <- executorModule 32 | .makeThreadPoolExecutor( 33 | configuration.boundedConnectExecutor, 34 | new ConfigurableThreadFactory(Config(Some("hikari-connect-%02d"))) 35 | ) 36 | .map(ExecutionContext.fromExecutorService) 37 | hikariMetricsFactory = new MicrometerMetricsTrackerFactory(meterRegistry) 38 | doobieTransactor <- DoobieHikariModule 39 | .make[Task](configuration.database, boundedConnectExecutionContext, executorModule.blocker, Some(hikariMetricsFactory)) 40 | } yield doobieTransactor 41 | ``` 42 | -------------------------------------------------------------------------------- /site/docs/subprojects/flyway.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Flyway" 4 | --- 5 | 6 | # Flyway 7 | 8 | `libraryDependencies += "com.avast" %% "sst-flyway" % "@VERSION@"` 9 | 10 | This subproject initializes `Flyway` which can be used to do automated SQL DB migrations. See the [documentation of Flyway](https://flywaydb.org/documentation/) 11 | on how to go about that. 12 | 13 | The method `make` requires `javax.sql.DataSource` which you can for example obtain from `doobie-hikari` subproject: 14 | 15 | ```scala mdoc:compile-only 16 | import cats.effect.Resource 17 | import com.avast.sst.doobie.DoobieHikariModule 18 | import com.avast.sst.flyway.FlywayModule 19 | import zio.Task 20 | import zio.interop.catz.* 21 | 22 | for { 23 | doobieTransactor <- DoobieHikariModule.make[Task](???, ???, ???, ???) 24 | flyway <- Resource.eval(FlywayModule.make[Task](doobieTransactor.kernel, ???)) 25 | _ <- Resource.eval(Task.effect(flyway.migrate())) 26 | } yield () 27 | ``` 28 | -------------------------------------------------------------------------------- /site/docs/subprojects/fs2-kafka.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "FS2 Kafka" 4 | --- 5 | 6 | # FS2 Kafka 7 | 8 | `libraryDependencies += "com.avast" %% "sst-fs2-kafka" % "@VERSION@"` 9 | 10 | This subproject initializes [FS2 Kafka](https://github.com/fd4s/fs2-kafka) consumer or producer: 11 | 12 | ```scala mdoc:silent 13 | import cats.effect.Resource 14 | import cats.syntax.flatMap.* 15 | import com.avast.sst.fs2kafka.* 16 | import fs2.kafka.{AutoOffsetReset, ProducerRecord, ProducerRecords} 17 | import zio.* 18 | import zio.interop.catz.* 19 | import zio.interop.catz.implicits.* 20 | 21 | implicit val runtime: Runtime[ZEnv] = zio.Runtime.default // this is just needed in example 22 | 23 | for { 24 | consumer <- Fs2KafkaModule.makeConsumer[Task, String, String]( 25 | ConsumerConfig(List("localhost:9092"), groupId = "test", autoOffsetReset = AutoOffsetReset.Earliest), None, None 26 | ) 27 | _ <- Resource.eval(consumer.subscribeTo("test")) 28 | consumerStream <- Resource.eval(consumer.stream) 29 | } yield consumerStream 30 | ``` 31 | 32 | The configuration of Kafka client is very large therefore you can either use the provided configuration case class, or you can use the underlying 33 | `ConsumerSettings`/`ProducerSettings` builders directly. 34 | 35 | The configuration case classes contain an "escape hatch" into the full world of Kafka client configuration options via untyped properties. 36 | This is there to be flexible in case it is needed. Documentation of all the configuration properties is available here: 37 | * [consumer](http://kafka.apache.org/documentation/#consumerconfigs) 38 | * [producer](http://kafka.apache.org/documentation/#producerconfigs) 39 | 40 | Beware that there is an optional dependency on `jackson-databind` for the default implementation of `SASL/OAUTHBEARER` in `kafka-clients`. 41 | You need to provide it explicitly: https://kafka.apache.org/documentation/#security_sasl_oauthbearer_clientconfig 42 | -------------------------------------------------------------------------------- /site/docs/subprojects/jvm.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "JVM" 4 | --- 5 | 6 | # JVM 7 | 8 | `libraryDependencies += "com.avast" %% "sst-jvm" % "@VERSION@"` 9 | 10 | Subproject `sst-jvm` provides pure implementations of different JVM-related utilities: 11 | 12 | * creation of thread pools, 13 | * standard in/out/err, 14 | * and random number generation. 15 | 16 | ```scala mdoc:silent 17 | import com.avast.sst.jvm.system.console.ConsoleModule 18 | import com.avast.sst.jvm.system.random.RandomModule 19 | import zio.interop.catz.* 20 | import zio.Task 21 | 22 | val program = for { 23 | random <- RandomModule.makeRandom[Task](1234L) // do not ever use seed like this! 24 | randomNumber <- random.nextInt 25 | console = ConsoleModule.make[Task] 26 | _ <- console.printLine(s"Random number: $randomNumber") 27 | } yield () 28 | 29 | val runtime = zio.Runtime.default // this is just needed in example 30 | ``` 31 | 32 | ```scala mdoc 33 | runtime.unsafeRun(program) 34 | ``` 35 | -------------------------------------------------------------------------------- /site/docs/subprojects/lettuce.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Lettuce (Redis)" 4 | --- 5 | 6 | # Lettuce (Redis) 7 | 8 | `libraryDependencies += "com.avast" %% "sst-lettuce" % "@VERSION@"` 9 | 10 | This subproject initializes [Lettuce](https://lettuce.io) Redis driver: 11 | 12 | ```scala mdoc:silent 13 | import cats.effect.Resource 14 | import com.avast.sst.lettuce.{LettuceConfig, LettuceModule} 15 | import io.lettuce.core.codec.{RedisCodec, StringCodec} 16 | import zio.* 17 | import zio.interop.catz.* 18 | 19 | implicit val runtime: Runtime[ZEnv] = zio.Runtime.default // this is just needed in example 20 | 21 | implicit val lettuceCodec: RedisCodec[String, String] = StringCodec.UTF8 22 | 23 | for { 24 | connection <- LettuceModule.makeConnection[Task, String, String](LettuceConfig("redis://localhost")) 25 | value <- Resource.eval(Task.effect(connection.sync().get("key"))) 26 | } yield value 27 | ``` 28 | -------------------------------------------------------------------------------- /site/docs/subprojects/micrometer.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Micrometer" 4 | --- 5 | 6 | # Micrometer 7 | 8 | `libraryDependencies += "com.avast" %% "sst-micrometer-jmx" % "@VERSION@"` 9 | 10 | This subproject allows you to monitor your applications using [Micrometer](https://micrometer.io). There are many actual implementations of 11 | the Micrometer API one of which is JMX. Subproject `sst-micrometer-jmx` implements the initialization of Micrometer for JMX. There are also 12 | interop subprojects such as `sst-http4s-server-micrometer` which implement monitoring of HTTP server and individual routes using Micrometer. 13 | 14 | ```scala mdoc:silent 15 | import cats.effect.{Clock, Resource} 16 | import com.avast.sst.http4s.server.* 17 | import com.avast.sst.http4s.server.micrometer.MicrometerHttp4sServerMetricsModule 18 | import com.avast.sst.jvm.execution.ExecutorModule 19 | import com.avast.sst.jvm.micrometer.MicrometerJvmModule 20 | import com.avast.sst.micrometer.jmx.* 21 | import org.http4s.dsl.Http4sDsl 22 | import org.http4s.HttpRoutes 23 | import org.http4s.server.Server 24 | import zio.interop.catz.* 25 | import zio.interop.catz.implicits.* 26 | import zio.* 27 | 28 | implicit val runtime: Runtime[ZEnv] = zio.Runtime.default // this is just needed in example 29 | 30 | val dsl = Http4sDsl[Task] // this is just needed in example 31 | import dsl.* 32 | 33 | for { 34 | executorModule <- ExecutorModule.makeFromExecutionContext[Task](runtime.platform.executor.asEC) 35 | clock = Clock.create[Task] 36 | jmxMeterRegistry <- MicrometerJmxModule.make[Task](MicrometerJmxConfig("com.avast")) 37 | _ <- Resource.eval(MicrometerJvmModule.make[Task](jmxMeterRegistry)) 38 | serverMetricsModule <- Resource.eval(MicrometerHttp4sServerMetricsModule.make[Task](jmxMeterRegistry, executorModule.blocker, clock)) 39 | routes = Http4sRouting.make { 40 | serverMetricsModule.serverMetrics { 41 | HttpRoutes.of[Task] { 42 | case GET -> Root / "hello" => Ok("Hello World!") 43 | } 44 | } 45 | } 46 | server <- Http4sBlazeServerModule.make[Task](Http4sBlazeServerConfig("127.0.0.1", 0), routes, executorModule.executionContext) 47 | } yield server 48 | ``` 49 | -------------------------------------------------------------------------------- /site/docs/subprojects/monix-catnap.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "monix-catnap" 4 | --- 5 | 6 | # monix-catnap 7 | 8 | `libraryDependencies += "com.avast" %% "sst-monix-catnap" % "@VERSION@"` 9 | 10 | This subproject provides interop between Scala Server Toolkit and [monix-catnap](https://monix.io/docs/3x/#monix-catnap) library. 11 | 12 | ## Circuit Breaker 13 | 14 | You can use `CircuitBreakerModule` to instantiate and configure a circuit breaker and you can implement `CircuitBreakerMetrics` to get 15 | monitoring of the circuit breaker. There is an implementation for Micrometer in `sst-monix-catnap-micrometer` subproject. 16 | 17 | All of this is tied with http4s HTTP client in the `sst-http4s-client-monix-catnap` subproject so in practice you want to use 18 | `Http4sClientCircuitBreakerModule` which wraps any `Client[F]` with a `CircuitBreaker` (it is recommended to have an enriched `CircuitBreaker` 19 | with logging and metrics - see `CircuitBreakerModule` companion object methods). However the most important feature of the enriched circuit 20 | breaker is that any HTTP failure (according to `HttpStatusClassifier`) is converted to an exception internally which triggers the circuit 21 | breaking mechanism. Failing server is not overloaded by more requests and we do not have to wait for the response if the server is failing 22 | anyway. 23 | -------------------------------------------------------------------------------- /site/docs/subprojects/sentry.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Sentry" 4 | --- 5 | 6 | # Sentry 7 | 8 | `libraryDependencies += "com.avast" %% "sst-sentry" % "@VERSION@"` 9 | 10 | This subproject allows you to initialize `SentryClient` from a configuration case class. There are two `make*` methods. The one called `make` 11 | does everything according to the configuration. The one called `makeWithReleaseFromPackage` adds a bit of clever behavior because it reads 12 | the `Implementation-Version` property from the `MANIFEST.MF` file from the JAR (package) of the respective `Main` class and uses it to override 13 | the `release` property of Sentry. This allows you to automatically propage the version of your application to Sentry. 14 | 15 | Initialization of the `SentryClient` is side-effectful so it is wrapped in `Resource[F, SentryClient]` and `F` is `Sync`. 16 | 17 | ```scala mdoc:silent 18 | import com.avast.sst.sentry.* 19 | import zio.interop.catz.* 20 | import zio.Task 21 | 22 | val sentry = SentryModule.make[Task](SentryConfig("")) 23 | ``` 24 | -------------------------------------------------------------------------------- /site/docs/subprojects/ssl-config.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "SSL Config" 4 | --- 5 | 6 | # SSL Config 7 | 8 | `libraryDependencies += "com.avast" %% "sst-ssl-config" % "@VERSION@"` 9 | 10 | This subproject allows you to create SSL context (`javax.net.ssl.SSLContext`) from a configuration file. It uses [SSL Config](https://github.com/lightbend/ssl-config) 11 | library to do so which means that this module is different than others and receives directly `com.typesafe.config.Config` instead of 12 | configuration case classes. See the [documentation of SSL Config](https://lightbend.github.io/ssl-config) for more information. 13 | 14 | Loading of SSL context is side-effectful so it is wrapped in `F` which is `Sync`. 15 | 16 | ```scala mdoc:silent 17 | import com.avast.sst.ssl.SslContextModule 18 | import com.typesafe.config.ConfigFactory 19 | import zio.interop.catz.* 20 | import zio.Task 21 | 22 | val config = ConfigFactory.load().getConfig("ssl-config") 23 | val sslContext = SslContextModule.make[Task](config) 24 | ``` 25 | -------------------------------------------------------------------------------- /site/menu.yml: -------------------------------------------------------------------------------- 1 | options: 2 | - title: Getting Started 3 | url: getting-started 4 | - title: Rationale 5 | url: rationale 6 | - title: Structure 7 | url: structure 8 | - title: Bundles 9 | url: bundles 10 | - title: Subprojects 11 | url: subprojects 12 | nested_options: 13 | - title: http4s 14 | url: subprojects/http4s 15 | - title: JVM 16 | url: subprojects/jvm 17 | - title: Micrometer 18 | url: subprojects/micrometer 19 | - title: PureConfig 20 | url: subprojects/pureconfig 21 | - title: Datastax Cassandra Driver 22 | url: subprojects/cassandra-datastax-driver 23 | - title: FS2 Kafka 24 | url: subprojects/fs2-kafka 25 | - title: SSL Config 26 | url: subprojects/ssl-config 27 | - title: doobie 28 | url: subprojects/doobie 29 | - title: Flyway 30 | url: subprojects/flyway 31 | - title: monix-catnap - Circuit Breaker 32 | url: subprojects/monix-catnap 33 | - title: Sentry 34 | url: subprojects/sentry 35 | - title: Lettuce (Redis) 36 | url: subprojects/lettuce 37 | -------------------------------------------------------------------------------- /ssl-config/src/main/scala/com/avast/sst/ssl/Slf4jLogger.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.ssl 2 | 3 | import com.typesafe.sslconfig.util.{LoggerFactory, NoDepsLogger} 4 | import org.slf4j.{Logger, LoggerFactory as Slf4jLoggerFactory} 5 | 6 | private class Slf4jLogger(l: Logger) extends NoDepsLogger { 7 | 8 | override def isDebugEnabled: Boolean = l.isDebugEnabled 9 | 10 | override def debug(msg: String): Unit = l.debug(msg) 11 | 12 | override def info(msg: String): Unit = l.info(msg) 13 | 14 | override def warn(msg: String): Unit = l.warn(msg) 15 | 16 | override def error(msg: String): Unit = l.error(msg) 17 | 18 | override def error(msg: String, throwable: Throwable): Unit = l.error(msg, throwable) 19 | 20 | } 21 | 22 | private[ssl] object Slf4jLogger { 23 | 24 | def factory: LoggerFactory = 25 | new LoggerFactory { 26 | override def apply(clazz: Class[?]): NoDepsLogger = new Slf4jLogger(Slf4jLoggerFactory.getLogger(clazz)) 27 | override def apply(name: String): NoDepsLogger = new Slf4jLogger(Slf4jLoggerFactory.getLogger(name)) 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ssl-config/src/main/scala/com/avast/sst/ssl/SslContextModule.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.ssl 2 | 3 | import cats.effect.Sync 4 | import cats.syntax.functor.* 5 | import com.typesafe.config.{Config, ConfigFactory} 6 | import com.typesafe.sslconfig.ssl.{ 7 | ConfigSSLContextBuilder, 8 | DefaultKeyManagerFactoryWrapper, 9 | DefaultTrustManagerFactoryWrapper, 10 | SSLConfigFactory 11 | } 12 | 13 | import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory} 14 | 15 | object SslContextModule { 16 | 17 | private val SslContextEnabledKey = "enabled" 18 | 19 | /** Initializes [[javax.net.ssl.SSLContext]] from the provided config. 20 | * 21 | * @param withReference 22 | * Whether we should use reference config of "ssl-config" library as well. 23 | */ 24 | def make[F[_]: Sync](config: Config, withReference: Boolean = true): F[SSLContext] = 25 | Sync[F].delay { 26 | val loggerFactory = Slf4jLogger.factory 27 | val finalConfig = if (withReference) config.withFallback(referenceConfigUnsafe()) else config 28 | new ConfigSSLContextBuilder( 29 | loggerFactory, 30 | SSLConfigFactory.parse(finalConfig, loggerFactory), 31 | new DefaultKeyManagerFactoryWrapper(KeyManagerFactory.getDefaultAlgorithm), 32 | new DefaultTrustManagerFactoryWrapper(TrustManagerFactory.getDefaultAlgorithm) 33 | ).build() 34 | } 35 | 36 | /** Initializes [[javax.net.ssl.SSLContext]] from the provided config if it is enabled. 37 | * 38 | * Expects a boolean value `enabled` at the root of the provided [[com.typesafe.config.Config]] which determines whether to initialize 39 | * the context or not. 40 | * 41 | * @param withReference 42 | * Whether we should use reference config of "ssl-config" library as well. 43 | */ 44 | def makeIfEnabled[F[_]: Sync](config: Config, withReference: Boolean = true): F[Option[SSLContext]] = { 45 | if (config.hasPath(SslContextEnabledKey) && config.getBoolean(SslContextEnabledKey)) { 46 | make(config, withReference).map(Some(_)) 47 | } else { 48 | Sync[F].delay(None) 49 | } 50 | 51 | } 52 | 53 | private def referenceConfigUnsafe(): Config = ConfigFactory.defaultReference().getConfig("ssl-config") 54 | 55 | } 56 | -------------------------------------------------------------------------------- /ssl-config/src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | ssl-config { 2 | trustManager { 3 | stores = [ 4 | { 5 | type = "JKS" 6 | path = "src/test/resources/truststore.jks" 7 | password = "CanNotMakeJKSWithoutPass" 8 | } 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ssl-config/src/test/resources/truststore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avast/scala-server-toolkit/ec9980f02317f3e27a474d9be34b933dba943d7a/ssl-config/src/test/resources/truststore.jks -------------------------------------------------------------------------------- /ssl-config/src/test/scala/com/avast/sst/ssl/SslContextModuleTest.scala: -------------------------------------------------------------------------------- 1 | package com.avast.sst.ssl 2 | 3 | import cats.effect.SyncIO 4 | import com.typesafe.config.ConfigFactory 5 | import org.scalatest.funsuite.AnyFunSuite 6 | 7 | class SslContextModuleTest extends AnyFunSuite { 8 | 9 | test("SslContextModule initializes properly from JKS store with reference config") { 10 | val sslContext = SslContextModule.make[SyncIO](ConfigFactory.empty()).unsafeRunSync() 11 | assert(sslContext.getProtocol === "TLSv1.2") 12 | } 13 | 14 | test("SslContextModule initializes properly from JKS store with provided config") { 15 | val sslContext = SslContextModule.make[SyncIO](ConfigFactory.load().getConfig("ssl-config"), withReference = false).unsafeRunSync() 16 | assert(sslContext.getProtocol === "TLSv1.2") 17 | } 18 | 19 | test("SslContextModule fails to initialize for empty config and no reference config") { 20 | val result = SslContextModule.make[SyncIO](ConfigFactory.empty(), withReference = false).attempt.unsafeRunSync() 21 | assert(result.isLeft) 22 | } 23 | 24 | } 25 | --------------------------------------------------------------------------------