├── .codebeatignore ├── .codeclimate.yml ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .gitignore ├── .idea ├── .gitignore ├── AugmentWebviewStateStore.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── copyright │ ├── Apache.xml │ └── profiles_settings.xml ├── detekt.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── ktlint.xml ├── misc.xml ├── php.xml ├── uiDesigner.xml └── vcs.xml ├── .run ├── Agent (no auth).run.xml ├── Proxy (auth).run.xml └── Proxy (no auth).run.xml ├── .travis.yml ├── License.txt ├── Makefile ├── README.md ├── bin ├── docker-agent.sh └── docker-proxy.sh ├── build.gradle.kts ├── config └── detekt │ └── detekt.yml ├── docs ├── cli-args.md ├── prometheus-proxy.png └── release.md ├── etc ├── compose │ └── proxy.yml ├── config │ └── config.conf ├── docker │ ├── agent.df │ └── proxy.df ├── jars │ └── tscfg-1.2.4.jar └── test-configs │ ├── junit-test.conf │ └── travis.conf ├── examples ├── federate.conf ├── myapps.conf ├── simple.conf ├── tls-no-mutual-auth.conf └── tls-with-mutual-auth.conf ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── grafana ├── prometheus-agents.json └── prometheus-proxy.json ├── jitpack.yml ├── logback └── docker-logback.xml ├── nginx ├── docker │ ├── Dockerfile │ ├── nginx.conf │ └── run.sh └── nginx-proxy.conf ├── prom-agent.conf ├── settings.gradle ├── src ├── main │ ├── java │ │ └── io │ │ │ └── prometheus │ │ │ └── common │ │ │ ├── ConfigVals.java │ │ │ └── README.txt │ ├── kotlin │ │ └── io │ │ │ └── prometheus │ │ │ ├── Agent.kt │ │ │ ├── Proxy.kt │ │ │ ├── agent │ │ │ ├── AgentClientInterceptor.kt │ │ │ ├── AgentConnectionContext.kt │ │ │ ├── AgentGrpcService.kt │ │ │ ├── AgentHttpService.kt │ │ │ ├── AgentMetrics.kt │ │ │ ├── AgentOptions.kt │ │ │ ├── AgentPathManager.kt │ │ │ ├── EmbeddedAgentInfo.kt │ │ │ ├── RequestFailureException.kt │ │ │ ├── SslSettings.kt │ │ │ └── TrustAllX509TrustManager.kt │ │ │ ├── common │ │ │ ├── BaseOptions.kt │ │ │ ├── ConfigWrappers.kt │ │ │ ├── Constants.kt │ │ │ ├── EnvVars.kt │ │ │ ├── GrpcObjects.kt │ │ │ ├── ScrapeResults.kt │ │ │ ├── TypeAliases.kt │ │ │ └── Utils.kt │ │ │ └── proxy │ │ │ ├── AgentContext.kt │ │ │ ├── AgentContextCleanupService.kt │ │ │ ├── AgentContextManager.kt │ │ │ ├── ChunkedContext.kt │ │ │ ├── ProxyConstants.kt │ │ │ ├── ProxyGrpcService.kt │ │ │ ├── ProxyHttpConfig.kt │ │ │ ├── ProxyHttpRoutes.kt │ │ │ ├── ProxyHttpService.kt │ │ │ ├── ProxyMetrics.kt │ │ │ ├── ProxyOptions.kt │ │ │ ├── ProxyPathManager.kt │ │ │ ├── ProxyServerInterceptor.kt │ │ │ ├── ProxyServerTransportFilter.kt │ │ │ ├── ProxyServiceImpl.kt │ │ │ ├── ProxyUtils.kt │ │ │ ├── ScrapeRequestManager.kt │ │ │ └── ScrapeRequestWrapper.kt │ ├── proto │ │ └── proxy_service.proto │ └── resources │ │ ├── banners │ │ ├── README.txt │ │ ├── agent.txt │ │ └── proxy.txt │ │ ├── logback.xml │ │ └── reference.conf └── test │ ├── kotlin │ └── io │ │ └── prometheus │ │ ├── AdminDefaultPathTest.kt │ │ ├── AdminEmptyPathTest.kt │ │ ├── AdminNonDefaultPathTest.kt │ │ ├── CommonCompanion.kt │ │ ├── CommonTests.kt │ │ ├── DataClassTest.kt │ │ ├── InProcessTestNoAdminMetricsTest.kt │ │ ├── InProcessTestWithAdminMetricsTest.kt │ │ ├── NettyTestNoAdminMetricsTest.kt │ │ ├── NettyTestWithAdminMetricsTest.kt │ │ ├── OptionsTest.kt │ │ ├── ProxyTests.kt │ │ ├── SimpleTests.kt │ │ ├── TestConstants.kt │ │ ├── TestUtils.kt │ │ ├── TlsNoMutualAuthTest.kt │ │ └── TlsWithMutualAuthTest.kt │ └── resources │ └── logback-test.xml └── testing └── certs ├── ca.pem ├── client.key ├── client.pem ├── server1.key └── server1.pem /.codebeatignore: -------------------------------------------------------------------------------- 1 | src/main/java/io/prometheus/common/ConfigVals.java -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | checkstyle: 3 | enabled: true 4 | channel: "beta" 5 | 6 | ratings: 7 | paths: 8 | - "**.java" 9 | 10 | exclude_patterns: 11 | - "src/main/java/io/prometheus/common/ConfigVals.java" 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .git/ 3 | .wercker/ 4 | bin/ 5 | out/ 6 | target/ 7 | etc/docker 8 | etc/compose 9 | *.yml 10 | *.iml 11 | *.md 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | insert_final_newline = true 8 | max_line_length = 120 9 | trim_trailing_whitespace = true 10 | 11 | [build.gradle] 12 | indent_size = 4 13 | 14 | # Override for Makefile 15 | [{Makefile, makefile}] 16 | indent_style = tab 17 | indent_size = 4 18 | 19 | [*.{kt,kts}] 20 | indent_size = 2 21 | ktlint_standard_no-wildcard-imports = disabled 22 | ktlint_standard_multiline-if-else = disabled 23 | ktlint_standard_string-template-indent = disabled 24 | ktlint_standard_indent = disabled 25 | ktlint_standard_multiline-expression-wrapping = disabled 26 | ktlint_standard_chain-method-continuation = disabled -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf binary 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf binary 9 | 10 | # Binary files should be left untouched 11 | *.jar binary 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Kotlin ### 20 | .kotlin 21 | 22 | ### Eclipse ### 23 | .apt_generated 24 | .classpath 25 | .factorypath 26 | .project 27 | .settings 28 | .springBeans 29 | .sts4-cache 30 | bin/ 31 | !**/src/main/**/bin/ 32 | !**/src/test/**/bin/ 33 | 34 | ### NetBeans ### 35 | /nbproject/private/ 36 | /nbbuild/ 37 | /dist/ 38 | /nbdist/ 39 | /.nb-gradle/ 40 | 41 | ### VS Code ### 42 | .vscode/ 43 | 44 | ### Mac OS ### 45 | .DS_Store 46 | 47 | ### Node.js ### 48 | node_modules 49 | dist 50 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | # GitHub Copilot persisted chat sessions 10 | /copilot/chatSessions 11 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 14 | 15 | 16 | 20 | 21 | 22 | 31 | 32 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/copyright/Apache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/detekt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 12 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/ktlint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | false 5 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /.idea/php.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.run/Agent (no auth).run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | -------------------------------------------------------------------------------- /.run/Proxy (auth).run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | -------------------------------------------------------------------------------- /.run/Proxy (no auth).run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | #- openjdk8 5 | #- oraclejdk11 6 | - openjdk11 7 | 8 | before_script: 9 | - chmod +x gradlew 10 | 11 | #script: 12 | # - ./gradlew check jacocoTestReport 13 | 14 | #after_success: 15 | # - bash <(curl -s https://codecov.io/bash) 16 | # - ./gradlew jacocoTestReport coveralls 17 | 18 | notifications: 19 | email: 20 | - pambrose@mac.com -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=2.1.0 2 | 3 | default: versioncheck 4 | 5 | stop: 6 | ./gradlew --stop 7 | 8 | clean: 9 | ./gradlew clean 10 | 11 | stubs: 12 | ./gradlew generateProto 13 | 14 | build: clean stubs 15 | ./gradlew build -xtest 16 | 17 | jars: 18 | ./gradlew agentJar proxyJar 19 | 20 | tests: 21 | ./gradlew --rerun-tasks check 22 | 23 | reports: 24 | ./gradlew koverMergedHtmlReport 25 | 26 | tsconfig: 27 | java -jar ./etc/jars/tscfg-1.2.4.jar --spec etc/config/config.conf --pn io.prometheus.common --cn ConfigVals --dd src/main/java/io/prometheus/common 28 | 29 | distro: build jars 30 | 31 | #PLATFORMS := linux/amd64,linux/arm64/v8,linux/s390x,linux/ppc64le 32 | PLATFORMS := linux/amd64,linux/arm64/v8,linux/s390x 33 | IMAGE_PREFIX := pambrose/prometheus 34 | 35 | docker-push: 36 | # prepare multiarch 37 | docker buildx use buildx 2>/dev/null || docker buildx create --use --name=buildx 38 | docker buildx build --platform ${PLATFORMS} -f ./etc/docker/proxy.df --push -t ${IMAGE_PREFIX}-proxy:latest -t ${IMAGE_PREFIX}-proxy:${VERSION} . 39 | docker buildx build --platform ${PLATFORMS} -f ./etc/docker/agent.df --push -t ${IMAGE_PREFIX}-agent:latest -t ${IMAGE_PREFIX}-agent:${VERSION} . 40 | 41 | all: distro docker-push 42 | 43 | build-coverage: 44 | ./mvnw clean org.jacoco:jacoco-maven-plugin:prepare-agent package jacoco:report 45 | 46 | report-coverage: 47 | ./mvnw -DrepoToken=${COVERALLS_TOKEN} clean package test jacoco:report coveralls:report 48 | 49 | sonar: 50 | ./mvnw sonar:sonar -Dsonar.host.url=http://localhost:9000 51 | 52 | site: 53 | ./mvnw site 54 | 55 | tree: 56 | ./gradlew -q dependencies 57 | 58 | depends: 59 | ./gradlew dependencies 60 | 61 | lint: 62 | ./gradlew lintKotlinMain 63 | ./gradlew lintKotlinTest 64 | 65 | versioncheck: 66 | ./gradlew dependencyUpdates 67 | 68 | refresh: 69 | ./gradlew --refresh-dependencies 70 | 71 | upgrade-wrapper: 72 | ./gradlew wrapper --gradle-version=8.13 --distribution-type=bin 73 | -------------------------------------------------------------------------------- /bin/docker-agent.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker run --rm -p 8083:8083 -p 8093:8093 \ 4 | --env AGENT_CONFIG='https://raw.githubusercontent.com/pambrose/prometheus-proxy/master/examples/simple.conf' \ 5 | --env PROXY_HOSTNAME=mymachine.lan \ 6 | pambrose/prometheus-agent:2.1.0 7 | -------------------------------------------------------------------------------- /bin/docker-proxy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker run --rm -p 8082:8082 -p 8092:8092 -p 50051:50051 -p 8080:8080 \ 4 | --env PROXY_CONFIG='https://raw.githubusercontent.com/pambrose/prometheus-proxy/master/examples/simple.conf' \ 5 | pambrose/prometheus-proxy:2.1.0 6 | -------------------------------------------------------------------------------- /docs/cli-args.md: -------------------------------------------------------------------------------- 1 | # CLI Args for running Agent and Proxy 2 | 3 | ## Agent CLI Args 4 | ```bash 5 | --conf https://raw.githubusercontent.com/pambrose/config-data/master/prometheus-proxy/agent.conf --metrics --admin 6 | ``` 7 | 8 | ## Proxy CLI Args 9 | ```bash 10 | --conf https://raw.githubusercontent.com/pambrose/config-data/master/prometheus-proxy/proxy.conf --metrics --admin 11 | ``` -------------------------------------------------------------------------------- /docs/prometheus-proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pambrose/prometheus-proxy/53f10c7f5bcf5ccb7b8731cc0b2d34dd7bc07211/docs/prometheus-proxy.png -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | # Release Creation 2 | 3 | 1) Create branch 4 | 5 | 2) Bump version in source 6 | 7 | 3) Modify code 8 | 9 | 4) Update the release date in `build.gradle` 10 | 11 | 5) Verify tests run cleanly before merge with: `make tests` 12 | 13 | 6) Check in branch and merge 14 | 15 | 7) Go back to master 16 | 17 | 8) Verify tests run cleanly after merge with: `make tests` 18 | 19 | 9) Build distro with: `make distro` 20 | 21 | 10) Create release on GitHub (https://github.com/pambrose/prometheus-proxy/releases) 22 | and upload the *build/libs/prometheus-proxy.jar* and *build/libs/prometheus-agent.jar* files. 23 | 24 | 11) Build and push docker images with: `make docker-push` 25 | 26 | 12) Update the *prometheus-proxy* and *prometheus-agent* repository descriptions on [Docker hub](https://hub.docker.com) 27 | with the latest version of *README.md*. -------------------------------------------------------------------------------- /etc/compose/proxy.yml: -------------------------------------------------------------------------------- 1 | prometheus-proxy: 2 | autoredeploy: true 3 | image: 'pambrose/prometheus-proxy:2.1.0' 4 | ports: 5 | - '8080:8080' 6 | - '8082:8082' 7 | - '8092:8092' 8 | - '50051:50051' 9 | environment: 10 | - PROXY_CONFIG=https://raw.githubusercontent.com/pambrose/config-data/master/prometheus-proxy/cloud-proxy.conf 11 | 12 | prometheus-test: 13 | autoredeploy: true 14 | image: 'pambrose/prometheus-test:latest' 15 | ports: 16 | - '9090:9090' 17 | 18 | #zipkin: 19 | # image: 'openzipkin/zipkin' 20 | # ports: 21 | # - '9411:9411' 22 | -------------------------------------------------------------------------------- /etc/docker/agent.df: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | MAINTAINER Paul Ambrose "pambrose@mac.com" 3 | RUN apk add openjdk17-jre 4 | 5 | # Define the user to use in this instance to prevent using root that even in a container, can be a security risk. 6 | ENV APPLICATION_USER prometheus 7 | 8 | # Then add the user, create the /app folder and give permissions to our user. 9 | RUN adduser --disabled-password --gecos '' $APPLICATION_USER 10 | 11 | RUN mkdir /app 12 | RUN chown -R $APPLICATION_USER /app 13 | 14 | # Mark this container to use the specified $APPLICATION_USER 15 | USER $APPLICATION_USER 16 | 17 | # Make /app the working directory 18 | WORKDIR /app 19 | 20 | COPY ./build/libs/prometheus-agent.jar /app/prometheus-agent.jar 21 | 22 | EXPOSE 8083 23 | EXPOSE 8093 24 | 25 | CMD [] 26 | 27 | ENTRYPOINT ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "/app/prometheus-agent.jar"] -------------------------------------------------------------------------------- /etc/docker/proxy.df: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | MAINTAINER Paul Ambrose "pambrose@mac.com" 3 | RUN apk add openjdk17-jre 4 | 5 | # Define the user to use in this instance to prevent using root that even in a container, can be a security risk. 6 | ENV APPLICATION_USER prometheus 7 | 8 | # Then add the user, create the /app folder and give permissions to our user. 9 | RUN adduser --disabled-password --gecos '' $APPLICATION_USER 10 | 11 | RUN mkdir /app 12 | RUN chown -R $APPLICATION_USER /app 13 | 14 | # Mark this container to use the specified $APPLICATION_USER 15 | USER $APPLICATION_USER 16 | 17 | # Make /app the working directory 18 | WORKDIR /app 19 | 20 | COPY ./build/libs/prometheus-proxy.jar /app/prometheus-proxy.jar 21 | 22 | EXPOSE 8080 23 | EXPOSE 8082 24 | EXPOSE 8092 25 | EXPOSE 50051 26 | EXPOSE 50440 27 | 28 | CMD [] 29 | 30 | ENTRYPOINT ["java", "-server", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "/app/prometheus-proxy.jar"] -------------------------------------------------------------------------------- /etc/jars/tscfg-1.2.4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pambrose/prometheus-proxy/53f10c7f5bcf5ccb7b8731cc0b2d34dd7bc07211/etc/jars/tscfg-1.2.4.jar -------------------------------------------------------------------------------- /etc/test-configs/junit-test.conf: -------------------------------------------------------------------------------- 1 | proxy { 2 | http.port = 8181 3 | internal.zipkin.enabled = true 4 | } 5 | 6 | agent { 7 | pathConfigs: [ 8 | { 9 | name: agent1 10 | path: agent1_metrics 11 | url: "http://localhost:8084/metrics" 12 | }, 13 | { 14 | name: agent2 15 | path: agent2_metrics 16 | url: "http://localhost:8085/metrics" 17 | }, 18 | { 19 | name: agent3 20 | path: agent3_metrics 21 | url: "http://localhost:8086/metrics" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /etc/test-configs/travis.conf: -------------------------------------------------------------------------------- 1 | proxy { 2 | 3 | zipkin.enabled = false 4 | 5 | metrics { 6 | standardExportsEnabled = true 7 | memoryPoolsExportsEnabled = true 8 | garbageCollectorExportsEnabled = true 9 | threadExportsEnabled = true 10 | classLoadingExportsEnabled = true 11 | versionInfoExportsEnabled = true 12 | } 13 | } 14 | 15 | agent { 16 | 17 | zipkin.enabled = false 18 | 19 | metrics { 20 | standardExportsEnabled = true 21 | memoryPoolsExportsEnabled = true 22 | garbageCollectorExportsEnabled = true 23 | threadExportsEnabled = true 24 | classLoadingExportsEnabled = true 25 | versionInfoExportsEnabled = true 26 | } 27 | 28 | // This exercises a code path 29 | pathConfigs: [ 30 | { 31 | name: agent1 32 | path: agent1_metrics 33 | url: "http://localhost:8082/metrics" 34 | }] 35 | } 36 | -------------------------------------------------------------------------------- /examples/federate.conf: -------------------------------------------------------------------------------- 1 | agent { 2 | pathConfigs: [ 3 | { 4 | name: "Federate metrics" 5 | path: federate_metrics 6 | url: "http://prometheus:9090/federate?match[]={job=~'.*'}" 7 | }, 8 | { 9 | name: "Agent metrics" 10 | path: agent_metrics 11 | url: "http://localhost:8083/metrics" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /examples/myapps.conf: -------------------------------------------------------------------------------- 1 | agent { 2 | pathConfigs: [ 3 | { 4 | name: "App1 metrics" 5 | path: app1_metrics 6 | labels: "{\"key1\": \"value1\", \"key2\": 2}" 7 | url: "http://app1.local:9100/metrics" 8 | }, 9 | { 10 | name: "App2 metrics" 11 | path: app2_metrics 12 | labels: "{\"key3\": \"value3\", \"key4\": 4}" 13 | url: "http://app2.local:9100/metrics" 14 | }, 15 | { 16 | name: "App3 metrics" 17 | path: app3_metrics 18 | labels: "{\"key5\": \"value5\", \"key6\": 6}" 19 | url: "http://app3.local:9100/metrics" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /examples/simple.conf: -------------------------------------------------------------------------------- 1 | proxy { 2 | admin.debugEnabled = true 3 | 4 | admin.enabled: true 5 | metrics.enabled: true 6 | 7 | http.requestLoggingEnabled: true 8 | } 9 | 10 | agent { 11 | 12 | proxy.hostname = localhost 13 | admin.enabled: true 14 | metrics.enabled: true 15 | 16 | pathConfigs: [ 17 | { 18 | name: "Proxy metrics" 19 | path: proxy_metrics 20 | labels: "{\"key1\": \"value1\", \"key2\": 2}" 21 | url: "http://localhost:8082/metrics" 22 | //url: "http://"${?HOSTNAME}":8082/metrics" 23 | } 24 | { 25 | name: "Agent metrics" 26 | path: agent_metrics 27 | labels: "{\"key3\": \"value3\", \"key4\": 4}" 28 | url: "http://localhost:8083/metrics" 29 | //url: "http://"${?HOSTNAME}":8083/metrics" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /examples/tls-no-mutual-auth.conf: -------------------------------------------------------------------------------- 1 | proxy { 2 | 3 | agent.port = 50440 4 | 5 | tls { 6 | certChainFilePath = "testing/certs/server1.pem" // Server certificate chain file path 7 | privateKeyFilePath = "testing/certs/server1.key" // Server private key file path 8 | trustCertCollectionFilePath = "" // Trust certificate collection file path 9 | } 10 | } 11 | 12 | agent { 13 | 14 | proxy { 15 | hostname = "localhost" // Proxy hostname 16 | port = 50440 // Proxy port 17 | } 18 | 19 | http { 20 | enableTrustAllX509Certificates = true 21 | } 22 | 23 | // Only trustCertCollectionFilePath is required on the client with TLS (no mutual authentication) 24 | tls { 25 | overrideAuthority = "foo.test.google.fr" // Override authority (for testing only) 26 | certChainFilePath = "" // Client certificate chain file path 27 | privateKeyFilePath = "" // Client private key file path 28 | trustCertCollectionFilePath = "testing/certs/ca.pem" // Trust certificate collection file path 29 | } 30 | } -------------------------------------------------------------------------------- /examples/tls-with-mutual-auth.conf: -------------------------------------------------------------------------------- 1 | proxy { 2 | 3 | agent.port = 50440 4 | 5 | tls { 6 | certChainFilePath = "testing/certs/server1.pem" // Server certificate chain file path 7 | privateKeyFilePath = "testing/certs/server1.key" // Server private key file path 8 | trustCertCollectionFilePath = "testing/certs/ca.pem" // Trust certificate collection file path 9 | } 10 | } 11 | 12 | agent { 13 | 14 | proxy { 15 | hostname = "localhost" // Proxy hostname 16 | port = 50440 // Proxy port 17 | } 18 | 19 | // Only trustCertCollectionFilePath is required on the client with TLS (with mutual authentication) 20 | tls { 21 | overrideAuthority = "foo.test.google.fr" // Override authority (for testing only) 22 | certChainFilePath = "testing/certs/client.pem" // Client certificate chain file path 23 | privateKeyFilePath = "testing/certs/client.key" // Client private key file path 24 | trustCertCollectionFilePath = "testing/certs/ca.pem" // Trust certificate collection file path 25 | } 26 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle settings 2 | kotlin.code.style=official 3 | kotlin.incremental=true 4 | # kotlin.experimental.tryK2=true 5 | org.gradle.daemon=true 6 | org.gradle.configureondemand=true 7 | org.gradle.parallel=true 8 | org.gradle.caching=true 9 | org.gradle.jvmargs=-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 10 | # Plugins 11 | systemProp.configVersion=5.5.4 12 | systemProp.detektVersion=1.23.8 13 | #systemProp.kotestPluginVersion=5.9.1 14 | systemProp.kotlinterVersion=5.0.1 15 | systemProp.kotlinVersion=2.1.20 16 | systemProp.koverVersion=0.9.1 17 | systemProp.protobufVersion=0.9.4 18 | systemProp.shadowVersion=8.1.1 19 | systemProp.versionsVersion=0.52.0 20 | # Jars 21 | annotationVersion=1.3.2 22 | datetimeVersion=0.6.2 23 | dropwizardVersion=4.2.30 24 | gengrpcVersion=1.4.1 25 | grpcVersion=1.71.0 26 | jcommanderVersion=2.0 27 | jettyVersion=10.0.25 28 | junitVersion=5.12.1 29 | junitPlatformVersion=1.12.1 30 | kluentVersion=1.73 31 | kotlinVersion=2.1.20 32 | ktorVersion=3.1.1 33 | logbackVersion=1.5.18 34 | loggingVersion=7.0.5 35 | # Keep in sync with grpc 36 | tcnativeVersion=2.0.70.Final 37 | prometheusVersion=0.16.0 38 | # Keep in sync with grpc 39 | protobufVersion=3.25.4 40 | # Keep in sync with grpc 41 | protocVersion=3.25.4 42 | serializationVersion=1.8.0 43 | slf4jVersion=2.0.13 44 | typesafeVersion=1.4.3 45 | utilsVersion=2.3.10 46 | zipkinVersion=6.1.0 47 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pambrose/prometheus-proxy/53f10c7f5bcf5ccb7b8731cc0b2d34dd7bc07211/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | #before_install: 4 | # - ./custom_setup.sh 5 | #install: 6 | # - echo "Running a custom install command" 7 | # - ./gradlew clean build -xtest publish publishToMavenLocal 8 | #env: 9 | # MYVAR: "custom environment variable" -------------------------------------------------------------------------------- /logback/docker-logback.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | %d{HH:mm:ss.SSS} %-5level [%file:%line] - %msg [%thread]%n 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /nginx/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf 3 | 4 | EXPOSE 50440 5 | -------------------------------------------------------------------------------- /nginx/docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | # 50440 is the agent.proxy.port value 3 | listen 50440 http2; 4 | 5 | # Prevent nginx from closing the gRPC connections (not working for me) 6 | # https://stackoverflow.com/questions/67430437/grpc-send-timeout-doesnt-work-nginx-closes-grpc-streams-unexpectedly 7 | client_header_timeout 1d; 8 | client_body_timeout 1d; 9 | 10 | location / { 11 | # The nginx gRPX options: https://nginx.org/en/docs/http/ngx_http_grpc_module.html 12 | # 50051 is the proxy.agent.port value 13 | grpc_pass grpc://alta.lan:50051; 14 | grpc_socket_keepalive on; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /nginx/docker/run.sh: -------------------------------------------------------------------------------- 1 | docker build -t pambrose/nginx2 . 2 | docker run --rm -p 50440:50440 pambrose/nginx2 -------------------------------------------------------------------------------- /nginx/nginx-proxy.conf: -------------------------------------------------------------------------------- 1 | proxy { 2 | # Required for use with nginx reverse proxy 3 | transportFilterDisabled = true 4 | } 5 | 6 | agent { 7 | # Required for use with nginx reverse proxy 8 | transportFilterDisabled = true 9 | 10 | proxy { 11 | # nginx http2 port specified in nginx.conf 12 | port = 50440 13 | } 14 | 15 | pathConfigs: [ 16 | { 17 | name: "App1 metrics" 18 | path: app1_metrics 19 | url: "http://localhost:8082/metrics" 20 | }, 21 | { 22 | name: "App2 metrics" 23 | path: app2_metrics 24 | url: "http://app2.local:9100/metrics" 25 | }, 26 | { 27 | name: "App3 metrics" 28 | path: app3_metrics 29 | url: "http://app3.local:9100/metrics" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /prom-agent.conf: -------------------------------------------------------------------------------- 1 | proxy { 2 | admin.debugEnabled = true 3 | 4 | admin.enabled: true 5 | metrics.enabled: true 6 | 7 | #transportFilterDisabled = true 8 | 9 | http.requestLoggingEnabled: true 10 | } 11 | 12 | agent { 13 | //scrapeTimeoutSecs = 16 14 | 15 | proxy.hostname = "mac.lan" 16 | admin.enabled: true 17 | metrics.enabled: true 18 | 19 | #transportFilterDisabled = true 20 | 21 | pathConfigs: [ 22 | { 23 | name: "Proxy metrics" 24 | path: proxy_metrics 25 | url: "http://localhost:8082/metrics" 26 | } 27 | { 28 | name: "Agent metrics" 29 | path: agent_metrics 30 | url: "http://localhost:8083/metrics" 31 | } 32 | { 33 | name: "Test metrics" 34 | path: test_val 35 | url: "http://localhost:8088/__test__" 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'prometheus-proxy' -------------------------------------------------------------------------------- /src/main/java/io/prometheus/common/README.txt: -------------------------------------------------------------------------------- 1 | ConfigVals.java is generated using `tscfg` with `make config`. 2 | (https://github.com/carueda/tscfg) 3 | Thus, do not manually edit ConfigVals.java. -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/agent/AgentClientInterceptor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.agent 20 | 21 | import io.github.oshai.kotlinlogging.KotlinLogging 22 | import io.grpc.CallOptions 23 | import io.grpc.Channel 24 | import io.grpc.ClientCall 25 | import io.grpc.ClientInterceptor 26 | import io.grpc.ForwardingClientCall 27 | import io.grpc.ForwardingClientCallListener 28 | import io.grpc.Metadata 29 | import io.grpc.MethodDescriptor 30 | import io.prometheus.Agent 31 | import io.prometheus.common.Messages.EMPTY_AGENT_ID_MSG 32 | import io.prometheus.proxy.ProxyServerInterceptor.Companion.META_AGENT_ID_KEY 33 | 34 | internal class AgentClientInterceptor( 35 | private val agent: Agent, 36 | ) : ClientInterceptor { 37 | override fun interceptCall( 38 | method: MethodDescriptor, 39 | callOptions: CallOptions, 40 | next: Channel, 41 | ): ClientCall = 42 | object : ForwardingClientCall.SimpleForwardingClientCall( 43 | agent.grpcService.channel.newCall(method, callOptions), 44 | ) { 45 | override fun start( 46 | responseListener: Listener, 47 | metadata: Metadata, 48 | ) { 49 | super.start( 50 | object : ForwardingClientCallListener.SimpleForwardingClientCallListener(responseListener) { 51 | override fun onHeaders(headers: Metadata) { 52 | // Grab agent_id from headers if not already assigned 53 | if (agent.agentId.isEmpty()) { 54 | headers.get(META_AGENT_ID_KEY) 55 | ?.also { agentId -> 56 | agent.agentId = agentId 57 | check(agent.agentId.isNotEmpty()) { EMPTY_AGENT_ID_MSG } 58 | logger.info { "Assigned agentId: $agentId to $agent" } 59 | } ?: logger.error { "Headers missing AGENT_ID key" } 60 | } 61 | 62 | super.onHeaders(headers) 63 | } 64 | }, 65 | metadata, 66 | ) 67 | } 68 | } 69 | 70 | companion object { 71 | private val logger = KotlinLogging.logger {} 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/agent/AgentConnectionContext.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.agent 20 | 21 | import com.github.pambrose.common.delegate.AtomicDelegates.atomicBoolean 22 | import io.ktor.utils.io.core.Closeable 23 | import io.prometheus.common.ScrapeRequestAction 24 | import io.prometheus.common.ScrapeResults 25 | import kotlinx.coroutines.channels.Channel 26 | 27 | internal class AgentConnectionContext : Closeable { 28 | private var disconnected by atomicBoolean(false) 29 | val scrapeRequestsChannel = Channel(Channel.UNLIMITED) 30 | val scrapeResultsChannel = Channel(Channel.UNLIMITED) 31 | 32 | override fun close() { 33 | disconnected = true 34 | scrapeRequestsChannel.cancel() 35 | scrapeResultsChannel.cancel() 36 | } 37 | 38 | val connected get() = !disconnected 39 | } 40 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/agent/AgentHttpService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.agent 20 | 21 | import com.github.pambrose.common.dsl.KtorDsl.get 22 | import com.github.pambrose.common.util.isNotNull 23 | import com.github.pambrose.common.util.isNull 24 | import com.github.pambrose.common.util.simpleClassName 25 | import com.github.pambrose.common.util.zip 26 | import com.google.common.net.HttpHeaders.ACCEPT 27 | import com.google.common.net.HttpHeaders.CONTENT_TYPE 28 | import io.github.oshai.kotlinlogging.KotlinLogging 29 | import io.ktor.client.HttpClient 30 | import io.ktor.client.engine.cio.CIO 31 | import io.ktor.client.plugins.HttpRequestRetry 32 | import io.ktor.client.plugins.HttpTimeout 33 | import io.ktor.client.plugins.auth.Auth 34 | import io.ktor.client.plugins.auth.providers.BasicAuthCredentials 35 | import io.ktor.client.plugins.auth.providers.basic 36 | import io.ktor.client.plugins.timeout 37 | import io.ktor.client.request.HttpRequestBuilder 38 | import io.ktor.client.request.header 39 | import io.ktor.client.statement.HttpResponse 40 | import io.ktor.client.statement.bodyAsText 41 | import io.ktor.http.HttpStatusCode 42 | import io.ktor.http.Url 43 | import io.ktor.http.isSuccess 44 | import io.prometheus.Agent 45 | import io.prometheus.common.ScrapeResults 46 | import io.prometheus.common.ScrapeResults.Companion.errorCode 47 | import io.prometheus.common.Utils.decodeParams 48 | import io.prometheus.common.Utils.ifTrue 49 | import io.prometheus.common.Utils.lambda 50 | import io.prometheus.grpc.ScrapeRequest 51 | import kotlin.time.Duration.Companion.seconds 52 | 53 | internal class AgentHttpService( 54 | val agent: Agent, 55 | ) { 56 | suspend fun fetchScrapeUrl(scrapeRequest: ScrapeRequest): ScrapeResults { 57 | val pathContext = agent.pathManager[scrapeRequest.path] 58 | return if (pathContext.isNull()) 59 | handleInvalidPath(scrapeRequest) 60 | else 61 | fetchContentFromUrl(scrapeRequest, pathContext) 62 | } 63 | 64 | private suspend fun AgentHttpService.fetchContentFromUrl( 65 | scrapeRequest: ScrapeRequest, 66 | pathContext: AgentPathManager.PathContext, 67 | ): ScrapeResults = 68 | ScrapeResults(agentId = scrapeRequest.agentId, scrapeId = scrapeRequest.scrapeId).also { scrapeResults -> 69 | val requestTimer = if (agent.isMetricsEnabled) agent.startTimer(agent) else null 70 | // Add the incoming query params to the url 71 | val url = pathContext.url + decodeParams(scrapeRequest.encodedQueryParams) 72 | logger.debug { "Fetching $pathContext ${if (url.isNotBlank()) "URL: $url" else ""}" } 73 | 74 | // Content is fetched here 75 | try { 76 | fetchContent(url, scrapeRequest, scrapeResults) 77 | } finally { 78 | requestTimer?.observeDuration() 79 | } 80 | agent.updateScrapeCounter(scrapeResults.scrapeCounterMsg.load()) 81 | } 82 | 83 | private suspend fun fetchContent( 84 | url: String, 85 | scrapeRequest: ScrapeRequest, 86 | scrapeResults: ScrapeResults, 87 | ) { 88 | runCatching { 89 | newHttpClient(url).use { client -> 90 | client.get( 91 | url = url, 92 | setUp = prepareRequestHeaders(scrapeRequest), 93 | block = processHttpResponse(url, scrapeRequest, scrapeResults), 94 | ) 95 | } 96 | }.onFailure { e -> 97 | with(scrapeResults) { 98 | statusCode = errorCode(e, url) 99 | failureReason = e.message ?: e.simpleClassName 100 | if (scrapeRequest.debugEnabled) 101 | setDebugInfo(url, "${e.simpleClassName} - ${e.message}") 102 | } 103 | } 104 | } 105 | 106 | private fun prepareRequestHeaders(request: ScrapeRequest): HttpRequestBuilder.() -> Unit = 107 | lambda { 108 | request.accept.also { if (it.isNotEmpty()) header(ACCEPT, it) } 109 | val scrapeTimeout = agent.options.scrapeTimeoutSecs.seconds 110 | logger.debug { "Setting scrapeTimeoutSecs = $scrapeTimeout" } 111 | timeout { requestTimeoutMillis = scrapeTimeout.inWholeMilliseconds } 112 | val authHeader = request.authHeader.ifBlank { null } 113 | authHeader?.also { header(io.ktor.http.HttpHeaders.Authorization, it) } 114 | } 115 | 116 | private fun processHttpResponse( 117 | url: String, 118 | scrapeRequest: ScrapeRequest, 119 | scrapeResults: ScrapeResults, 120 | ): suspend (HttpResponse) -> Unit = 121 | lambda { response -> 122 | scrapeResults.statusCode = response.status.value 123 | setScrapeDetailsAndDebugInfo(scrapeRequest, scrapeResults, response, url) 124 | } 125 | 126 | private suspend fun setScrapeDetailsAndDebugInfo( 127 | scrapeRequest: ScrapeRequest, 128 | scrapeResults: ScrapeResults, 129 | response: HttpResponse, 130 | url: String, 131 | ) { 132 | with(scrapeResults) { 133 | if (response.status.isSuccess()) { 134 | contentType = response.headers[CONTENT_TYPE].orEmpty() 135 | if (agent.options.debugEnabled) 136 | logger.info { "CT check - setScrapeDetailsAndDebugInfo() contentType: $contentType" } 137 | // Zip the content here 138 | val content = response.bodyAsText() 139 | zipped = content.length > agent.configVals.agent.minGzipSizeBytes 140 | if (zipped) 141 | contentAsZipped = content.zip() 142 | else 143 | contentAsText = content 144 | validResponse = true 145 | 146 | scrapeRequest.debugEnabled.ifTrue { setDebugInfo(url) } 147 | scrapeCounterMsg.store(SUCCESS_MSG) 148 | } else { 149 | scrapeRequest.debugEnabled.ifTrue { setDebugInfo(url, "Unsuccessful response code $statusCode") } 150 | scrapeCounterMsg.store(UNSUCCESSFUL_MSG) 151 | } 152 | } 153 | } 154 | 155 | private fun newHttpClient(url: String): HttpClient = 156 | HttpClient(CIO) { 157 | expectSuccess = false 158 | engine { 159 | val timeout = agent.configVals.agent.internal.cioTimeoutSecs.seconds 160 | requestTimeout = timeout.inWholeMilliseconds 161 | 162 | val enableTrustAllX509Certificates = agent.configVals.agent.http.enableTrustAllX509Certificates 163 | if (enableTrustAllX509Certificates) { 164 | https { 165 | // trustManager = SslSettings.getTrustManager() 166 | trustManager = TrustAllX509TrustManager 167 | } 168 | } 169 | } 170 | 171 | install(HttpTimeout) 172 | 173 | install(HttpRequestRetry) { 174 | agent.options.scrapeMaxRetries.also { maxRetries -> 175 | if (maxRetries <= 0) { 176 | noRetry() 177 | } else { 178 | retryOnException(maxRetries) 179 | retryIf(maxRetries) { _, response -> 180 | !response.status.isSuccess() && response.status != HttpStatusCode.NotFound 181 | } 182 | modifyRequest { it.headers.append("x-retry-count", retryCount.toString()) } 183 | exponentialDelay() 184 | } 185 | } 186 | } 187 | 188 | val urlObj = Url(url) 189 | val user = urlObj.user 190 | val passwd = urlObj.password 191 | if (user.isNotNull() && passwd.isNotNull()) { 192 | install(Auth) { 193 | basic { 194 | credentials { 195 | BasicAuthCredentials(user, passwd) 196 | } 197 | } 198 | } 199 | } 200 | } 201 | 202 | companion object { 203 | private val logger = KotlinLogging.logger {} 204 | private const val INVALID_PATH_MSG = "invalid_path" 205 | private const val SUCCESS_MSG = "success" 206 | private const val UNSUCCESSFUL_MSG = "unsuccessful" 207 | 208 | private fun handleInvalidPath(scrapeRequest: ScrapeRequest): ScrapeResults { 209 | val scrapeResults = with(scrapeRequest) { ScrapeResults(agentId = agentId, scrapeId = scrapeId) } 210 | logger.warn { "Invalid path in fetchScrapeUrl(): ${scrapeRequest.path}" } 211 | scrapeResults.scrapeCounterMsg.store(INVALID_PATH_MSG) 212 | scrapeRequest.debugEnabled.ifTrue { scrapeResults.setDebugInfo("None", "Invalid path: ${scrapeRequest.path}") } 213 | return scrapeResults 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/agent/AgentMetrics.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.agent 20 | 21 | import com.github.pambrose.common.dsl.PrometheusDsl.counter 22 | import com.github.pambrose.common.dsl.PrometheusDsl.gauge 23 | import com.github.pambrose.common.dsl.PrometheusDsl.summary 24 | import com.github.pambrose.common.metrics.SamplerGaugeCollector 25 | import io.prometheus.Agent 26 | import io.prometheus.common.Utils.lambda 27 | 28 | internal class AgentMetrics( 29 | agent: Agent, 30 | ) { 31 | val scrapeRequestCount = 32 | counter { 33 | name("agent_scrape_request_count") 34 | help("Agent scrape request count") 35 | labelNames(LAUNCH_ID, TYPE) 36 | } 37 | 38 | val scrapeResultCount = 39 | counter { 40 | name("agent_scrape_result_count") 41 | help("Agent scrape result count") 42 | labelNames(LAUNCH_ID, TYPE) 43 | } 44 | 45 | val connectCount = 46 | counter { 47 | name("agent_connect_count") 48 | help("Agent connect count") 49 | labelNames(LAUNCH_ID, TYPE) 50 | } 51 | 52 | val scrapeRequestLatency = 53 | summary { 54 | name("agent_scrape_request_latency_seconds") 55 | help("Agent scrape request latency in seconds") 56 | labelNames(LAUNCH_ID, AGENT_NAME) 57 | } 58 | 59 | init { 60 | gauge { 61 | name("agent_start_time_seconds") 62 | labelNames(LAUNCH_ID) 63 | help("Agent start time in seconds") 64 | }.labels(agent.launchId).setToCurrentTime() 65 | 66 | SamplerGaugeCollector( 67 | "agent_scrape_backlog_size", 68 | "Agent scrape backlog size", 69 | labelNames = listOf(LAUNCH_ID), 70 | labelValues = listOf(agent.launchId), 71 | data = lambda { agent.scrapeRequestBacklogSize.load().toDouble() }, 72 | ) 73 | } 74 | 75 | companion object { 76 | private const val LAUNCH_ID = "launch_id" 77 | private const val AGENT_NAME = "agent_name" 78 | private const val TYPE = "type" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/agent/AgentOptions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.agent 20 | 21 | import com.beust.jcommander.Parameter 22 | import io.github.oshai.kotlinlogging.KotlinLogging 23 | import io.prometheus.Agent 24 | import io.prometheus.common.BaseOptions 25 | import io.prometheus.common.EnvVars.AGENT_CONFIG 26 | import io.prometheus.common.EnvVars.AGENT_NAME 27 | import io.prometheus.common.EnvVars.CHUNK_CONTENT_SIZE_KBS 28 | import io.prometheus.common.EnvVars.CONSOLIDATED 29 | import io.prometheus.common.EnvVars.KEEPALIVE_WITHOUT_CALLS 30 | import io.prometheus.common.EnvVars.MIN_GZIP_SIZE_BYTES 31 | import io.prometheus.common.EnvVars.OVERRIDE_AUTHORITY 32 | import io.prometheus.common.EnvVars.PROXY_HOSTNAME 33 | import io.prometheus.common.EnvVars.SCRAPE_MAX_RETRIES 34 | import io.prometheus.common.EnvVars.SCRAPE_TIMEOUT_SECS 35 | import io.prometheus.common.EnvVars.TRUST_ALL_X509_CERTIFICATES 36 | import kotlin.time.Duration.Companion.seconds 37 | 38 | class AgentOptions( 39 | argv: Array, 40 | exitOnMissingConfig: Boolean, 41 | ) : BaseOptions(Agent::class.java.name, argv, AGENT_CONFIG.name, exitOnMissingConfig) { 42 | constructor(args: List, exitOnMissingConfig: Boolean) : 43 | this(args.toTypedArray(), exitOnMissingConfig) 44 | 45 | constructor(configFilename: String, exitOnMissingConfig: Boolean) : 46 | this(listOf("--config", configFilename), exitOnMissingConfig) 47 | 48 | @Parameter(names = ["-p", "--proxy"], description = "Proxy hostname") 49 | var proxyHostname = "" 50 | private set 51 | 52 | @Parameter(names = ["-n", "--name"], description = "Agent name") 53 | var agentName = "" 54 | private set 55 | 56 | @Parameter(names = ["-o", "--consolidated"], description = "Consolidated Agent") 57 | var consolidated = false 58 | private set 59 | 60 | @Parameter(names = ["--over", "--override"], description = "Override Authority") 61 | var overrideAuthority = "" 62 | private set 63 | 64 | @Parameter(names = ["--timeout"], description = "Scrape timeout time (seconds)") 65 | var scrapeTimeoutSecs = -1 66 | private set 67 | 68 | @Parameter(names = ["--max_retries"], description = "Scrape max retries") 69 | var scrapeMaxRetries = -1 70 | private set 71 | 72 | @Parameter(names = ["--chunk"], description = "Threshold for chunking content to Proxy and buffer size (KBs)") 73 | var chunkContentSizeKbs = -1 74 | private set 75 | 76 | @Parameter(names = ["--gzip"], description = "Minimum size for content to be gzipped (bytes)") 77 | var minGzipSizeBytes = -1 78 | private set 79 | 80 | @Parameter(names = ["--trust_all_x509"], description = "Disable SSL verification for https agent endpoints") 81 | var trustAllX509Certificates = false 82 | private set 83 | 84 | @Parameter(names = ["--keepalive_without_calls"], description = "gRPC KeepAlive without calls") 85 | var keepAliveWithoutCalls = false 86 | private set 87 | 88 | init { 89 | parseOptions() 90 | } 91 | 92 | override fun assignConfigVals() { 93 | configVals.agent 94 | .also { agentConfigVals -> 95 | if (proxyHostname.isEmpty()) { 96 | val configHostname = agentConfigVals.proxy.hostname 97 | val str = if (":" in configHostname) 98 | configHostname 99 | else 100 | "$configHostname:${agentConfigVals.proxy.port}" 101 | proxyHostname = PROXY_HOSTNAME.getEnv(str) 102 | } 103 | logger.info { "proxyHostname: $proxyHostname" } 104 | 105 | if (agentName.isEmpty()) 106 | agentName = AGENT_NAME.getEnv(agentConfigVals.name) 107 | logger.info { "agentName: $agentName" } 108 | 109 | if (!consolidated) 110 | consolidated = CONSOLIDATED.getEnv(agentConfigVals.consolidated) 111 | logger.info { "consolidated: $consolidated" } 112 | 113 | if (scrapeTimeoutSecs == -1) 114 | scrapeTimeoutSecs = SCRAPE_TIMEOUT_SECS.getEnv(agentConfigVals.scrapeTimeoutSecs) 115 | logger.info { "scrapeTimeoutSecs: ${scrapeTimeoutSecs.seconds}" } 116 | 117 | if (scrapeMaxRetries == -1) 118 | scrapeMaxRetries = SCRAPE_MAX_RETRIES.getEnv(agentConfigVals.scrapeMaxRetries) 119 | logger.info { "scrapeMaxRetries: $scrapeMaxRetries" } 120 | 121 | if (chunkContentSizeKbs == -1) 122 | chunkContentSizeKbs = CHUNK_CONTENT_SIZE_KBS.getEnv(agentConfigVals.chunkContentSizeKbs) 123 | // Multiply the value time KB 124 | chunkContentSizeKbs *= 1024 125 | logger.info { "chunkContentSizeKbs: $chunkContentSizeKbs" } 126 | 127 | if (minGzipSizeBytes == -1) 128 | minGzipSizeBytes = MIN_GZIP_SIZE_BYTES.getEnv(agentConfigVals.minGzipSizeBytes) 129 | logger.info { "minGzipSizeBytes: $minGzipSizeBytes" } 130 | 131 | if (overrideAuthority.isEmpty()) 132 | overrideAuthority = OVERRIDE_AUTHORITY.getEnv(agentConfigVals.tls.overrideAuthority) 133 | logger.info { "overrideAuthority: $overrideAuthority" } 134 | 135 | if (!trustAllX509Certificates) 136 | trustAllX509Certificates = 137 | TRUST_ALL_X509_CERTIFICATES.getEnv(agentConfigVals.http.enableTrustAllX509Certificates) 138 | logger.info { "trustAllX509Certificates: $trustAllX509Certificates" } 139 | 140 | if (!keepAliveWithoutCalls) 141 | keepAliveWithoutCalls = KEEPALIVE_WITHOUT_CALLS.getEnv(agentConfigVals.grpc.keepAliveWithoutCalls) 142 | logger.info { "grpc.keepAliveWithoutCalls: $keepAliveWithoutCalls" } 143 | 144 | with(agentConfigVals) { 145 | assignKeepAliveTimeSecs(grpc.keepAliveTimeSecs) 146 | assignKeepAliveTimeoutSecs(grpc.keepAliveTimeoutSecs) 147 | assignAdminEnabled(admin.enabled) 148 | assignAdminPort(admin.port) 149 | assignMetricsEnabled(metrics.enabled) 150 | assignMetricsPort(metrics.port) 151 | assignTransportFilterDisabled(transportFilterDisabled) 152 | assignDebugEnabled(admin.debugEnabled) 153 | 154 | assignCertChainFilePath(tls.certChainFilePath) 155 | assignPrivateKeyFilePath(tls.privateKeyFilePath) 156 | assignTrustCertCollectionFilePath(tls.trustCertCollectionFilePath) 157 | 158 | logger.info { "scrapeTimeoutSecs: ${scrapeTimeoutSecs.seconds}" } 159 | logger.info { "agent.internal.cioTimeoutSecs: ${internal.cioTimeoutSecs.seconds}" } 160 | 161 | val pauseVal = internal.heartbeatCheckPauseMillis 162 | logger.info { "agent.internal.heartbeatCheckPauseMillis: $pauseVal" } 163 | 164 | val inactivityVal = internal.heartbeatMaxInactivitySecs 165 | logger.info { "agent.internal.heartbeatMaxInactivitySecs: $inactivityVal" } 166 | } 167 | } 168 | } 169 | 170 | companion object { 171 | private val logger = KotlinLogging.logger {} 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/agent/AgentPathManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.agent 20 | 21 | import com.github.pambrose.common.util.isNotNull 22 | import com.github.pambrose.common.util.isNull 23 | import com.google.common.collect.Maps.newConcurrentMap 24 | import io.github.oshai.kotlinlogging.KotlinLogging 25 | import io.prometheus.Agent 26 | import io.prometheus.common.Messages.EMPTY_PATH_MSG 27 | import io.prometheus.common.Utils.defaultEmptyJsonObject 28 | 29 | internal class AgentPathManager( 30 | private val agent: Agent, 31 | ) { 32 | private val agentConfigVals = agent.configVals.agent 33 | private val pathContextMap = newConcurrentMap() 34 | 35 | operator fun get(path: String): PathContext? = pathContextMap[path] 36 | 37 | fun clear() = pathContextMap.clear() 38 | 39 | fun pathMapSize(): Int = agent.grpcService.pathMapSize() 40 | 41 | private val pathConfigs = 42 | agentConfigVals.pathConfigs 43 | .map { 44 | mapOf( 45 | NAME to """"${it.name}"""", 46 | PATH to it.path, 47 | URL to it.url, 48 | LABELS to it.labels, 49 | ) 50 | } 51 | .onEach { 52 | logger.info { "Proxy path /${it[PATH]} will be assigned to ${it[URL]} with labels ${it[LABELS]}" } 53 | } 54 | 55 | suspend fun registerPaths() = 56 | pathConfigs.forEach { 57 | val path = it[PATH] 58 | val url = it[URL] 59 | val labels = it[LABELS] 60 | if (path.isNotNull() && url.isNotNull() && labels.isNotNull()) 61 | registerPath(path, url, labels) 62 | else 63 | logger.error { "Null path/url/labels value: $path/$url/$labels" } 64 | } 65 | 66 | suspend fun registerPath( 67 | pathVal: String, 68 | url: String, 69 | labels: String = "{}", 70 | ) { 71 | require(pathVal.isNotEmpty()) { EMPTY_PATH_MSG } 72 | require(url.isNotEmpty()) { "Empty URL" } 73 | 74 | val path = if (pathVal.startsWith("/")) pathVal.substring(1) else pathVal 75 | val labelsJson = labels.defaultEmptyJsonObject() 76 | val pathId = agent.grpcService.registerPathOnProxy(path, labelsJson).pathId 77 | if (!agent.isTestMode) 78 | logger.info { "Registered $url as /$path with labels $labelsJson" } 79 | pathContextMap[path] = PathContext(pathId, path, url, labelsJson) 80 | } 81 | 82 | suspend fun unregisterPath(pathVal: String) { 83 | require(pathVal.isNotEmpty()) { EMPTY_PATH_MSG } 84 | 85 | val path = if (pathVal.startsWith("/")) pathVal.substring(1) else pathVal 86 | agent.grpcService.unregisterPathOnProxy(path) 87 | val pathContext = pathContextMap.remove(path) 88 | when { 89 | pathContext.isNull() -> logger.info { "No path value /$path found in pathContextMap when unregistering" } 90 | !agent.isTestMode -> logger.info { "Unregistered /$path for ${pathContext.url}" } 91 | } 92 | } 93 | 94 | fun toPlainText(): String { 95 | val maxName = pathConfigs.maxOfOrNull { it[NAME].orEmpty().length } ?: 0 96 | val maxPath = pathConfigs.maxOfOrNull { it[PATH].orEmpty().length } ?: 0 97 | return "Agent Path Configs:\n" + "Name".padEnd(maxName + 1) + "Path".padEnd(maxPath + 2) + "URL\n" + 98 | pathConfigs.joinToString("\n") { c -> "${c[NAME]?.padEnd(maxName)} /${c[PATH]?.padEnd(maxPath)} ${c[URL]}" } 99 | } 100 | 101 | companion object { 102 | private val logger = KotlinLogging.logger {} 103 | private const val NAME = "name" 104 | private const val PATH = "path" 105 | private const val URL = "url" 106 | private const val LABELS = "labels" 107 | } 108 | 109 | data class PathContext( 110 | val pathId: Long, 111 | val path: String, 112 | val url: String, 113 | val labels: String, 114 | ) 115 | } 116 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/agent/EmbeddedAgentInfo.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.prometheus.agent 18 | 19 | data class EmbeddedAgentInfo( 20 | val launchId: String, 21 | val agentName: String, 22 | ) 23 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/agent/RequestFailureException.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.agent 20 | 21 | internal class RequestFailureException( 22 | message: String, 23 | ) : Exception(message) { 24 | companion object { 25 | private const val serialVersionUID = 8748724180953791199L 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/agent/SslSettings.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.prometheus.agent 18 | 19 | import java.io.FileInputStream 20 | import java.security.KeyStore 21 | import javax.net.ssl.SSLContext 22 | import javax.net.ssl.TrustManagerFactory 23 | import javax.net.ssl.X509TrustManager 24 | 25 | // https://github.com/Hakky54/mutual-tls-ssl/blob/master/client/src/main/java/nl/altindag/client/service/KtorCIOHttpClientService.kt 26 | 27 | @Suppress("unused") 28 | object SslSettings { 29 | fun getKeyStore( 30 | fileName: String, 31 | password: String, 32 | ): KeyStore = 33 | KeyStore.getInstance(KeyStore.getDefaultType()) 34 | .apply { 35 | val keyStoreFile = FileInputStream(fileName) 36 | val keyStorePassword = password.toCharArray() 37 | load(keyStoreFile, keyStorePassword) 38 | } 39 | 40 | fun getTrustManagerFactory( 41 | fileName: String, 42 | password: String, 43 | ): TrustManagerFactory? = 44 | TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) 45 | .apply { 46 | init(getKeyStore(fileName, password)) 47 | } 48 | 49 | fun getSslContext( 50 | fileName: String, 51 | password: String, 52 | ): SSLContext? = 53 | SSLContext.getInstance("TLS") 54 | .apply { 55 | init(null, getTrustManagerFactory(fileName, password)?.trustManagers, null) 56 | } 57 | 58 | fun getTrustManager( 59 | fileName: String, 60 | password: String, 61 | ): X509TrustManager = 62 | getTrustManagerFactory(fileName, password)?.trustManagers?.first { it is X509TrustManager } as X509TrustManager 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/agent/TrustAllX509TrustManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.prometheus.agent 18 | 19 | import java.security.cert.X509Certificate 20 | import javax.net.ssl.X509TrustManager 21 | 22 | // https://stackoverflow.com/questions/66490928/how-can-i-disable-ktor-client-ssl-verification 23 | 24 | object TrustAllX509TrustManager : X509TrustManager { 25 | private val EMPTY_CERTIFICATES: Array = arrayOfNulls(0) 26 | 27 | override fun getAcceptedIssuers(): Array = EMPTY_CERTIFICATES 28 | 29 | override fun checkClientTrusted( 30 | certs: Array?, 31 | authType: String?, 32 | ) { 33 | } 34 | 35 | override fun checkServerTrusted( 36 | certs: Array?, 37 | authType: String?, 38 | ) { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/common/ConfigWrappers.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.common 20 | 21 | import com.github.pambrose.common.service.AdminConfig 22 | import com.github.pambrose.common.service.MetricsConfig 23 | import com.github.pambrose.common.service.ZipkinConfig 24 | 25 | @Suppress("unused") 26 | internal object ConfigWrappers { 27 | fun newAdminConfig( 28 | enabled: Boolean, 29 | port: Int, 30 | admin: ConfigVals.Proxy2.Admin2, 31 | ) = AdminConfig( 32 | enabled = enabled, 33 | port = port, 34 | pingPath = admin.pingPath, 35 | versionPath = admin.versionPath, 36 | healthCheckPath = admin.healthCheckPath, 37 | threadDumpPath = admin.threadDumpPath, 38 | ) 39 | 40 | fun newAdminConfig( 41 | enabled: Boolean, 42 | port: Int, 43 | admin: ConfigVals.Agent.Admin, 44 | ) = AdminConfig( 45 | enabled = enabled, 46 | port = port, 47 | pingPath = admin.pingPath, 48 | versionPath = admin.versionPath, 49 | healthCheckPath = admin.healthCheckPath, 50 | threadDumpPath = admin.threadDumpPath, 51 | ) 52 | 53 | fun newMetricsConfig( 54 | enabled: Boolean, 55 | port: Int, 56 | metrics: ConfigVals.Proxy2.Metrics2, 57 | ) = MetricsConfig( 58 | enabled = enabled, 59 | port = port, 60 | path = metrics.path, 61 | standardExportsEnabled = metrics.standardExportsEnabled, 62 | memoryPoolsExportsEnabled = metrics.memoryPoolsExportsEnabled, 63 | garbageCollectorExportsEnabled = metrics.garbageCollectorExportsEnabled, 64 | threadExportsEnabled = metrics.threadExportsEnabled, 65 | classLoadingExportsEnabled = metrics.classLoadingExportsEnabled, 66 | versionInfoExportsEnabled = metrics.versionInfoExportsEnabled, 67 | ) 68 | 69 | fun newMetricsConfig( 70 | enabled: Boolean, 71 | port: Int, 72 | metrics: ConfigVals.Agent.Metrics, 73 | ) = MetricsConfig( 74 | enabled = enabled, 75 | port = port, 76 | path = metrics.path, 77 | standardExportsEnabled = metrics.standardExportsEnabled, 78 | memoryPoolsExportsEnabled = metrics.memoryPoolsExportsEnabled, 79 | garbageCollectorExportsEnabled = metrics.garbageCollectorExportsEnabled, 80 | threadExportsEnabled = metrics.threadExportsEnabled, 81 | classLoadingExportsEnabled = metrics.classLoadingExportsEnabled, 82 | versionInfoExportsEnabled = metrics.versionInfoExportsEnabled, 83 | ) 84 | 85 | fun newZipkinConfig(zipkin: ConfigVals.Proxy2.Internal2.Zipkin2) = 86 | ZipkinConfig( 87 | enabled = zipkin.enabled, 88 | hostname = zipkin.hostname, 89 | port = zipkin.port, 90 | path = zipkin.path, 91 | serviceName = zipkin.serviceName, 92 | ) 93 | 94 | fun newZipkinConfig(zipkin: ConfigVals.Agent.Internal.Zipkin) = 95 | ZipkinConfig( 96 | enabled = zipkin.enabled, 97 | hostname = zipkin.hostname, 98 | port = zipkin.port, 99 | path = zipkin.path, 100 | serviceName = zipkin.serviceName, 101 | ) 102 | } 103 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/common/Constants.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.prometheus.common 18 | 19 | import com.google.protobuf.Empty 20 | 21 | internal object Constants { 22 | const val UNKNOWN = "Unknown" 23 | } 24 | 25 | internal object Messages { 26 | const val EMPTY_AGENT_ID_MSG = "Empty agentId" 27 | const val EMPTY_PATH_MSG = "Empty path" 28 | } 29 | 30 | internal object DefaultObjects { 31 | val EMPTY_INSTANCE: Empty = Empty.getDefaultInstance() 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/common/EnvVars.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.common 20 | 21 | import java.lang.System.getenv 22 | 23 | enum class EnvVars { 24 | // Proxy 25 | PROXY_CONFIG, 26 | PROXY_PORT, 27 | AGENT_PORT, 28 | SD_ENABLED, 29 | SD_PATH, 30 | SD_TARGET_PREFIX, 31 | REFLECTION_DISABLED, 32 | 33 | HANDSHAKE_TIMEOUT_SECS, 34 | PERMIT_KEEPALIVE_WITHOUT_CALLS, 35 | PERMIT_KEEPALIVE_TIME_SECS, 36 | MAX_CONNECTION_IDLE_SECS, 37 | MAX_CONNECTION_AGE_SECS, 38 | MAX_CONNECTION_AGE_GRACE_SECS, 39 | 40 | // Agent 41 | AGENT_CONFIG, 42 | PROXY_HOSTNAME, 43 | AGENT_NAME, 44 | CONSOLIDATED, 45 | SCRAPE_TIMEOUT_SECS, 46 | SCRAPE_MAX_RETRIES, 47 | CHUNK_CONTENT_SIZE_KBS, 48 | MIN_GZIP_SIZE_BYTES, 49 | TRUST_ALL_X509_CERTIFICATES, 50 | 51 | KEEPALIVE_WITHOUT_CALLS, 52 | 53 | // Common 54 | DEBUG_ENABLED, 55 | METRICS_ENABLED, 56 | METRICS_PORT, 57 | ADMIN_ENABLED, 58 | ADMIN_PORT, 59 | TRANSPORT_FILTER_DISABLED, 60 | 61 | CERT_CHAIN_FILE_PATH, 62 | PRIVATE_KEY_FILE_PATH, 63 | TRUST_CERT_COLLECTION_FILE_PATH, 64 | OVERRIDE_AUTHORITY, 65 | 66 | KEEPALIVE_TIME_SECS, 67 | KEEPALIVE_TIMEOUT_SECS, 68 | ; 69 | 70 | fun getEnv(defaultVal: String) = getenv(name) ?: defaultVal 71 | 72 | fun getEnv(defaultVal: Boolean) = getenv(name)?.toBoolean() ?: defaultVal 73 | 74 | fun getEnv(defaultVal: Int) = getenv(name)?.toInt() ?: defaultVal 75 | 76 | fun getEnv(defaultVal: Long) = getenv(name)?.toLong() ?: defaultVal 77 | } 78 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/common/GrpcObjects.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.common 20 | 21 | import com.google.protobuf.ByteString 22 | import io.prometheus.grpc.ChunkData 23 | import io.prometheus.grpc.ChunkedScrapeResponse 24 | import io.prometheus.grpc.ScrapeResponse 25 | import io.prometheus.grpc.SummaryData 26 | import java.util.zip.CRC32 27 | 28 | internal object GrpcObjects { 29 | fun ScrapeResponse.toScrapeResults() = 30 | ScrapeResults( 31 | agentId = agentId, 32 | scrapeId = scrapeId, 33 | validResponse = validResponse, 34 | statusCode = statusCode, 35 | contentType = contentType, 36 | zipped = zipped, 37 | failureReason = failureReason, 38 | url = url, 39 | ).also { results -> 40 | if (zipped) 41 | results.contentAsZipped = contentAsZipped.toByteArray() 42 | else 43 | results.contentAsText = contentAsText 44 | } 45 | 46 | fun newScrapeResponseChunk( 47 | scrapeId: Long, 48 | totalChunkCount: Int, 49 | readByteCount: Int, 50 | checksum: CRC32, 51 | buffer: ByteArray, 52 | ) = ChunkedScrapeResponse 53 | .newBuilder() 54 | .apply { 55 | chunk = ChunkData 56 | .newBuilder() 57 | .also { 58 | it.chunkScrapeId = scrapeId 59 | it.chunkCount = totalChunkCount 60 | it.chunkByteCount = readByteCount 61 | it.chunkChecksum = checksum.value 62 | it.chunkBytes = ByteString.copyFrom(buffer) 63 | } 64 | .build() 65 | } 66 | .build()!! 67 | 68 | fun newScrapeResponseSummary( 69 | scrapeId: Long, 70 | totalChunkCount: Int, 71 | totalByteCount: Int, 72 | checksum: CRC32, 73 | ) = ChunkedScrapeResponse 74 | .newBuilder() 75 | .also { 76 | it.summary = 77 | SummaryData 78 | .newBuilder() 79 | .also { 80 | it.summaryScrapeId = scrapeId 81 | it.summaryChunkCount = totalChunkCount 82 | it.summaryByteCount = totalByteCount 83 | it.summaryChecksum = checksum.value 84 | } 85 | .build() 86 | } 87 | .build()!! 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/common/ScrapeResults.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.common 20 | 21 | import com.github.pambrose.common.util.EMPTY_BYTE_ARRAY 22 | import com.github.pambrose.common.util.simpleClassName 23 | import com.google.protobuf.ByteString 24 | import io.github.oshai.kotlinlogging.KotlinLogging 25 | import io.ktor.client.plugins.HttpRequestTimeoutException 26 | import io.ktor.http.HttpStatusCode.Companion.NotFound 27 | import io.ktor.http.HttpStatusCode.Companion.RequestTimeout 28 | import io.ktor.http.HttpStatusCode.Companion.ServiceUnavailable 29 | import io.ktor.network.sockets.SocketTimeoutException 30 | import io.prometheus.grpc.ChunkedScrapeResponse 31 | import io.prometheus.grpc.HeaderData 32 | import io.prometheus.grpc.ScrapeResponse 33 | import kotlinx.coroutines.TimeoutCancellationException 34 | import java.io.IOException 35 | import java.net.http.HttpConnectTimeoutException 36 | import kotlin.concurrent.atomics.AtomicReference 37 | 38 | internal class ScrapeResults( 39 | val agentId: String, 40 | val scrapeId: Long, 41 | var validResponse: Boolean = false, 42 | var statusCode: Int = NotFound.value, 43 | var contentType: String = "", 44 | var zipped: Boolean = false, 45 | var contentAsText: String = "", 46 | var contentAsZipped: ByteArray = EMPTY_BYTE_ARRAY, 47 | var failureReason: String = "", 48 | var url: String = "", 49 | ) { 50 | val scrapeCounterMsg = AtomicReference("") 51 | 52 | fun setDebugInfo( 53 | url: String, 54 | failureReason: String = "", 55 | ) { 56 | this.url = url 57 | this.failureReason = failureReason 58 | } 59 | 60 | fun toScrapeResponse() = 61 | ScrapeResponse 62 | .newBuilder() 63 | .also { 64 | it.agentId = agentId 65 | it.scrapeId = scrapeId 66 | it.validResponse = validResponse 67 | it.statusCode = statusCode 68 | it.contentType = contentType 69 | it.zipped = zipped 70 | if (zipped) 71 | it.contentAsZipped = ByteString.copyFrom(contentAsZipped) 72 | else 73 | it.contentAsText = contentAsText 74 | it.failureReason = failureReason 75 | it.url = url 76 | } 77 | .build()!! 78 | 79 | fun toScrapeResponseHeader() = 80 | ChunkedScrapeResponse 81 | .newBuilder() 82 | .apply { 83 | header = HeaderData 84 | .newBuilder() 85 | .also { 86 | it.headerValidResponse = validResponse 87 | it.headerAgentId = agentId 88 | it.headerScrapeId = scrapeId 89 | it.headerStatusCode = statusCode 90 | it.headerFailureReason = failureReason 91 | it.headerUrl = url 92 | it.headerContentType = contentType 93 | } 94 | .build() 95 | }.build()!! 96 | 97 | companion object { 98 | private val logger = KotlinLogging.logger {} 99 | 100 | fun errorCode( 101 | e: Throwable, 102 | url: String, 103 | ): Int = 104 | when (e) { 105 | is TimeoutCancellationException, 106 | is HttpConnectTimeoutException, 107 | is SocketTimeoutException, 108 | is HttpRequestTimeoutException, 109 | -> { 110 | logger.warn(e) { "fetchScrapeUrl() $e - $url" } 111 | RequestTimeout.value 112 | } 113 | 114 | is IOException -> { 115 | logger.info { "Failed HTTP request: $url [${e.simpleClassName}: ${e.message}]" } 116 | NotFound.value 117 | } 118 | 119 | else -> { 120 | logger.warn(e) { "fetchScrapeUrl() $e - $url" } 121 | ServiceUnavailable.value 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/common/TypeAliases.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.prometheus.common 18 | 19 | @Suppress("unused") 20 | object TypeAliases 21 | 22 | internal typealias ScrapeRequestAction = suspend () -> ScrapeResults 23 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/common/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.common 20 | 21 | import com.beust.jcommander.IParameterValidator 22 | import com.beust.jcommander.JCommander 23 | import com.github.pambrose.common.util.Version.Companion.versionDesc 24 | import io.prometheus.Proxy 25 | import kotlinx.serialization.json.Json 26 | import java.net.URLDecoder 27 | import java.util.* 28 | import kotlin.system.exitProcess 29 | import kotlin.text.Charsets.UTF_8 30 | 31 | object Utils { 32 | internal fun getVersionDesc(asJson: Boolean = false): String = Proxy::class.versionDesc(asJson) 33 | 34 | internal class VersionValidator : IParameterValidator { 35 | override fun validate( 36 | name: String, 37 | value: String, 38 | ) { 39 | val console = JCommander().console 40 | console.println(getVersionDesc(false)) 41 | exitProcess(0) 42 | } 43 | } 44 | 45 | // This eliminates an extra set of paren in when blocks and if/else stmts 46 | fun lambda(block: T) = block 47 | 48 | fun Boolean.ifTrue(block: () -> Unit) { 49 | if (this) block() 50 | } 51 | 52 | fun String.toLowercase() = this.lowercase(Locale.getDefault()) 53 | 54 | fun decodeParams(encodedQueryParams: String): String = 55 | if (encodedQueryParams.isNotBlank()) "?${URLDecoder.decode(encodedQueryParams, UTF_8.name())}" else "" 56 | 57 | internal fun String.defaultEmptyJsonObject() = if (isEmpty()) "{}" else this 58 | 59 | fun String.toJsonElement() = Json.parseToJsonElement(this) 60 | } 61 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/AgentContext.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import com.github.pambrose.common.delegate.AtomicDelegates.atomicBoolean 22 | import com.github.pambrose.common.delegate.AtomicDelegates.nonNullableReference 23 | import com.github.pambrose.common.dsl.GuavaDsl.toStringElements 24 | import io.prometheus.grpc.RegisterAgentRequest 25 | import kotlinx.coroutines.channels.Channel 26 | import kotlin.concurrent.atomics.AtomicInt 27 | import kotlin.concurrent.atomics.AtomicLong 28 | import kotlin.concurrent.atomics.incrementAndFetch 29 | import kotlin.concurrent.atomics.minusAssign 30 | import kotlin.concurrent.atomics.plusAssign 31 | import kotlin.time.TimeMark 32 | import kotlin.time.TimeSource.Monotonic 33 | 34 | internal class AgentContext( 35 | private val remoteAddr: String, 36 | ) { 37 | val agentId = AGENT_ID_GENERATOR.incrementAndFetch().toString() 38 | 39 | private val scrapeRequestChannel = Channel(Channel.UNLIMITED) 40 | private val channelBacklogSize = AtomicInt(0) 41 | 42 | private val clock = Monotonic 43 | private var lastActivityTimeMark: TimeMark by nonNullableReference(clock.markNow()) 44 | private var lastRequestTimeMark: TimeMark by nonNullableReference(clock.markNow()) 45 | private var valid by atomicBoolean(true) 46 | 47 | private var launchId: String by nonNullableReference("Unassigned") 48 | var hostName: String by nonNullableReference("Unassigned") 49 | private set 50 | var agentName: String by nonNullableReference("Unassigned") 51 | private set 52 | var consolidated: Boolean by nonNullableReference(false) 53 | private set 54 | 55 | internal val desc: String 56 | get() = if (consolidated) "consolidated " else "" 57 | 58 | private val lastRequestDuration 59 | get() = lastRequestTimeMark.elapsedNow() 60 | 61 | val inactivityDuration 62 | get() = lastActivityTimeMark.elapsedNow() 63 | 64 | val scrapeRequestBacklogSize: Int 65 | get() = channelBacklogSize.load() 66 | 67 | init { 68 | markActivityTime(true) 69 | } 70 | 71 | fun assignProperties(request: RegisterAgentRequest) { 72 | launchId = request.launchId 73 | agentName = request.agentName 74 | hostName = request.hostName 75 | consolidated = request.consolidated 76 | } 77 | 78 | suspend fun writeScrapeRequest(scrapeRequest: ScrapeRequestWrapper) { 79 | scrapeRequestChannel.send(scrapeRequest) 80 | channelBacklogSize += 1 81 | } 82 | 83 | suspend fun readScrapeRequest(): ScrapeRequestWrapper? = 84 | scrapeRequestChannel.receiveCatching().getOrNull() 85 | ?.apply { 86 | channelBacklogSize -= 1 87 | } 88 | 89 | fun isValid() = valid && !scrapeRequestChannel.isClosedForReceive 90 | 91 | fun isNotValid() = !isValid() 92 | 93 | fun invalidate() { 94 | valid = false 95 | scrapeRequestChannel.close() 96 | } 97 | 98 | fun markActivityTime(isRequest: Boolean) { 99 | lastActivityTimeMark = clock.markNow() 100 | 101 | if (isRequest) 102 | lastRequestTimeMark = clock.markNow() 103 | } 104 | 105 | override fun toString() = 106 | toStringElements { 107 | add("agentId", agentId) 108 | add("launchId", launchId) 109 | add("consolidated", consolidated) 110 | add("valid", valid) 111 | add("agentName", agentName) 112 | add("hostName", hostName) 113 | add("remoteAddr", remoteAddr) 114 | add("lastRequestDuration", lastRequestDuration) 115 | // add("inactivityDuration", inactivityDuration) 116 | } 117 | 118 | override fun equals(other: Any?): Boolean { 119 | if (this === other) return true 120 | if (javaClass != other?.javaClass) return false 121 | other as AgentContext 122 | return agentId == other.agentId 123 | } 124 | 125 | override fun hashCode() = agentId.hashCode() 126 | 127 | companion object { 128 | private val AGENT_ID_GENERATOR = AtomicLong(0L) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/AgentContextCleanupService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import com.github.pambrose.common.concurrent.GenericExecutionThreadService 22 | import com.github.pambrose.common.concurrent.genericServiceListener 23 | import com.github.pambrose.common.dsl.GuavaDsl.toStringElements 24 | import com.github.pambrose.common.util.sleep 25 | import com.google.common.util.concurrent.MoreExecutors 26 | import io.github.oshai.kotlinlogging.KotlinLogging 27 | import io.prometheus.Proxy 28 | import io.prometheus.common.ConfigVals 29 | import io.prometheus.common.Utils.lambda 30 | import kotlin.time.Duration.Companion.seconds 31 | 32 | internal class AgentContextCleanupService( 33 | private val proxy: Proxy, 34 | private val configVals: ConfigVals.Proxy2.Internal2, 35 | initBlock: (AgentContextCleanupService.() -> Unit) = lambda {}, 36 | ) : GenericExecutionThreadService() { 37 | init { 38 | addListener(genericServiceListener(logger), MoreExecutors.directExecutor()) 39 | initBlock(this) 40 | } 41 | 42 | override fun run() { 43 | val maxAgentInactivityTime = configVals.maxAgentInactivitySecs.seconds 44 | val pauseTime = configVals.staleAgentCheckPauseSecs.seconds 45 | while (isRunning) { 46 | proxy.agentContextManager.agentContextMap 47 | .forEach { (agentId, agentContext) -> 48 | val inactiveTime = agentContext.inactivityDuration 49 | if (inactiveTime > maxAgentInactivityTime) { 50 | logger.info { 51 | val id = agentContext.agentId 52 | "Evicting agentId $id after $inactiveTime (max $maxAgentInactivityTime) of inactivity: $agentContext" 53 | } 54 | proxy.removeAgentContext(agentId, "Eviction") 55 | proxy.metrics { agentEvictionCount.inc() } 56 | } 57 | } 58 | sleep(pauseTime) 59 | } 60 | } 61 | 62 | override fun toString() = 63 | toStringElements { 64 | add("max inactivity secs", configVals.maxAgentInactivitySecs) 65 | add("pause secs", configVals.staleAgentCheckPauseSecs) 66 | } 67 | 68 | companion object { 69 | private val logger = KotlinLogging.logger {} 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/AgentContextManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import com.github.pambrose.common.util.isNull 22 | import com.google.common.collect.Maps.newConcurrentMap 23 | import io.github.oshai.kotlinlogging.KotlinLogging 24 | import java.util.concurrent.ConcurrentMap 25 | 26 | internal class AgentContextManager( 27 | private val isTestMode: Boolean, 28 | ) { 29 | // Map agent_id to AgentContext 30 | val agentContextMap: ConcurrentMap = newConcurrentMap() 31 | val agentContextSize: Int get() = agentContextMap.size 32 | 33 | // Map scrape_id to ChunkedContext 34 | val chunkedContextMap: ConcurrentMap = newConcurrentMap() 35 | val chunkedContextSize: Int get() = chunkedContextMap.size 36 | 37 | val totalAgentScrapeRequestBacklogSize: Int get() = agentContextMap.values.sumOf { it.scrapeRequestBacklogSize } 38 | 39 | fun addAgentContext(agentContext: AgentContext): AgentContext? { 40 | logger.info { "Registering agentId: ${agentContext.agentId}" } 41 | return agentContextMap.put(agentContext.agentId, agentContext) 42 | } 43 | 44 | fun getAgentContext(agentId: String) = agentContextMap[agentId] 45 | 46 | fun removeFromContextManager( 47 | agentId: String, 48 | reason: String, 49 | ): AgentContext? = 50 | agentContextMap.remove(agentId) 51 | .let { agentContext -> 52 | if (agentContext.isNull()) { 53 | logger.warn { "Missing AgentContext for agentId: $agentId ($reason)" } 54 | } else { 55 | if (!isTestMode) 56 | logger.info { "Removed $agentContext for agentId: $agentId ($reason)" } 57 | agentContext.invalidate() 58 | } 59 | agentContext 60 | } 61 | 62 | companion object { 63 | private val logger = KotlinLogging.logger {} 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ChunkedContext.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import io.prometheus.common.ScrapeResults 22 | import io.prometheus.grpc.ChunkedScrapeResponse 23 | import java.io.ByteArrayOutputStream 24 | import java.util.zip.CRC32 25 | 26 | internal class ChunkedContext( 27 | response: ChunkedScrapeResponse, 28 | ) { 29 | private val checksum = CRC32() 30 | private val baos = ByteArrayOutputStream() 31 | 32 | var totalChunkCount = 0 33 | private set 34 | var totalByteCount = 0 35 | private set 36 | 37 | val scrapeResults = 38 | response.header.run { 39 | ScrapeResults( 40 | validResponse = headerValidResponse, 41 | scrapeId = headerScrapeId, 42 | agentId = headerAgentId, 43 | statusCode = headerStatusCode, 44 | zipped = true, 45 | failureReason = headerFailureReason, 46 | url = headerUrl, 47 | contentType = headerContentType, 48 | ) 49 | } 50 | 51 | fun applyChunk( 52 | data: ByteArray, 53 | chunkByteCount: Int, 54 | chunkCount: Int, 55 | chunkChecksum: Long, 56 | ) { 57 | totalChunkCount++ 58 | totalByteCount += chunkByteCount 59 | checksum.update(data, 0, data.size) 60 | baos.write(data, 0, chunkByteCount) 61 | 62 | check(totalChunkCount == chunkCount) 63 | check(checksum.value == chunkChecksum) 64 | } 65 | 66 | fun applySummary( 67 | summaryChunkCount: Int, 68 | summaryByteCount: Int, 69 | summaryChecksum: Long, 70 | ) { 71 | check(totalChunkCount == summaryChunkCount) 72 | check(totalByteCount == summaryByteCount) 73 | check(checksum.value == summaryChecksum) 74 | 75 | baos.flush() 76 | scrapeResults.contentAsZipped = baos.toByteArray() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ProxyConstants.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.prometheus.proxy 18 | 19 | object ProxyConstants { 20 | const val MISSING_PATH_MSG = "Request missing path" 21 | const val CACHE_CONTROL_VALUE = "must-revalidate,no-store" 22 | const val FAVICON_FILENAME = "favicon.ico" 23 | } 24 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ProxyGrpcService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import brave.grpc.GrpcTracing 22 | import com.codahale.metrics.health.HealthCheck 23 | import com.github.pambrose.common.concurrent.GenericIdleService 24 | import com.github.pambrose.common.concurrent.genericServiceListener 25 | import com.github.pambrose.common.dsl.GrpcDsl.server 26 | import com.github.pambrose.common.dsl.GuavaDsl.toStringElements 27 | import com.github.pambrose.common.dsl.MetricsDsl.healthCheck 28 | import com.github.pambrose.common.utils.TlsContext 29 | import com.github.pambrose.common.utils.TlsContext.Companion.PLAINTEXT_CONTEXT 30 | import com.github.pambrose.common.utils.TlsUtils.buildServerTlsContext 31 | import com.github.pambrose.common.utils.shutdownGracefully 32 | import com.github.pambrose.common.utils.shutdownWithJvm 33 | import com.google.common.util.concurrent.MoreExecutors 34 | import io.github.oshai.kotlinlogging.KotlinLogging 35 | import io.grpc.Server 36 | import io.grpc.ServerInterceptor 37 | import io.grpc.ServerInterceptors 38 | import io.grpc.protobuf.services.ProtoReflectionServiceV1 39 | import io.prometheus.Proxy 40 | import java.util.concurrent.TimeUnit.SECONDS 41 | import kotlin.time.Duration.Companion.seconds 42 | 43 | internal class ProxyGrpcService( 44 | private val proxy: Proxy, 45 | private val port: Int = -1, 46 | private val inProcessName: String = "", 47 | ) : GenericIdleService() { 48 | private val tracing by lazy { proxy.zipkinReporterService.newTracing("grpc_server") } 49 | private val grpcTracing by lazy { GrpcTracing.create(tracing) } 50 | private val grpcServer: Server 51 | val healthCheck: HealthCheck 52 | 53 | init { 54 | val options = proxy.options 55 | val tlsContext = 56 | if (options.certChainFilePath.isNotEmpty() || options.privateKeyFilePath.isNotEmpty()) 57 | buildServerTlsContext( 58 | certChainFilePath = options.certChainFilePath, 59 | privateKeyFilePath = options.privateKeyFilePath, 60 | trustCertCollectionFilePath = options.trustCertCollectionFilePath, 61 | ) 62 | else 63 | PLAINTEXT_CONTEXT 64 | 65 | grpcServer = createGrpcServer(tlsContext, options) 66 | grpcServer.shutdownWithJvm(2.seconds) 67 | addListener(genericServiceListener(logger), MoreExecutors.directExecutor()) 68 | 69 | healthCheck = healthCheck { 70 | if (grpcServer.isShutdown || grpcServer.isTerminated) 71 | HealthCheck.Result.unhealthy("gRPC server is not running") 72 | else 73 | HealthCheck.Result.healthy() 74 | } 75 | } 76 | 77 | private fun createGrpcServer( 78 | tlsContext: TlsContext, 79 | options: ProxyOptions, 80 | ): Server = 81 | server( 82 | port = port, 83 | tlsContext = tlsContext, 84 | inProcessServerName = inProcessName, 85 | ) { 86 | val proxyService = ProxyServiceImpl(proxy) 87 | val interceptors: List = 88 | buildList { 89 | if (!options.transportFilterDisabled) 90 | add(ProxyServerInterceptor()) 91 | if (proxy.isZipkinEnabled) 92 | add(grpcTracing.newServerInterceptor()) 93 | } 94 | 95 | addService(ServerInterceptors.intercept(proxyService.bindService(), interceptors)) 96 | 97 | if (!options.transportFilterDisabled) 98 | addTransportFilter(ProxyServerTransportFilter(proxy)) 99 | 100 | if (!options.reflectionDisabled) 101 | addService(ProtoReflectionServiceV1.newInstance()) 102 | 103 | if (options.handshakeTimeoutSecs > -1L) 104 | handshakeTimeout(options.handshakeTimeoutSecs, SECONDS) 105 | 106 | if (options.keepAliveTimeSecs > -1L) 107 | keepAliveTime(options.keepAliveTimeSecs, SECONDS) 108 | 109 | if (options.keepAliveTimeoutSecs > -1L) 110 | keepAliveTimeout(options.keepAliveTimeoutSecs, SECONDS) 111 | 112 | if (options.permitKeepAliveWithoutCalls) 113 | permitKeepAliveWithoutCalls(options.permitKeepAliveWithoutCalls) 114 | 115 | if (options.permitKeepAliveTimeSecs > -1L) 116 | permitKeepAliveTime(options.permitKeepAliveTimeSecs, SECONDS) 117 | 118 | if (options.maxConnectionIdleSecs > -1L) 119 | maxConnectionIdle(options.maxConnectionIdleSecs, SECONDS) 120 | 121 | if (options.maxConnectionAgeSecs > -1L) 122 | maxConnectionAge(options.maxConnectionAgeSecs, SECONDS) 123 | 124 | if (options.maxConnectionAgeGraceSecs > -1L) 125 | maxConnectionAgeGrace(options.maxConnectionAgeGraceSecs, SECONDS) 126 | } 127 | 128 | override fun startUp() { 129 | grpcServer.start() 130 | } 131 | 132 | override fun shutDown() { 133 | if (proxy.isZipkinEnabled) 134 | tracing.close() 135 | grpcServer.shutdownGracefully(2.seconds) 136 | } 137 | 138 | override fun toString() = 139 | toStringElements { 140 | if (inProcessName.isNotEmpty()) { 141 | add("serverType", "InProcess") 142 | add("serverName", inProcessName) 143 | } else { 144 | add("serverType", "Netty") 145 | add("port", port) 146 | } 147 | } 148 | 149 | companion object { 150 | private val logger = KotlinLogging.logger {} 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ProxyHttpConfig.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.prometheus.proxy 18 | 19 | import com.github.pambrose.common.util.simpleClassName 20 | import io.github.oshai.kotlinlogging.KotlinLogging 21 | import io.ktor.http.ContentType.Text 22 | import io.ktor.http.HttpHeaders 23 | import io.ktor.http.HttpStatusCode 24 | import io.ktor.http.HttpStatusCode.Companion.NotFound 25 | import io.ktor.http.content.TextContent 26 | import io.ktor.http.withCharset 27 | import io.ktor.server.application.Application 28 | import io.ktor.server.application.ApplicationCall 29 | import io.ktor.server.application.install 30 | import io.ktor.server.logging.toLogString 31 | import io.ktor.server.plugins.calllogging.CallLogging 32 | import io.ktor.server.plugins.calllogging.CallLoggingConfig 33 | import io.ktor.server.plugins.compression.Compression 34 | import io.ktor.server.plugins.compression.CompressionConfig 35 | import io.ktor.server.plugins.compression.deflate 36 | import io.ktor.server.plugins.compression.gzip 37 | import io.ktor.server.plugins.compression.minimumSize 38 | import io.ktor.server.plugins.defaultheaders.DefaultHeaders 39 | import io.ktor.server.plugins.origin 40 | import io.ktor.server.plugins.statuspages.StatusPages 41 | import io.ktor.server.plugins.statuspages.StatusPagesConfig 42 | import io.ktor.server.request.path 43 | import io.ktor.server.response.respond 44 | import io.prometheus.Proxy 45 | import org.slf4j.event.Level 46 | 47 | internal object ProxyHttpConfig { 48 | private val logger = KotlinLogging.logger {} 49 | 50 | fun Application.configureKtorServer( 51 | proxy: Proxy, 52 | isTestMode: Boolean, 53 | ) { 54 | install(DefaultHeaders) { 55 | header("X-Engine", "Ktor") 56 | } 57 | 58 | if (!isTestMode && proxy.options.configVals.proxy.http.requestLoggingEnabled) 59 | install(CallLogging) { 60 | configureCallLogging() 61 | } 62 | 63 | install(Compression) { 64 | configureCompression() 65 | } 66 | 67 | install(StatusPages) { 68 | configureStatusPages() 69 | } 70 | } 71 | 72 | private fun CallLoggingConfig.configureCallLogging() { 73 | level = Level.INFO 74 | filter { call -> call.request.path().startsWith("/") } 75 | format { call -> getFormattedLog(call) } 76 | } 77 | 78 | private fun getFormattedLog(call: ApplicationCall) = 79 | with(call) { 80 | when (val status = response.status()) { 81 | HttpStatusCode.Found -> { 82 | val logMsg = request.toLogString() 83 | "$status: $logMsg -> ${response.headers[HttpHeaders.Location]} - ${request.origin.remoteHost}" 84 | } 85 | 86 | else -> "$status: ${request.toLogString()} - ${request.origin.remoteHost}" 87 | } 88 | } 89 | 90 | private fun CompressionConfig.configureCompression() { 91 | gzip { 92 | priority = 1.0 93 | } 94 | deflate { 95 | priority = 10.0 96 | minimumSize(1024) // condition 97 | } 98 | } 99 | 100 | private fun StatusPagesConfig.configureStatusPages() { 101 | // Catch all 102 | exception { call, cause -> 103 | logger.info(cause) { " Throwable caught: ${cause.simpleClassName}" } 104 | call.respond(NotFound) 105 | } 106 | 107 | status(NotFound) { call, cause -> 108 | call.respond(TextContent("${cause.value} ${cause.description}", Text.Plain.withCharset(Charsets.UTF_8), cause)) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ProxyHttpService.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction", "UnstableApiUsage") 18 | 19 | package io.prometheus.proxy 20 | 21 | import com.github.pambrose.common.concurrent.GenericIdleService 22 | import com.github.pambrose.common.concurrent.genericServiceListener 23 | import com.github.pambrose.common.dsl.GuavaDsl.toStringElements 24 | import com.github.pambrose.common.util.sleep 25 | import com.google.common.util.concurrent.MoreExecutors 26 | import io.github.oshai.kotlinlogging.KotlinLogging 27 | import io.ktor.server.cio.CIO 28 | import io.ktor.server.cio.CIOApplicationEngine.Configuration 29 | import io.ktor.server.engine.connector 30 | import io.ktor.server.engine.embeddedServer 31 | import io.prometheus.Proxy 32 | import io.prometheus.common.Utils.lambda 33 | import io.prometheus.proxy.ProxyHttpConfig.configureKtorServer 34 | import io.prometheus.proxy.ProxyHttpRoutes.configureHttpRoutes 35 | import kotlin.time.Duration.Companion.seconds 36 | import kotlin.time.DurationUnit.SECONDS 37 | 38 | internal class ProxyHttpService( 39 | private val proxy: Proxy, 40 | val httpPort: Int, 41 | isTestMode: Boolean, 42 | ) : GenericIdleService() { 43 | private val idleTimeout = 44 | with(proxy.proxyConfigVals.http) { (if (idleTimeoutSecs == -1) 45 else idleTimeoutSecs).seconds } 45 | 46 | private val tracing by lazy { proxy.zipkinReporterService.newTracing("proxy-http") } 47 | 48 | private fun getConfig(httpPort: Int): Configuration.() -> Unit = 49 | lambda { 50 | connector { 51 | host = "0.0.0.0" 52 | port = httpPort 53 | } 54 | connectionIdleTimeoutSeconds = idleTimeout.toInt(SECONDS) 55 | } 56 | 57 | private val httpServer = 58 | embeddedServer(factory = CIO, configure = getConfig(httpPort)) { 59 | configureKtorServer(proxy, isTestMode) 60 | configureHttpRoutes(proxy) 61 | } 62 | 63 | init { 64 | addListener(genericServiceListener(logger), MoreExecutors.directExecutor()) 65 | } 66 | 67 | override fun startUp() { 68 | httpServer.start() 69 | } 70 | 71 | override fun shutDown() { 72 | if (proxy.isZipkinEnabled) 73 | tracing.close() 74 | httpServer.stop(5.seconds.inWholeMilliseconds, 5.seconds.inWholeMilliseconds) 75 | sleep(2.seconds) 76 | } 77 | 78 | override fun toString() = toStringElements { add("port", httpPort) } 79 | 80 | companion object { 81 | private val logger = KotlinLogging.logger {} 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ProxyMetrics.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import com.github.pambrose.common.dsl.PrometheusDsl.counter 22 | import com.github.pambrose.common.dsl.PrometheusDsl.gauge 23 | import com.github.pambrose.common.dsl.PrometheusDsl.summary 24 | import com.github.pambrose.common.metrics.SamplerGaugeCollector 25 | import io.prometheus.Proxy 26 | import io.prometheus.common.Utils.lambda 27 | 28 | internal class ProxyMetrics( 29 | proxy: Proxy, 30 | ) { 31 | val scrapeRequestCount = 32 | counter { 33 | name("proxy_scrape_requests") 34 | help("Proxy scrape requests") 35 | labelNames("type") 36 | } 37 | 38 | val connectCount = 39 | counter { 40 | name("proxy_connect_count") 41 | help("Proxy connect count") 42 | } 43 | 44 | val agentEvictionCount = 45 | counter { 46 | name("proxy_eviction_count") 47 | help("Proxy eviction count") 48 | } 49 | 50 | val heartbeatCount = 51 | counter { 52 | name("proxy_heartbeat_count") 53 | help("Proxy heartbeat count") 54 | } 55 | 56 | val scrapeRequestLatency = 57 | summary { 58 | name("proxy_scrape_request_latency_seconds") 59 | help("Proxy scrape request latency in seconds") 60 | } 61 | 62 | init { 63 | gauge { 64 | name("proxy_start_time_seconds") 65 | help("Proxy start time in seconds") 66 | }.setToCurrentTime() 67 | 68 | SamplerGaugeCollector( 69 | name = "proxy_agent_map_size", 70 | help = "Proxy connected agents", 71 | data = lambda { proxy.agentContextManager.agentContextSize.toDouble() }, 72 | ) 73 | 74 | SamplerGaugeCollector( 75 | name = "proxy_chunk_context_map_size", 76 | help = "Proxy chunk context map size", 77 | data = lambda { proxy.agentContextManager.chunkedContextSize.toDouble() }, 78 | ) 79 | 80 | SamplerGaugeCollector( 81 | name = "proxy_path_map_size", 82 | help = "Proxy path map size", 83 | data = lambda { proxy.pathManager.pathMapSize.toDouble() }, 84 | ) 85 | 86 | SamplerGaugeCollector( 87 | name = "proxy_scrape_map_size", 88 | help = "Proxy scrape map size", 89 | data = lambda { proxy.scrapeRequestManager.scrapeMapSize.toDouble() }, 90 | ) 91 | 92 | SamplerGaugeCollector( 93 | name = "proxy_cumulative_agent_backlog_size", 94 | help = "Proxy cumulative agent backlog size", 95 | data = lambda { proxy.agentContextManager.totalAgentScrapeRequestBacklogSize.toDouble() }, 96 | ) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ProxyOptions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import com.beust.jcommander.Parameter 22 | import io.github.oshai.kotlinlogging.KotlinLogging 23 | import io.prometheus.Proxy 24 | import io.prometheus.common.BaseOptions 25 | import io.prometheus.common.EnvVars.AGENT_PORT 26 | import io.prometheus.common.EnvVars.HANDSHAKE_TIMEOUT_SECS 27 | import io.prometheus.common.EnvVars.MAX_CONNECTION_AGE_GRACE_SECS 28 | import io.prometheus.common.EnvVars.MAX_CONNECTION_AGE_SECS 29 | import io.prometheus.common.EnvVars.MAX_CONNECTION_IDLE_SECS 30 | import io.prometheus.common.EnvVars.PERMIT_KEEPALIVE_TIME_SECS 31 | import io.prometheus.common.EnvVars.PERMIT_KEEPALIVE_WITHOUT_CALLS 32 | import io.prometheus.common.EnvVars.PROXY_CONFIG 33 | import io.prometheus.common.EnvVars.PROXY_PORT 34 | import io.prometheus.common.EnvVars.REFLECTION_DISABLED 35 | import io.prometheus.common.EnvVars.SD_ENABLED 36 | import io.prometheus.common.EnvVars.SD_PATH 37 | import io.prometheus.common.EnvVars.SD_TARGET_PREFIX 38 | 39 | class ProxyOptions( 40 | argv: Array, 41 | ) : BaseOptions(Proxy::class.java.simpleName, argv, PROXY_CONFIG.name) { 42 | constructor(args: List) : this(args.toTypedArray()) 43 | 44 | @Parameter(names = ["-p", "--port"], description = "Proxy listen port") 45 | var proxyHttpPort = -1 46 | private set 47 | 48 | @Parameter(names = ["-a", "--agent_port"], description = "gRPC listen port for Agents") 49 | var proxyAgentPort = -1 50 | private set 51 | 52 | @Parameter(names = ["--sd_enabled"], description = "Service discovery endpoint enabled") 53 | var sdEnabled = false 54 | private set 55 | 56 | @Parameter(names = ["--sd_path"], description = "Service discovery endpoint path") 57 | var sdPath = "" 58 | private set 59 | 60 | @Parameter(names = ["--sd_target_prefix"], description = "Service discovery target prefix") 61 | var sdTargetPrefix = "" 62 | private set 63 | 64 | @Parameter(names = ["--ref-disabled"], description = "gRPC Reflection disabled") 65 | var reflectionDisabled = false 66 | private set 67 | 68 | @Parameter(names = ["--handshake_timeout_secs"], description = "gRPC Handshake timeout (secs)") 69 | var handshakeTimeoutSecs = -1L 70 | private set 71 | 72 | @Parameter(names = ["--permit_keepalive_without_calls"], description = "gRPC Permit KeepAlive without calls") 73 | var permitKeepAliveWithoutCalls = false 74 | private set 75 | 76 | @Parameter(names = ["--permit_keepalive_time_secs"], description = "gRPC Permit KeepAlive time (secs)") 77 | var permitKeepAliveTimeSecs = -1L 78 | private set 79 | 80 | @Parameter(names = ["--max_connection_idle_secs"], description = "gRPC Max connection idle (secs)") 81 | var maxConnectionIdleSecs = -1L 82 | private set 83 | 84 | @Parameter(names = ["--max_connection_age_secs"], description = "gRPC Max connection age (secs)") 85 | var maxConnectionAgeSecs = -1L 86 | private set 87 | 88 | @Parameter(names = ["--max_connection_age_grace_secs"], description = "gRPC Max connection age grace (secs)") 89 | var maxConnectionAgeGraceSecs = -1L 90 | private set 91 | 92 | init { 93 | parseOptions() 94 | } 95 | 96 | override fun assignConfigVals() { 97 | configVals.proxy 98 | .also { proxyConfigVals -> 99 | if (proxyHttpPort == -1) 100 | proxyHttpPort = PROXY_PORT.getEnv(proxyConfigVals.http.port) 101 | logger.info { "proxyHttpPort: $proxyHttpPort" } 102 | 103 | if (proxyAgentPort == -1) 104 | proxyAgentPort = AGENT_PORT.getEnv(proxyConfigVals.agent.port) 105 | logger.info { "proxyAgentPort: $proxyAgentPort" } 106 | 107 | if (!sdEnabled) 108 | sdEnabled = SD_ENABLED.getEnv(proxyConfigVals.service.discovery.enabled) 109 | logger.info { "sdEnabled: $sdEnabled" } 110 | 111 | if (sdPath.isEmpty()) 112 | sdPath = SD_PATH.getEnv(proxyConfigVals.service.discovery.path) 113 | if (sdEnabled) 114 | require(sdPath.isNotEmpty()) { "sdPath is empty" } 115 | else 116 | logger.info { "sdPath: $sdPath" } 117 | 118 | if (sdTargetPrefix.isEmpty()) 119 | sdTargetPrefix = SD_TARGET_PREFIX.getEnv(proxyConfigVals.service.discovery.targetPrefix) 120 | if (sdEnabled) 121 | require(sdTargetPrefix.isNotEmpty()) { "sdTargetPrefix is empty" } 122 | else 123 | logger.info { "sdTargetPrefix: $sdTargetPrefix" } 124 | 125 | if (!reflectionDisabled) 126 | reflectionDisabled = REFLECTION_DISABLED.getEnv(proxyConfigVals.reflectionDisabled) 127 | logger.info { "reflectionDisabled: $reflectionDisabled" } 128 | 129 | if (handshakeTimeoutSecs == -1L) 130 | handshakeTimeoutSecs = HANDSHAKE_TIMEOUT_SECS.getEnv(proxyConfigVals.grpc.handshakeTimeoutSecs) 131 | val hsTimeout = if (handshakeTimeoutSecs == -1L) "default (120)" else handshakeTimeoutSecs 132 | logger.info { "grpc.handshakeTimeoutSecs: $hsTimeout" } 133 | 134 | if (!permitKeepAliveWithoutCalls) 135 | permitKeepAliveWithoutCalls = 136 | PERMIT_KEEPALIVE_WITHOUT_CALLS.getEnv(proxyConfigVals.grpc.permitKeepAliveWithoutCalls) 137 | logger.info { "grpc.permitKeepAliveWithoutCalls: $permitKeepAliveWithoutCalls" } 138 | 139 | if (permitKeepAliveTimeSecs == -1L) 140 | permitKeepAliveTimeSecs = PERMIT_KEEPALIVE_TIME_SECS.getEnv(proxyConfigVals.grpc.permitKeepAliveTimeSecs) 141 | val kaTime = if (permitKeepAliveTimeSecs == -1L) "default (300)" else permitKeepAliveTimeSecs 142 | logger.info { "grpc.permitKeepAliveTimeSecs: $kaTime" } 143 | 144 | if (maxConnectionIdleSecs == -1L) 145 | maxConnectionIdleSecs = MAX_CONNECTION_IDLE_SECS.getEnv(proxyConfigVals.grpc.maxConnectionIdleSecs) 146 | val idleVal = if (maxConnectionIdleSecs == -1L) "default (INT_MAX)" else maxConnectionIdleSecs 147 | logger.info { "grpc.maxConnectionIdleSecs: $idleVal" } 148 | 149 | if (maxConnectionAgeSecs == -1L) 150 | maxConnectionAgeSecs = MAX_CONNECTION_AGE_SECS.getEnv(proxyConfigVals.grpc.maxConnectionAgeSecs) 151 | val ageVal = if (maxConnectionAgeSecs == -1L) "default (INT_MAX)" else maxConnectionAgeSecs 152 | logger.info { "grpc.maxConnectionAgeSecs: $ageVal" } 153 | 154 | if (maxConnectionAgeGraceSecs == -1L) 155 | maxConnectionAgeGraceSecs = 156 | MAX_CONNECTION_AGE_GRACE_SECS.getEnv(proxyConfigVals.grpc.maxConnectionAgeGraceSecs) 157 | val graceVal = if (maxConnectionAgeGraceSecs == -1L) "default (INT_MAX)" else maxConnectionAgeGraceSecs 158 | logger.info { "grpc.maxConnectionAgeGraceSecs: $graceVal" } 159 | 160 | with(proxyConfigVals) { 161 | assignKeepAliveTimeSecs(grpc.keepAliveTimeSecs) 162 | assignKeepAliveTimeoutSecs(grpc.keepAliveTimeoutSecs) 163 | assignAdminEnabled(admin.enabled) 164 | assignAdminPort(admin.port) 165 | assignMetricsEnabled(metrics.enabled) 166 | assignMetricsPort(metrics.port) 167 | assignTransportFilterDisabled(transportFilterDisabled) 168 | assignDebugEnabled(admin.debugEnabled) 169 | 170 | assignCertChainFilePath(tls.certChainFilePath) 171 | assignPrivateKeyFilePath(tls.privateKeyFilePath) 172 | assignTrustCertCollectionFilePath(tls.trustCertCollectionFilePath) 173 | 174 | logger.info { "internal.scrapeRequestTimeoutSecs: ${internal.scrapeRequestTimeoutSecs}" } 175 | logger.info { "internal.staleAgentCheckPauseSecs: ${internal.staleAgentCheckPauseSecs}" } 176 | logger.info { "internal.maxAgentInactivitySecs: ${internal.maxAgentInactivitySecs}" } 177 | } 178 | } 179 | } 180 | 181 | companion object { 182 | private val logger = KotlinLogging.logger {} 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ProxyPathManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import com.github.pambrose.common.util.isNotNull 22 | import com.github.pambrose.common.util.isNull 23 | import com.google.common.collect.Maps.newConcurrentMap 24 | import io.github.oshai.kotlinlogging.KotlinLogging 25 | import io.prometheus.Proxy 26 | import io.prometheus.common.Messages.EMPTY_AGENT_ID_MSG 27 | import io.prometheus.common.Messages.EMPTY_PATH_MSG 28 | import io.prometheus.grpc.UnregisterPathResponse 29 | 30 | internal class ProxyPathManager( 31 | private val proxy: Proxy, 32 | private val isTestMode: Boolean, 33 | ) { 34 | class AgentContextInfo( 35 | val isConsolidated: Boolean, 36 | val labels: String, 37 | val agentContexts: MutableList, 38 | ) { 39 | fun isNotValid() = !isConsolidated && agentContexts[0].isNotValid() 40 | 41 | override fun toString(): String = 42 | "AgentContextInfo(consolidated=$isConsolidated, labels=$labels,agentContexts=$agentContexts)" 43 | } 44 | 45 | private val pathMap = newConcurrentMap() 46 | 47 | fun getAgentContextInfo(path: String) = pathMap[path] 48 | 49 | val pathMapSize: Int 50 | get() = pathMap.size 51 | 52 | val allPaths: List 53 | get() = synchronized(pathMap) { 54 | return pathMap.keys.toList() 55 | } 56 | 57 | fun addPath( 58 | path: String, 59 | labels: String, 60 | agentContext: AgentContext, 61 | ) { 62 | require(path.isNotEmpty()) { EMPTY_PATH_MSG } 63 | 64 | synchronized(pathMap) { 65 | val agentInfo = pathMap[path] 66 | if (agentContext.consolidated) { 67 | if (agentInfo.isNull()) { 68 | pathMap[path] = AgentContextInfo(true, labels, mutableListOf(agentContext)) 69 | } else { 70 | if (agentContext.consolidated != agentInfo.isConsolidated) 71 | logger.warn { 72 | "Mismatch of agent context types: ${agentContext.consolidated} and ${agentInfo.isConsolidated}" 73 | } 74 | else 75 | agentInfo.agentContexts += agentContext 76 | } 77 | } else { 78 | if (agentInfo.isNotNull()) logger.info { "Overwriting path /$path for ${agentInfo.agentContexts[0]}" } 79 | pathMap[path] = AgentContextInfo(false, labels, mutableListOf(agentContext)) 80 | } 81 | 82 | if (!isTestMode) logger.info { "Added path /$path for $agentContext" } 83 | } 84 | } 85 | 86 | fun removePath( 87 | path: String, 88 | agentId: String, 89 | ): UnregisterPathResponse { 90 | require(path.isNotEmpty()) { EMPTY_PATH_MSG } 91 | require(agentId.isNotEmpty()) { EMPTY_AGENT_ID_MSG } 92 | 93 | synchronized(pathMap) { 94 | val agentInfo = pathMap[path] 95 | val results = 96 | if (agentInfo.isNull()) { 97 | val msg = "Unable to remove path /$path - path not found" 98 | logger.error { msg } 99 | false to msg 100 | } else { 101 | val agentContext = agentInfo.agentContexts.firstOrNull { it.agentId == agentId } 102 | if (agentContext.isNull()) { 103 | val agentIds = agentInfo.agentContexts.joinToString(", ") { it.agentId } 104 | val msg = "Unable to remove path /$path - invalid agentId: $agentId -- [$agentIds]" 105 | logger.error { msg } 106 | false to msg 107 | } else { 108 | if (agentInfo.isConsolidated && agentInfo.agentContexts.size > 1) { 109 | agentInfo.agentContexts.remove(agentContext) 110 | if (!isTestMode) 111 | logger.info { "Removed element of path /$path for $agentInfo" } 112 | } else { 113 | pathMap.remove(path) 114 | if (!isTestMode) 115 | logger.info { "Removed path /$path for $agentInfo" } 116 | } 117 | true to "" 118 | } 119 | } 120 | return UnregisterPathResponse 121 | .newBuilder() 122 | .also { 123 | it.valid = results.first 124 | it.reason = results.second 125 | } 126 | .build() 127 | } 128 | } 129 | 130 | // This is called on agent disconnects 131 | fun removeFromPathManager( 132 | agentId: String, 133 | reason: String, 134 | ) { 135 | require(agentId.isNotEmpty()) { EMPTY_AGENT_ID_MSG } 136 | 137 | val agentContext = proxy.agentContextManager.getAgentContext(agentId) 138 | if (agentContext.isNull()) { 139 | logger.warn { "Missing agent context for agentId: $agentId ($reason)" } 140 | } else { 141 | logger.info { "Removing paths for agentId: $agentId ($reason)" } 142 | 143 | synchronized(pathMap) { 144 | pathMap.forEach { (k, v) -> 145 | if (v.agentContexts.size == 1) { 146 | if (v.agentContexts[0].agentId == agentId) 147 | pathMap.remove(k) 148 | ?.also { 149 | if (!isTestMode) 150 | logger.info { "Removed path /$k for $it" } 151 | } ?: logger.warn { "Missing ${agentContext.desc}path /$k for agentId: $agentId" } 152 | } else { 153 | val removed = v.agentContexts.removeIf { it.agentId == agentId } 154 | if (removed) 155 | logger.info { "Removed path /$k for $agentContext" } 156 | else 157 | logger.warn { "Missing path /$k for agentId: $agentId" } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | 164 | fun toPlainText() = 165 | if (pathMap.isEmpty()) { 166 | "No agents connected." 167 | } else { 168 | val maxPath = pathMap.keys.maxOfOrNull { it.length } ?: 0 169 | "Proxy Path Map:\n" + "Path".padEnd(maxPath + 2) + "Agent Context\n" + 170 | pathMap 171 | .toSortedMap() 172 | .map { c -> "/${c.key.padEnd(maxPath)} ${c.value.agentContexts.size} ${c.value}" } 173 | .joinToString("\n\n") 174 | } 175 | 176 | companion object { 177 | private val logger = KotlinLogging.logger {} 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ProxyServerInterceptor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import io.grpc.ForwardingServerCall 22 | import io.grpc.Metadata 23 | import io.grpc.Metadata.ASCII_STRING_MARSHALLER 24 | import io.grpc.ServerCall 25 | import io.grpc.ServerCallHandler 26 | import io.grpc.ServerInterceptor 27 | import io.prometheus.proxy.ProxyServerTransportFilter.Companion.AGENT_ID 28 | import io.prometheus.proxy.ProxyServerTransportFilter.Companion.AGENT_ID_KEY 29 | 30 | internal class ProxyServerInterceptor : ServerInterceptor { 31 | override fun interceptCall( 32 | call: ServerCall, 33 | requestHeaders: Metadata, 34 | handler: ServerCallHandler, 35 | ): ServerCall.Listener = 36 | handler.startCall( 37 | object : ForwardingServerCall.SimpleForwardingServerCall(call) { 38 | override fun sendHeaders(headers: Metadata) { 39 | // ATTRIB_AGENT_ID was assigned in ServerTransportFilter 40 | call.attributes.get(AGENT_ID_KEY)?.also { headers.put(META_AGENT_ID_KEY, it) } 41 | super.sendHeaders(headers) 42 | } 43 | }, 44 | requestHeaders, 45 | ) 46 | 47 | companion object { 48 | internal val META_AGENT_ID_KEY = Metadata.Key.of(AGENT_ID, ASCII_STRING_MARSHALLER) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ProxyServerTransportFilter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import com.github.pambrose.common.dsl.GrpcDsl.attributes 22 | import com.github.pambrose.common.util.isNotNull 23 | import io.github.oshai.kotlinlogging.KotlinLogging 24 | import io.grpc.Attributes 25 | import io.grpc.ServerTransportFilter 26 | import io.prometheus.Proxy 27 | import io.prometheus.proxy.ProxyServiceImpl.Companion.UNKNOWN_ADDRESS 28 | 29 | internal class ProxyServerTransportFilter( 30 | private val proxy: Proxy, 31 | ) : ServerTransportFilter() { 32 | override fun transportReady(attributes: Attributes): Attributes { 33 | val remoteAddress = attributes.get(REMOTE_ADDR_KEY)?.toString() ?: UNKNOWN_ADDRESS 34 | val agentContext = AgentContext(remoteAddress) 35 | proxy.agentContextManager.addAgentContext(agentContext) 36 | 37 | return attributes { 38 | set(AGENT_ID_KEY, agentContext.agentId) 39 | setAll(attributes) 40 | } 41 | } 42 | 43 | override fun transportTerminated(attributes: Attributes) { 44 | attributes.get(AGENT_ID_KEY)?.also { agentId -> 45 | val context = proxy.removeAgentContext(agentId, "Termination") 46 | logger.info { "Disconnected ${if (context.isNotNull()) "from $context" else "with invalid agentId: $agentId"}" } 47 | } ?: logger.error { "Missing agentId in transportTerminated()" } 48 | super.transportTerminated(attributes) 49 | } 50 | 51 | companion object { 52 | private val logger = KotlinLogging.logger {} 53 | internal const val AGENT_ID = "agent-id" 54 | private const val REMOTE_ADDR = "remote-addr" 55 | internal val AGENT_ID_KEY: Attributes.Key = Attributes.Key.create(AGENT_ID) 56 | private val REMOTE_ADDR_KEY: Attributes.Key = Attributes.Key.create(REMOTE_ADDR) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ProxyUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.prometheus.proxy 18 | 19 | import io.github.oshai.kotlinlogging.KLogger 20 | import io.ktor.http.ContentType 21 | import io.ktor.http.ContentType.Text 22 | import io.ktor.http.HttpHeaders 23 | import io.ktor.http.HttpStatusCode 24 | import io.ktor.http.withCharset 25 | import io.ktor.server.application.ApplicationCall 26 | import io.ktor.server.response.header 27 | import io.ktor.server.response.respondText 28 | import io.prometheus.Proxy 29 | import io.prometheus.proxy.ProxyConstants.CACHE_CONTROL_VALUE 30 | import io.prometheus.proxy.ProxyConstants.MISSING_PATH_MSG 31 | 32 | object ProxyUtils { 33 | fun invalidAgentContextResponse( 34 | path: String, 35 | proxy: Proxy, 36 | logger: KLogger, 37 | responseResults: ResponseResults, 38 | ) { 39 | updateResponse( 40 | message = "Invalid AgentContext for /$path", 41 | proxy = proxy, 42 | logger = logger, 43 | logLevel = KLogger::error, 44 | responseResults = responseResults, 45 | updateMsg = "invalid_agent_context", 46 | statusCode = HttpStatusCode.NotFound, 47 | ) 48 | } 49 | 50 | fun invalidPathResponse( 51 | path: String, 52 | proxy: Proxy, 53 | logger: KLogger, 54 | responseResults: ResponseResults, 55 | ) { 56 | updateResponse( 57 | message = "Invalid path request /$path", 58 | proxy = proxy, 59 | logger = logger, 60 | logLevel = KLogger::info, 61 | responseResults = responseResults, 62 | updateMsg = "invalid_path", 63 | statusCode = HttpStatusCode.NotFound, 64 | ) 65 | } 66 | 67 | fun emptyPathResponse( 68 | proxy: Proxy, 69 | logger: KLogger, 70 | responseResults: ResponseResults, 71 | ) { 72 | updateResponse( 73 | message = MISSING_PATH_MSG, 74 | proxy = proxy, 75 | logger = logger, 76 | logLevel = KLogger::info, 77 | responseResults = responseResults, 78 | updateMsg = "missing_path", 79 | statusCode = HttpStatusCode.NotFound, 80 | ) 81 | } 82 | 83 | fun proxyNotRunningResponse( 84 | logger: KLogger, 85 | responseResults: ResponseResults, 86 | ) { 87 | updateResponse( 88 | message = "Proxy stopped", 89 | proxy = null, 90 | logger = logger, 91 | logLevel = KLogger::error, 92 | responseResults = responseResults, 93 | updateMsg = "proxy_stopped", 94 | statusCode = HttpStatusCode.ServiceUnavailable, 95 | ) 96 | } 97 | 98 | private fun updateResponse( 99 | message: String, 100 | proxy: Proxy?, 101 | logger: KLogger, 102 | logLevel: (KLogger, () -> String) -> Unit, 103 | responseResults: ResponseResults, 104 | updateMsg: String, 105 | statusCode: HttpStatusCode, 106 | ) { 107 | proxy?.logActivity(message) 108 | logLevel(logger) { message } 109 | responseResults.apply { 110 | this.updateMsg = updateMsg 111 | this.statusCode = statusCode 112 | } 113 | } 114 | 115 | fun incrementScrapeRequestCount( 116 | proxy: Proxy, 117 | type: String, 118 | ) { 119 | if (type.isNotEmpty()) proxy.metrics { scrapeRequestCount.labels(type).inc() } 120 | } 121 | 122 | suspend fun ApplicationCall.respondWith( 123 | text: String, 124 | contentType: ContentType = Text.Plain.withCharset(Charsets.UTF_8), 125 | status: HttpStatusCode = HttpStatusCode.OK, 126 | ) { 127 | response.header(HttpHeaders.CacheControl, CACHE_CONTROL_VALUE) 128 | response.status(status) 129 | respondText(text, contentType, status) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ScrapeRequestManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import com.google.common.collect.Maps.newConcurrentMap 22 | import io.github.oshai.kotlinlogging.KotlinLogging 23 | import io.prometheus.common.ScrapeResults 24 | import java.util.concurrent.ConcurrentMap 25 | 26 | internal class ScrapeRequestManager { 27 | // Map scrape_id to agent_id 28 | val scrapeRequestMap: ConcurrentMap = newConcurrentMap() 29 | 30 | val scrapeMapSize: Int 31 | get() = scrapeRequestMap.size 32 | 33 | fun addToScrapeRequestMap(scrapeRequest: ScrapeRequestWrapper): ScrapeRequestWrapper? { 34 | val scrapeId = scrapeRequest.scrapeId 35 | logger.debug { "Adding scrapeId: $scrapeId to scrapeRequestMap" } 36 | return scrapeRequestMap.put(scrapeId, scrapeRequest) 37 | } 38 | 39 | fun assignScrapeResults(scrapeResults: ScrapeResults) { 40 | val scrapeId = scrapeResults.scrapeId 41 | scrapeRequestMap[scrapeId] 42 | ?.also { wrapper -> 43 | wrapper.scrapeResults = scrapeResults 44 | wrapper.markComplete() 45 | wrapper.agentContext.markActivityTime(true) 46 | } ?: logger.error { "Missing ScrapeRequestWrapper for scrape_id: $scrapeId" } 47 | } 48 | 49 | fun removeFromScrapeRequestMap(scrapeId: Long): ScrapeRequestWrapper? { 50 | logger.debug { "Removing scrapeId: $scrapeId from scrapeRequestMap" } 51 | return scrapeRequestMap.remove(scrapeId) 52 | } 53 | 54 | companion object { 55 | private val logger = KotlinLogging.logger {} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/kotlin/io/prometheus/proxy/ScrapeRequestWrapper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2024 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus.proxy 20 | 21 | import com.github.pambrose.common.delegate.AtomicDelegates.nonNullableReference 22 | import com.github.pambrose.common.dsl.GuavaDsl.toStringElements 23 | import com.github.pambrose.common.util.isNotNull 24 | import io.prometheus.Proxy 25 | import io.prometheus.common.Messages.EMPTY_AGENT_ID_MSG 26 | import io.prometheus.common.ScrapeResults 27 | import io.prometheus.grpc.ScrapeRequest 28 | import kotlinx.coroutines.channels.Channel 29 | import kotlinx.coroutines.withTimeoutOrNull 30 | import kotlin.concurrent.atomics.AtomicLong 31 | import kotlin.concurrent.atomics.fetchAndIncrement 32 | import kotlin.time.Duration 33 | import kotlin.time.TimeSource.Monotonic 34 | 35 | internal class ScrapeRequestWrapper( 36 | val agentContext: AgentContext, 37 | proxy: Proxy, 38 | path: String, 39 | encodedQueryParams: String, 40 | authHeader: String, 41 | accept: String?, 42 | debugEnabled: Boolean, 43 | ) { 44 | private val clock = Monotonic 45 | private val createTimeMark = clock.markNow() 46 | private val completeChannel = Channel() 47 | private val requestTimer = if (proxy.isMetricsEnabled) proxy.metrics.scrapeRequestLatency.startTimer() else null 48 | 49 | val scrapeRequest = 50 | ScrapeRequest 51 | .newBuilder() 52 | .also { 53 | require(agentContext.agentId.isNotEmpty()) { EMPTY_AGENT_ID_MSG } 54 | it.agentId = agentContext.agentId 55 | it.scrapeId = SCRAPE_ID_GENERATOR.fetchAndIncrement() 56 | it.path = path 57 | it.accept = accept.orEmpty() 58 | it.debugEnabled = debugEnabled 59 | it.encodedQueryParams = encodedQueryParams 60 | it.authHeader = authHeader 61 | } 62 | .build()!! 63 | 64 | var scrapeResults: ScrapeResults by nonNullableReference() 65 | 66 | val scrapeId: Long 67 | get() = scrapeRequest.scrapeId 68 | 69 | fun ageDuration() = createTimeMark.elapsedNow() 70 | 71 | fun markComplete() { 72 | requestTimer?.observeDuration() 73 | completeChannel.close() 74 | } 75 | 76 | suspend fun suspendUntilComplete(waitMillis: Duration) = 77 | withTimeoutOrNull(waitMillis.inWholeMilliseconds) { 78 | // completeChannel will eventually close and never get a value, or timeout 79 | runCatching { 80 | completeChannel.receive() 81 | true 82 | }.getOrElse { 83 | true 84 | } 85 | }.isNotNull() 86 | 87 | override fun toString() = 88 | toStringElements { 89 | add("scrapeId", scrapeRequest.scrapeId) 90 | add("path", scrapeRequest.path) 91 | } 92 | 93 | companion object { 94 | private val SCRAPE_ID_GENERATOR = AtomicLong(0L) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/proto/proxy_service.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import public "google/protobuf/empty.proto"; 4 | 5 | option java_multiple_files = true; 6 | option java_package = "io.prometheus.grpc"; 7 | 8 | message RegisterAgentRequest { 9 | string agent_id = 1; 10 | string launch_id = 2; 11 | string agent_name = 3; 12 | string host_name = 4; 13 | bool consolidated = 6; 14 | } 15 | 16 | message RegisterAgentResponse { 17 | bool valid = 1; 18 | string reason = 2; 19 | string agent_id = 3; 20 | string proxy_url = 4; 21 | } 22 | 23 | message RegisterPathRequest { 24 | string agent_id = 1; 25 | string path = 2; 26 | string labels = 3; 27 | } 28 | 29 | message RegisterPathResponse { 30 | bool valid = 1; 31 | string reason = 2; 32 | int32 path_count = 3; 33 | int64 path_id = 4; 34 | } 35 | 36 | message UnregisterPathRequest { 37 | string agent_id = 1; 38 | string path = 2; 39 | } 40 | 41 | message UnregisterPathResponse { 42 | bool valid = 1; 43 | string reason = 2; 44 | } 45 | 46 | message PathMapSizeRequest { 47 | string agent_id = 1; 48 | } 49 | 50 | message PathMapSizeResponse { 51 | int32 path_count = 1; 52 | } 53 | 54 | message AgentInfo { 55 | string agent_id = 1; 56 | } 57 | 58 | message ScrapeRequest { 59 | string agent_id = 1; 60 | int64 scrape_id = 2; 61 | string path = 3; 62 | string accept = 4; 63 | bool debug_enabled = 5; 64 | string encodedQueryParams = 6; 65 | string authHeader = 7; 66 | } 67 | 68 | message ScrapeResponse { 69 | bool valid_response = 1; 70 | string agent_id = 2; 71 | int64 scrape_id = 3; 72 | int32 status_code = 4; 73 | string failure_reason = 5; 74 | string url = 6; 75 | string content_type = 7; 76 | bool zipped = 8; 77 | oneof content_one_of { 78 | string content_as_text = 9; 79 | bytes content_as_zipped = 10; 80 | } 81 | } 82 | 83 | message ChunkedScrapeResponse { 84 | oneof chunk_one_of { 85 | // Changes to the field names meta, data, and summary are hard-coded in the impl code 86 | HeaderData header = 1; 87 | ChunkData chunk = 2; 88 | SummaryData summary = 3; 89 | } 90 | } 91 | 92 | message HeaderData { 93 | bool header_valid_response = 1; 94 | string header_agent_id = 2; 95 | int64 header_scrape_id = 3; 96 | int32 header_status_code = 4; 97 | string header_failure_reason = 5; 98 | string header_url = 6; 99 | string header_content_type = 7; 100 | } 101 | 102 | message ChunkData { 103 | int64 chunk_scrape_id = 1; 104 | int32 chunk_count = 2; 105 | int32 chunk_byte_count = 3; 106 | int64 chunk_checksum = 4; 107 | bytes chunk_bytes = 5; 108 | } 109 | 110 | message SummaryData { 111 | int64 summary_scrape_id = 1; 112 | int32 summary_chunk_count = 2; 113 | int32 summary_byte_count = 3; 114 | int64 summary_checksum = 4; 115 | } 116 | 117 | message HeartBeatRequest { 118 | string agent_id = 1; 119 | } 120 | 121 | message HeartBeatResponse { 122 | bool valid = 1; 123 | string reason = 2; 124 | } 125 | 126 | service ProxyService { 127 | rpc connectAgent (google.protobuf.Empty) returns (google.protobuf.Empty) { 128 | } 129 | 130 | rpc connectAgentWithTransportFilterDisabled (google.protobuf.Empty) returns (AgentInfo) { 131 | } 132 | 133 | rpc registerAgent (RegisterAgentRequest) returns (RegisterAgentResponse) { 134 | } 135 | 136 | rpc registerPath (RegisterPathRequest) returns (RegisterPathResponse) { 137 | } 138 | 139 | rpc unregisterPath (UnregisterPathRequest) returns (UnregisterPathResponse) { 140 | } 141 | 142 | rpc pathMapSize (PathMapSizeRequest) returns (PathMapSizeResponse) { 143 | } 144 | 145 | rpc readRequestsFromProxy (AgentInfo) returns (stream ScrapeRequest) { 146 | } 147 | 148 | rpc writeResponsesToProxy (stream ScrapeResponse) returns (google.protobuf.Empty) { 149 | } 150 | 151 | rpc writeChunkedResponsesToProxy (stream ChunkedScrapeResponse) returns (google.protobuf.Empty) { 152 | } 153 | 154 | rpc sendHeartBeat (HeartBeatRequest) returns (HeartBeatResponse) { 155 | } 156 | } 157 | 158 | -------------------------------------------------------------------------------- /src/main/resources/banners/README.txt: -------------------------------------------------------------------------------- 1 | Generated with: http://patorjk.com/software/taag/#p=display&h=0&f=Big%20Money-nw&t=Prometheus%0A%20%20%20%20%20Agent -------------------------------------------------------------------------------- /src/main/resources/banners/agent.txt: -------------------------------------------------------------------------------- 1 | $$$$$$$\ $$\ $$\ 2 | $$ __$$\ $$ | $$ | 3 | $$ | $$ | $$$$$$\ $$$$$$\ $$$$$$\$$$$\ $$$$$$\ $$$$$$\ $$$$$$$\ $$$$$$\ $$\ $$\ $$$$$$$\ 4 | $$$$$$$ |$$ __$$\ $$ __$$\ $$ _$$ _$$\ $$ __$$\ \_$$ _| $$ __$$\ $$ __$$\ $$ | $$ |$$ _____| 5 | $$ ____/ $$ | \__|$$ / $$ |$$ / $$ / $$ |$$$$$$$$ | $$ | $$ | $$ |$$$$$$$$ |$$ | $$ |\$$$$$$\ 6 | $$ | $$ | $$ | $$ |$$ | $$ | $$ |$$ ____| $$ |$$\ $$ | $$ |$$ ____|$$ | $$ | \____$$\ 7 | $$ | $$ | \$$$$$$ |$$ | $$ | $$ |\$$$$$$$\ \$$$$ |$$ | $$ |\$$$$$$$\ \$$$$$$ |$$$$$$$ | 8 | \__| \__| \______/ \__| \__| \__| \_______| \____/ \__| \__| \_______| \______/ \_______/ 9 | 10 | 11 | 12 | $$$$$$\ $$\ 13 | $$ __$$\ $$ | 14 | $$ / $$ | $$$$$$\ $$$$$$\ $$$$$$$\ $$$$$$\ 15 | $$$$$$$$ |$$ __$$\ $$ __$$\ $$ __$$\ \_$$ _| 16 | $$ __$$ |$$ / $$ |$$$$$$$$ |$$ | $$ | $$ | 17 | $$ | $$ |$$ | $$ |$$ ____|$$ | $$ | $$ |$$\ 18 | $$ | $$ |\$$$$$$$ |\$$$$$$$\ $$ | $$ | \$$$$ | 19 | \__| \__| \____$$ | \_______|\__| \__| \____/ 20 | $$\ $$ | 21 | \$$$$$$ | 22 | \______/ -------------------------------------------------------------------------------- /src/main/resources/banners/proxy.txt: -------------------------------------------------------------------------------- 1 | $$$$$$$\ $$\ $$\ 2 | $$ __$$\ $$ | $$ | 3 | $$ | $$ | $$$$$$\ $$$$$$\ $$$$$$\$$$$\ $$$$$$\ $$$$$$\ $$$$$$$\ $$$$$$\ $$\ $$\ $$$$$$$\ 4 | $$$$$$$ |$$ __$$\ $$ __$$\ $$ _$$ _$$\ $$ __$$\ \_$$ _| $$ __$$\ $$ __$$\ $$ | $$ |$$ _____| 5 | $$ ____/ $$ | \__|$$ / $$ |$$ / $$ / $$ |$$$$$$$$ | $$ | $$ | $$ |$$$$$$$$ |$$ | $$ |\$$$$$$\ 6 | $$ | $$ | $$ | $$ |$$ | $$ | $$ |$$ ____| $$ |$$\ $$ | $$ |$$ ____|$$ | $$ | \____$$\ 7 | $$ | $$ | \$$$$$$ |$$ | $$ | $$ |\$$$$$$$\ \$$$$ |$$ | $$ |\$$$$$$$\ \$$$$$$ |$$$$$$$ | 8 | \__| \__| \______/ \__| \__| \__| \_______| \____/ \__| \__| \_______| \______/ \_______/ 9 | 10 | 11 | 12 | $$$$$$$\ 13 | $$ __$$\ 14 | $$ | $$ | $$$$$$\ $$$$$$\ $$\ $$\ $$\ $$\ 15 | $$$$$$$ |$$ __$$\ $$ __$$\ \$$\ $$ |$$ | $$ | 16 | $$ ____/ $$ | \__|$$ / $$ | \$$$$ / $$ | $$ | 17 | $$ | $$ | $$ | $$ | $$ $$< $$ | $$ | 18 | $$ | $$ | \$$$$$$ |$$ /\$$\ \$$$$$$$ | 19 | \__| \__| \______/ \__/ \__| \____$$ | 20 | $$\ $$ | 21 | \$$$$$$ | 22 | \______/ -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | %d{HH:mm:ss.SSS} %-5level [%file:%line] - %msg [%thread]%n 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | proxy { 2 | http {} 3 | 4 | agent {} 5 | 6 | admin {} 7 | 8 | metrics { 9 | grpc {} 10 | } 11 | 12 | internal { 13 | zipkin {} 14 | blitz {} 15 | } 16 | } 17 | 18 | agent { 19 | proxy {} 20 | 21 | http {} 22 | 23 | admin {} 24 | 25 | metrics { 26 | grpc {} 27 | } 28 | 29 | pathConfigs: [] 30 | 31 | internal { 32 | zipkin {} 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/AdminDefaultPathTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.dsl.KtorDsl.blockingGet 22 | import io.ktor.client.statement.bodyAsText 23 | import io.ktor.http.HttpStatusCode 24 | import io.prometheus.TestUtils.startAgent 25 | import io.prometheus.TestUtils.startProxy 26 | import io.prometheus.common.Utils.lambda 27 | import org.amshove.kluent.shouldBeEqualTo 28 | import org.amshove.kluent.shouldBeGreaterThan 29 | import org.amshove.kluent.shouldContain 30 | import org.amshove.kluent.shouldStartWith 31 | import org.junit.jupiter.api.AfterAll 32 | import org.junit.jupiter.api.BeforeAll 33 | import org.junit.jupiter.api.Test 34 | 35 | class AdminDefaultPathTest { 36 | private val agentConfigVals = agent.agentConfigVals 37 | private val proxyConfigVals = proxy.proxyConfigVals 38 | 39 | @Test 40 | fun proxyPingPathTest() { 41 | with(proxyConfigVals.admin) { 42 | blockingGet("$port/$pingPath".withPrefix()) { response -> 43 | response.status shouldBeEqualTo HttpStatusCode.OK 44 | response.bodyAsText() shouldStartWith "pong" 45 | } 46 | } 47 | } 48 | 49 | @Test 50 | fun agentPingPathTest() { 51 | with(agentConfigVals.admin) { 52 | blockingGet("$port/$pingPath".withPrefix()) { response -> 53 | response.status shouldBeEqualTo HttpStatusCode.OK 54 | response.bodyAsText() shouldStartWith "pong" 55 | } 56 | } 57 | } 58 | 59 | @Test 60 | fun proxyVersionPathTest() { 61 | with(agentConfigVals.admin) { 62 | blockingGet("$port/$versionPath".withPrefix()) { response -> 63 | response.status shouldBeEqualTo HttpStatusCode.OK 64 | response.bodyAsText() shouldContain "version" 65 | } 66 | } 67 | } 68 | 69 | @Test 70 | fun agentVersionPathTest() { 71 | with(agentConfigVals.admin) { 72 | blockingGet("$port/$versionPath".withPrefix()) { response -> 73 | response.status shouldBeEqualTo HttpStatusCode.OK 74 | response.bodyAsText() shouldContain "version" 75 | } 76 | } 77 | } 78 | 79 | @Test 80 | fun proxyHealthCheckPathTest() { 81 | with(proxyConfigVals.admin) { 82 | blockingGet("$port/$healthCheckPath".withPrefix()) { response -> 83 | response.status shouldBeEqualTo HttpStatusCode.OK 84 | response.bodyAsText().length shouldBeGreaterThan 10 85 | } 86 | } 87 | } 88 | 89 | @Test 90 | fun agentHealthCheckPathTest() { 91 | with(agentConfigVals.admin) { 92 | blockingGet("$port/$healthCheckPath".withPrefix()) { response -> 93 | response.bodyAsText().length shouldBeGreaterThan 10 94 | } 95 | } 96 | } 97 | 98 | @Test 99 | fun proxyThreadDumpPathTest() { 100 | with(proxyConfigVals.admin) { 101 | blockingGet("$port/$threadDumpPath".withPrefix()) { response -> 102 | response.bodyAsText().length shouldBeGreaterThan 10 103 | } 104 | } 105 | } 106 | 107 | @Test 108 | fun agentThreadDumpPathTest() { 109 | with(agentConfigVals.admin) { 110 | blockingGet("$port/$threadDumpPath".withPrefix()) { response -> 111 | response.bodyAsText().length shouldBeGreaterThan 10 112 | } 113 | } 114 | } 115 | 116 | companion object : CommonCompanion() { 117 | @JvmStatic 118 | @BeforeAll 119 | fun setUp() = 120 | setItUp( 121 | proxySetup = lambda { startProxy(adminEnabled = true) }, 122 | agentSetup = lambda { startAgent(adminEnabled = true) }, 123 | ) 124 | 125 | @JvmStatic 126 | @AfterAll 127 | fun takeDown() = takeItDown() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/AdminEmptyPathTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.dsl.KtorDsl.blockingGet 22 | import io.ktor.http.HttpStatusCode 23 | import io.prometheus.TestUtils.startAgent 24 | import io.prometheus.TestUtils.startProxy 25 | import io.prometheus.common.ConfigVals 26 | import io.prometheus.common.Utils.lambda 27 | import org.amshove.kluent.shouldBeEqualTo 28 | import org.junit.jupiter.api.AfterAll 29 | import org.junit.jupiter.api.BeforeAll 30 | import org.junit.jupiter.api.Test 31 | 32 | class AdminEmptyPathTest { 33 | private val proxyConfigVals: ConfigVals.Proxy2 = proxy.configVals.proxy 34 | 35 | @Test 36 | fun proxyPingPathTest() { 37 | with(proxyConfigVals.admin) { 38 | port shouldBeEqualTo 8098 39 | pingPath shouldBeEqualTo "" 40 | 41 | blockingGet("$port/$pingPath".withPrefix()) { response -> 42 | response.status shouldBeEqualTo HttpStatusCode.NotFound 43 | } 44 | } 45 | } 46 | 47 | @Test 48 | fun proxyVersionPathTest() { 49 | with(proxyConfigVals.admin) { 50 | port shouldBeEqualTo 8098 51 | versionPath shouldBeEqualTo "" 52 | 53 | blockingGet("$port/$versionPath".withPrefix()) { response -> 54 | response.status shouldBeEqualTo HttpStatusCode.NotFound 55 | } 56 | } 57 | } 58 | 59 | @Test 60 | fun proxyHealthCheckPathTest() { 61 | with(proxyConfigVals.admin) { 62 | healthCheckPath shouldBeEqualTo "" 63 | 64 | blockingGet("$port/$healthCheckPath".withPrefix()) { response -> 65 | response.status shouldBeEqualTo HttpStatusCode.NotFound 66 | } 67 | } 68 | } 69 | 70 | @Test 71 | fun proxyThreadDumpPathTest() { 72 | with(proxyConfigVals.admin) { 73 | threadDumpPath shouldBeEqualTo "" 74 | 75 | blockingGet("$port/$threadDumpPath".withPrefix()) { response -> 76 | response.status shouldBeEqualTo HttpStatusCode.NotFound 77 | } 78 | } 79 | } 80 | 81 | companion object : CommonCompanion() { 82 | @JvmStatic 83 | @BeforeAll 84 | fun setUp() = 85 | setItUp( 86 | proxySetup = lambda { 87 | startProxy( 88 | adminEnabled = true, 89 | argv = listOf( 90 | "-Dproxy.admin.port=8098", 91 | "-Dproxy.admin.pingPath=\"\"", 92 | "-Dproxy.admin.versionPath=\"\"", 93 | "-Dproxy.admin.healthCheckPath=\"\"", 94 | "-Dproxy.admin.threadDumpPath=\"\"", 95 | ), 96 | ) 97 | }, 98 | agentSetup = lambda { startAgent(adminEnabled = true) }, 99 | ) 100 | 101 | @JvmStatic 102 | @AfterAll 103 | fun takeDown() = takeItDown() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/AdminNonDefaultPathTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.dsl.KtorDsl.blockingGet 22 | import io.ktor.client.statement.bodyAsText 23 | import io.ktor.http.HttpStatusCode 24 | import io.prometheus.TestUtils.startAgent 25 | import io.prometheus.TestUtils.startProxy 26 | import io.prometheus.common.ConfigVals 27 | import io.prometheus.common.Utils.lambda 28 | import org.amshove.kluent.shouldBeEqualTo 29 | import org.amshove.kluent.shouldBeGreaterThan 30 | import org.amshove.kluent.shouldContain 31 | import org.amshove.kluent.shouldStartWith 32 | import org.junit.jupiter.api.AfterAll 33 | import org.junit.jupiter.api.BeforeAll 34 | import org.junit.jupiter.api.Test 35 | 36 | class AdminNonDefaultPathTest { 37 | private val proxyConfigVals: ConfigVals.Proxy2 = proxy.configVals.proxy 38 | 39 | @Test 40 | fun proxyPingPathTest() { 41 | with(proxyConfigVals.admin) { 42 | port shouldBeEqualTo 8099 43 | pingPath shouldBeEqualTo "pingPath2" 44 | 45 | blockingGet("$port/$pingPath".withPrefix()) { response -> 46 | response.status shouldBeEqualTo HttpStatusCode.OK 47 | response.bodyAsText() shouldStartWith "pong" 48 | } 49 | } 50 | } 51 | 52 | @Test 53 | fun proxyVersionPathTest() { 54 | with(proxyConfigVals.admin) { 55 | port shouldBeEqualTo 8099 56 | versionPath shouldBeEqualTo "versionPath2" 57 | 58 | blockingGet("$port/$versionPath".withPrefix()) { response -> 59 | response.status shouldBeEqualTo HttpStatusCode.OK 60 | response.bodyAsText() shouldContain "version" 61 | } 62 | } 63 | } 64 | 65 | @Test 66 | fun proxyHealthCheckPathTest() { 67 | with(proxyConfigVals.admin) { 68 | healthCheckPath shouldBeEqualTo "healthCheckPath2" 69 | 70 | blockingGet("$port/$healthCheckPath".withPrefix()) { response -> 71 | response.status shouldBeEqualTo HttpStatusCode.OK 72 | response.bodyAsText().length shouldBeGreaterThan 10 73 | } 74 | } 75 | } 76 | 77 | @Test 78 | fun proxyThreadDumpPathTest() { 79 | with(proxyConfigVals.admin) { 80 | threadDumpPath shouldBeEqualTo "threadDumpPath2" 81 | 82 | blockingGet("$port/$threadDumpPath".withPrefix()) { response -> 83 | response.bodyAsText().length shouldBeGreaterThan 10 84 | } 85 | } 86 | } 87 | 88 | companion object : CommonCompanion() { 89 | @JvmStatic 90 | @BeforeAll 91 | fun setUp() = 92 | setItUp( 93 | proxySetup = lambda { 94 | startProxy( 95 | adminEnabled = true, 96 | argv = listOf( 97 | "-Dproxy.admin.port=8099", 98 | "-Dproxy.admin.pingPath=pingPath2", 99 | "-Dproxy.admin.versionPath=versionPath2", 100 | "-Dproxy.admin.healthCheckPath=healthCheckPath2", 101 | "-Dproxy.admin.threadDumpPath=threadDumpPath2", 102 | ), 103 | ) 104 | }, 105 | agentSetup = lambda { startAgent(adminEnabled = true) }, 106 | ) 107 | 108 | @JvmStatic 109 | @AfterAll 110 | fun takeDown() = takeItDown() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/CommonCompanion.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.util.simpleClassName 22 | import io.github.oshai.kotlinlogging.KotlinLogging 23 | import io.prometheus.client.CollectorRegistry 24 | import io.prometheus.common.Utils.lambda 25 | import kotlinx.coroutines.Dispatchers 26 | import kotlinx.coroutines.launch 27 | import kotlinx.coroutines.runBlocking 28 | import kotlin.properties.Delegates.notNull 29 | import kotlin.time.Duration.Companion.seconds 30 | 31 | open class CommonCompanion { 32 | private val logger = KotlinLogging.logger {} 33 | protected var proxy: Proxy by notNull() 34 | protected var agent: Agent by notNull() 35 | 36 | protected fun setItUp( 37 | proxySetup: () -> Proxy, 38 | agentSetup: () -> Agent, 39 | actions: () -> Unit = lambda {}, 40 | ) { 41 | CollectorRegistry.defaultRegistry.clear() 42 | 43 | runBlocking { 44 | launch(Dispatchers.IO + exceptionHandler(logger)) { 45 | proxy = proxySetup.invoke() 46 | } 47 | 48 | launch(Dispatchers.IO + exceptionHandler(logger)) { 49 | agent = agentSetup.invoke().apply { awaitInitialConnection(10.seconds) } 50 | } 51 | } 52 | 53 | actions.invoke() 54 | 55 | logger.info { "Started ${proxy.simpleClassName} and ${agent.simpleClassName}" } 56 | } 57 | 58 | protected fun takeItDown() { 59 | runBlocking { 60 | for (service in listOf(proxy, agent)) { 61 | logger.info { "Stopping ${service.simpleClassName}" } 62 | launch(Dispatchers.IO + exceptionHandler(logger)) { service.stopSync() } 63 | } 64 | } 65 | 66 | logger.info { "Stopped ${proxy.simpleClassName} and ${agent.simpleClassName}" } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/CommonTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.util.simpleClassName 22 | import io.prometheus.ProxyTests.timeoutTest 23 | import io.prometheus.SimpleTests.addRemovePathsTest 24 | import io.prometheus.SimpleTests.invalidAgentUrlTest 25 | import io.prometheus.SimpleTests.invalidPathTest 26 | import io.prometheus.SimpleTests.missingPathTest 27 | import io.prometheus.SimpleTests.threadedAddRemovePathsTest 28 | import kotlinx.coroutines.runBlocking 29 | import org.junit.jupiter.api.Test 30 | 31 | abstract class CommonTests( 32 | private val args: ProxyCallTestArgs, 33 | ) { 34 | @Test 35 | fun proxyCallTest() = runBlocking { ProxyTests.proxyCallTest(args) } 36 | 37 | @Test 38 | fun missingPathTest() = missingPathTest(simpleClassName) 39 | 40 | @Test 41 | fun invalidPathTest() = invalidPathTest(simpleClassName) 42 | 43 | @Test 44 | fun addRemovePathsTest() = runBlocking { addRemovePathsTest(args.agent.pathManager, simpleClassName) } 45 | 46 | @Test 47 | fun threadedAddRemovePathsTest() = runBlocking { threadedAddRemovePathsTest(args.agent.pathManager, simpleClassName) } 48 | 49 | @Test 50 | fun invalidAgentUrlTest() = runBlocking { invalidAgentUrlTest(args.agent.pathManager, simpleClassName) } 51 | 52 | @Test 53 | fun timeoutTest() = runBlocking { timeoutTest(args.agent.pathManager, simpleClassName) } 54 | 55 | companion object { 56 | const val HTTP_SERVER_COUNT = 5 57 | const val PATH_COUNT = 50 58 | const val SEQUENTIAL_QUERY_COUNT = 200 59 | const val PARALLEL_QUERY_COUNT = 10 60 | const val MIN_DELAY_MILLIS = 400 61 | const val MAX_DELAY_MILLIS = 600 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/DataClassTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.typesafe.config.ConfigFactory 22 | import com.typesafe.config.ConfigParseOptions 23 | import com.typesafe.config.ConfigSyntax 24 | import io.prometheus.common.ConfigVals 25 | import io.prometheus.common.ConfigWrappers.newAdminConfig 26 | import io.prometheus.common.ConfigWrappers.newMetricsConfig 27 | import io.prometheus.common.ConfigWrappers.newZipkinConfig 28 | import org.amshove.kluent.shouldBeEqualTo 29 | import org.amshove.kluent.shouldBeFalse 30 | import org.amshove.kluent.shouldBeTrue 31 | import org.junit.jupiter.api.Test 32 | 33 | class DataClassTest { 34 | private fun configVals(str: String): ConfigVals { 35 | val config = ConfigFactory.parseString(str, ConfigParseOptions.defaults().setSyntax(ConfigSyntax.CONF)) 36 | return ConfigVals(config.withFallback(ConfigFactory.load().resolve()).resolve()) 37 | } 38 | 39 | @Test 40 | fun adminConfigTest() { 41 | var vals = configVals("agent.admin.enabled=true") 42 | newAdminConfig(vals.agent.admin.enabled, -1, vals.agent.admin) 43 | .also { 44 | it.enabled.shouldBeTrue() 45 | } 46 | 47 | vals = configVals("agent.admin.port=888") 48 | newAdminConfig(vals.agent.admin.enabled, vals.agent.admin.port, vals.agent.admin) 49 | .also { 50 | it.enabled.shouldBeFalse() 51 | it.port shouldBeEqualTo 888 52 | } 53 | 54 | newAdminConfig(true, 444, configVals("agent.admin.pingPath=a pingpath val").agent.admin) 55 | .also { 56 | it.pingPath shouldBeEqualTo "a pingpath val" 57 | } 58 | 59 | newAdminConfig(true, 444, configVals("agent.admin.versionPath=a versionpath val").agent.admin) 60 | .also { 61 | it.versionPath shouldBeEqualTo "a versionpath val" 62 | } 63 | 64 | newAdminConfig(true, 444, configVals("agent.admin.healthCheckPath=a healthCheckPath val").agent.admin) 65 | .also { 66 | it.healthCheckPath shouldBeEqualTo "a healthCheckPath val" 67 | } 68 | 69 | newAdminConfig(true, 444, configVals("agent.admin.threadDumpPath=a threadDumpPath val").agent.admin) 70 | .also { 71 | it.threadDumpPath shouldBeEqualTo "a threadDumpPath val" 72 | } 73 | } 74 | 75 | @Test 76 | fun metricsConfigTest() { 77 | newMetricsConfig(true, 555, configVals("agent.metrics.enabled=true").agent.metrics) 78 | .also { 79 | it.enabled.shouldBeTrue() 80 | } 81 | 82 | newMetricsConfig(true, 555, configVals("agent.metrics.hostname=testval").agent.metrics) 83 | .also { 84 | it.port shouldBeEqualTo 555 85 | } 86 | 87 | newMetricsConfig(true, 555, configVals("agent.metrics.path=a path val").agent.metrics) 88 | .also { 89 | it.path shouldBeEqualTo "a path val" 90 | } 91 | 92 | newMetricsConfig(true, 555, configVals("agent.metrics.standardExportsEnabled=true").agent.metrics) 93 | .also { 94 | it.standardExportsEnabled.shouldBeTrue() 95 | } 96 | 97 | newMetricsConfig(true, 555, configVals("agent.metrics.memoryPoolsExportsEnabled=true").agent.metrics) 98 | .also { 99 | it.memoryPoolsExportsEnabled.shouldBeTrue() 100 | } 101 | 102 | newMetricsConfig(true, 555, configVals("agent.metrics.garbageCollectorExportsEnabled=true").agent.metrics) 103 | .also { 104 | it.garbageCollectorExportsEnabled.shouldBeTrue() 105 | } 106 | 107 | newMetricsConfig(true, 555, configVals("agent.metrics.threadExportsEnabled=true").agent.metrics) 108 | .also { 109 | it.threadExportsEnabled.shouldBeTrue() 110 | } 111 | 112 | newMetricsConfig(true, 555, configVals("agent.metrics.classLoadingExportsEnabled=true").agent.metrics) 113 | .also { 114 | it.classLoadingExportsEnabled.shouldBeTrue() 115 | } 116 | 117 | newMetricsConfig(true, 555, configVals("agent.metrics.versionInfoExportsEnabled=true").agent.metrics) 118 | .also { 119 | it.versionInfoExportsEnabled.shouldBeTrue() 120 | } 121 | } 122 | 123 | @Test 124 | fun zipkinConfigTest() { 125 | newZipkinConfig(configVals("agent.internal.zipkin.enabled=true").agent.internal.zipkin) 126 | .also { 127 | it.enabled.shouldBeTrue() 128 | } 129 | 130 | newZipkinConfig(configVals("agent.internal.zipkin.hostname=testval").agent.internal.zipkin) 131 | .also { 132 | it.hostname shouldBeEqualTo "testval" 133 | } 134 | 135 | newZipkinConfig(configVals("agent.internal.zipkin.port=999").agent.internal.zipkin) 136 | .also { 137 | it.port shouldBeEqualTo 999 138 | } 139 | 140 | newZipkinConfig(configVals("agent.internal.zipkin.path=a path val").agent.internal.zipkin) 141 | .also { 142 | it.path shouldBeEqualTo "a path val" 143 | } 144 | 145 | newZipkinConfig(configVals("agent.internal.zipkin.serviceName=a service name").agent.internal.zipkin) 146 | .also { 147 | it.serviceName shouldBeEqualTo "a service name" 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/InProcessTestNoAdminMetricsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.util.simpleClassName 22 | import io.prometheus.TestConstants.DEFAULT_CHUNK_SIZE 23 | import io.prometheus.TestConstants.DEFAULT_TIMEOUT 24 | import io.prometheus.TestUtils.startAgent 25 | import io.prometheus.TestUtils.startProxy 26 | import io.prometheus.common.Utils.lambda 27 | import org.junit.jupiter.api.AfterAll 28 | import org.junit.jupiter.api.BeforeAll 29 | 30 | class InProcessTestNoAdminMetricsTest : 31 | CommonTests(ProxyCallTestArgs(agent = agent, startPort = 10100, caller = simpleClassName)) { 32 | companion object : CommonCompanion() { 33 | @JvmStatic 34 | @BeforeAll 35 | fun setUp() = 36 | setItUp( 37 | proxySetup = lambda { startProxy("nometrics") }, 38 | agentSetup = lambda { 39 | startAgent( 40 | serverName = "nometrics", 41 | scrapeTimeoutSecs = DEFAULT_TIMEOUT, 42 | chunkContentSizeKbs = DEFAULT_CHUNK_SIZE, 43 | ) 44 | }, 45 | ) 46 | 47 | @JvmStatic 48 | @AfterAll 49 | fun takeDown() = takeItDown() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/InProcessTestWithAdminMetricsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.util.simpleClassName 22 | import io.prometheus.TestConstants.DEFAULT_CHUNK_SIZE 23 | import io.prometheus.TestConstants.DEFAULT_TIMEOUT 24 | import io.prometheus.TestUtils.startAgent 25 | import io.prometheus.TestUtils.startProxy 26 | import io.prometheus.common.Utils.lambda 27 | import org.junit.jupiter.api.AfterAll 28 | import org.junit.jupiter.api.BeforeAll 29 | 30 | class InProcessTestWithAdminMetricsTest : 31 | CommonTests(ProxyCallTestArgs(agent = agent, startPort = 10700, caller = simpleClassName)) { 32 | companion object : CommonCompanion() { 33 | @JvmStatic 34 | @BeforeAll 35 | fun setUp() = 36 | setItUp( 37 | proxySetup = lambda { startProxy("withmetrics", adminEnabled = true, metricsEnabled = true) }, 38 | agentSetup = lambda { 39 | startAgent( 40 | serverName = "withmetrics", 41 | adminEnabled = true, 42 | metricsEnabled = true, 43 | scrapeTimeoutSecs = DEFAULT_TIMEOUT, 44 | chunkContentSizeKbs = DEFAULT_CHUNK_SIZE, 45 | ) 46 | }, 47 | ) 48 | 49 | @JvmStatic 50 | @AfterAll 51 | fun takeDown() = takeItDown() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/NettyTestNoAdminMetricsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.util.simpleClassName 22 | import io.prometheus.TestConstants.DEFAULT_CHUNK_SIZE 23 | import io.prometheus.TestConstants.DEFAULT_TIMEOUT 24 | import io.prometheus.TestUtils.startAgent 25 | import io.prometheus.TestUtils.startProxy 26 | import io.prometheus.common.Utils.lambda 27 | import org.junit.jupiter.api.AfterAll 28 | import org.junit.jupiter.api.BeforeAll 29 | 30 | class NettyTestNoAdminMetricsTest : 31 | CommonTests( 32 | ProxyCallTestArgs( 33 | agent = agent, 34 | startPort = 10900, 35 | caller = simpleClassName, 36 | ), 37 | ) { 38 | companion object : CommonCompanion() { 39 | @JvmStatic 40 | @BeforeAll 41 | fun setUp() = 42 | setItUp( 43 | proxySetup = lambda { startProxy() }, 44 | agentSetup = lambda { 45 | startAgent( 46 | scrapeTimeoutSecs = DEFAULT_TIMEOUT, 47 | chunkContentSizeKbs = DEFAULT_CHUNK_SIZE, 48 | ) 49 | }, 50 | ) 51 | 52 | @JvmStatic 53 | @AfterAll 54 | fun takeDown() = takeItDown() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/NettyTestWithAdminMetricsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.dsl.KtorDsl.get 22 | import com.github.pambrose.common.dsl.KtorDsl.withHttpClient 23 | import com.github.pambrose.common.util.simpleClassName 24 | import com.github.pambrose.common.util.sleep 25 | import io.ktor.client.statement.bodyAsText 26 | import io.ktor.http.HttpStatusCode 27 | import io.prometheus.TestConstants.DEFAULT_CHUNK_SIZE 28 | import io.prometheus.TestConstants.DEFAULT_TIMEOUT 29 | import io.prometheus.TestUtils.startAgent 30 | import io.prometheus.TestUtils.startProxy 31 | import io.prometheus.common.Utils.lambda 32 | import kotlinx.coroutines.runBlocking 33 | import org.amshove.kluent.shouldBeEqualTo 34 | import org.amshove.kluent.shouldBeGreaterThan 35 | import org.junit.jupiter.api.AfterAll 36 | import org.junit.jupiter.api.BeforeAll 37 | import org.junit.jupiter.api.Test 38 | import kotlin.time.Duration.Companion.seconds 39 | 40 | class NettyTestWithAdminMetricsTest : 41 | CommonTests( 42 | ProxyCallTestArgs( 43 | agent = agent, 44 | startPort = 10300, 45 | caller = simpleClassName, 46 | ), 47 | ) { 48 | @Test 49 | fun adminDebugCallsTest() { 50 | runBlocking { 51 | withHttpClient { 52 | get("8093/debug".withPrefix()) { response -> 53 | val body = response.bodyAsText() 54 | body.length shouldBeGreaterThan 100 55 | response.status shouldBeEqualTo HttpStatusCode.OK 56 | } 57 | } 58 | 59 | withHttpClient { 60 | get("8092/debug".withPrefix()) { response -> 61 | val body = response.bodyAsText() 62 | body.length shouldBeGreaterThan 100 63 | response.status shouldBeEqualTo HttpStatusCode.OK 64 | } 65 | } 66 | } 67 | } 68 | 69 | companion object : CommonCompanion() { 70 | @JvmStatic 71 | @BeforeAll 72 | fun setUp() = 73 | setItUp( 74 | proxySetup = lambda { 75 | startProxy( 76 | adminEnabled = true, 77 | debugEnabled = true, 78 | metricsEnabled = true, 79 | ) 80 | }, 81 | agentSetup = lambda { 82 | startAgent( 83 | adminEnabled = true, 84 | debugEnabled = true, 85 | metricsEnabled = true, 86 | scrapeTimeoutSecs = DEFAULT_TIMEOUT, 87 | chunkContentSizeKbs = DEFAULT_CHUNK_SIZE, 88 | ) 89 | }, 90 | actions = lambda { 91 | // Wait long enough to trigger heartbeat for code coverage 92 | sleep(15.seconds) 93 | }, 94 | ) 95 | 96 | @JvmStatic 97 | @AfterAll 98 | fun takeDown() = takeItDown() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/OptionsTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import io.prometheus.TestConstants.OPTIONS_CONFIG 22 | import io.prometheus.agent.AgentOptions 23 | import io.prometheus.proxy.ProxyOptions 24 | import org.amshove.kluent.shouldBeEqualTo 25 | import org.amshove.kluent.shouldBeFalse 26 | import org.amshove.kluent.shouldBeTrue 27 | import org.junit.jupiter.api.Test 28 | 29 | class OptionsTest { 30 | @Test 31 | fun verifyDefaultValues() { 32 | val configVals = readProxyOptions(listOf()) 33 | configVals.proxy 34 | .apply { 35 | http.port shouldBeEqualTo 8080 36 | internal.zipkin.enabled.shouldBeFalse() 37 | } 38 | } 39 | 40 | @Test 41 | fun verifyConfValues() { 42 | val configVals = readProxyOptions(listOf("--config", OPTIONS_CONFIG)) 43 | configVals.proxy 44 | .apply { 45 | http.port shouldBeEqualTo 8181 46 | internal.zipkin.enabled.shouldBeTrue() 47 | } 48 | } 49 | 50 | @Test 51 | fun verifyUnquotedPropValue() { 52 | val configVals = readProxyOptions(listOf("-Dproxy.http.port=9393", "-Dproxy.internal.zipkin.enabled=true")) 53 | configVals.proxy 54 | .apply { 55 | http.port shouldBeEqualTo 9393 56 | internal.zipkin.enabled.shouldBeTrue() 57 | } 58 | } 59 | 60 | @Test 61 | fun verifyQuotedPropValue() { 62 | val configVals = readProxyOptions(listOf("-Dproxy.http.port=9394")) 63 | configVals.proxy.http.port shouldBeEqualTo 9394 64 | } 65 | 66 | @Test 67 | fun verifyPathConfigs() { 68 | val configVals = readAgentOptions(listOf("--config", OPTIONS_CONFIG)) 69 | configVals.agent.pathConfigs.size shouldBeEqualTo 3 70 | } 71 | 72 | @Test 73 | fun verifyProxyDefaults() { 74 | ProxyOptions(listOf()) 75 | .apply { 76 | proxyHttpPort shouldBeEqualTo 8080 77 | proxyAgentPort shouldBeEqualTo 50051 78 | } 79 | } 80 | 81 | @Test 82 | fun verifyAgentDefaults() { 83 | val options = AgentOptions(listOf("--name", "test-name", "--proxy", "host5"), false) 84 | options 85 | .apply { 86 | metricsEnabled shouldBeEqualTo false 87 | dynamicParams.size shouldBeEqualTo 0 88 | agentName shouldBeEqualTo "test-name" 89 | proxyHostname shouldBeEqualTo "host5" 90 | } 91 | } 92 | 93 | private fun readProxyOptions(argList: List) = ProxyOptions(argList).configVals 94 | 95 | private fun readAgentOptions(argList: List) = AgentOptions(argList, false).configVals 96 | } 97 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/SimpleTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.dsl.KtorDsl.blockingGet 22 | import io.github.oshai.kotlinlogging.KotlinLogging 23 | import io.ktor.http.HttpStatusCode 24 | import io.prometheus.TestConstants.PROXY_PORT 25 | import io.prometheus.agent.AgentPathManager 26 | import kotlinx.coroutines.Dispatchers 27 | import kotlinx.coroutines.launch 28 | import kotlinx.coroutines.sync.Mutex 29 | import kotlinx.coroutines.sync.withLock 30 | import kotlinx.coroutines.withTimeoutOrNull 31 | import org.amshove.kluent.shouldBeEqualTo 32 | import org.amshove.kluent.shouldBeNull 33 | import org.amshove.kluent.shouldNotBeNull 34 | import kotlin.time.Duration.Companion.seconds 35 | 36 | internal object SimpleTests { 37 | private val logger = KotlinLogging.logger {} 38 | 39 | fun missingPathTest(caller: String) { 40 | logger.debug { "Calling missingPathTest() from $caller" } 41 | blockingGet("$PROXY_PORT/".withPrefix()) { response -> 42 | response.status shouldBeEqualTo HttpStatusCode.NotFound 43 | } 44 | } 45 | 46 | fun invalidPathTest(caller: String) { 47 | logger.debug { "Calling invalidPathTest() from $caller" } 48 | blockingGet("$PROXY_PORT/invalid_path".withPrefix()) { response -> 49 | response.status shouldBeEqualTo HttpStatusCode.NotFound 50 | } 51 | } 52 | 53 | suspend fun addRemovePathsTest( 54 | pathManager: AgentPathManager, 55 | caller: String, 56 | ) { 57 | logger.debug { "Calling addRemovePathsTest() from $caller" } 58 | 59 | // Take into account pre-existing paths already registered 60 | val originalSize = pathManager.pathMapSize() 61 | 62 | var cnt = 0 63 | repeat(TestConstants.REPS) { i -> 64 | val path = "test-$i" 65 | pathManager.let { manager -> 66 | manager.registerPath(path, "$PROXY_PORT/$path".withPrefix()) 67 | cnt++ 68 | manager.pathMapSize() shouldBeEqualTo originalSize + cnt 69 | manager.unregisterPath(path) 70 | cnt-- 71 | manager.pathMapSize() shouldBeEqualTo originalSize + cnt 72 | } 73 | } 74 | } 75 | 76 | suspend fun invalidAgentUrlTest( 77 | pathManager: AgentPathManager, 78 | caller: String, 79 | badPath: String = "badPath", 80 | ) { 81 | logger.debug { "Calling invalidAgentUrlTest() from $caller" } 82 | 83 | pathManager.registerPath(badPath, "33/metrics".withPrefix()) 84 | blockingGet("$PROXY_PORT/$badPath".withPrefix()) { response -> 85 | response.status shouldBeEqualTo HttpStatusCode.NotFound 86 | } 87 | pathManager.unregisterPath(badPath) 88 | } 89 | 90 | suspend fun threadedAddRemovePathsTest( 91 | pathManager: AgentPathManager, 92 | caller: String, 93 | ) { 94 | logger.debug { "Calling threadedAddRemovePathsTest() from $caller" } 95 | val paths: MutableList = mutableListOf() 96 | 97 | // Take into account pre-existing paths already registered 98 | val originalSize = pathManager.pathMapSize() 99 | 100 | withTimeoutOrNull(30.seconds.inWholeMilliseconds) { 101 | val mutex = Mutex() 102 | val jobs = 103 | List(TestConstants.REPS) { i -> 104 | launch(Dispatchers.Default + exceptionHandler(logger)) { 105 | val path = "test-$i}" 106 | val url = "$PROXY_PORT/$path".withPrefix() 107 | mutex.withLock { paths += path } 108 | pathManager.registerPath(path, url) 109 | } 110 | } 111 | 112 | jobs.forEach { job -> 113 | job.join() 114 | job.getCancellationException().cause.shouldBeNull() 115 | } 116 | }.shouldNotBeNull() 117 | 118 | paths.size shouldBeEqualTo TestConstants.REPS 119 | pathManager.pathMapSize() shouldBeEqualTo (originalSize + TestConstants.REPS) 120 | 121 | withTimeoutOrNull(30.seconds.inWholeMilliseconds) { 122 | val jobs = 123 | List(paths.size) { 124 | launch(Dispatchers.Default + exceptionHandler(logger)) { 125 | pathManager.unregisterPath(paths[it]) 126 | } 127 | } 128 | 129 | jobs.forEach { job -> 130 | job.join() 131 | job.getCancellationException().cause.shouldBeNull() 132 | } 133 | }.shouldNotBeNull() 134 | 135 | pathManager.pathMapSize() shouldBeEqualTo originalSize 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/TestConstants.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import java.io.File 22 | 23 | object TestConstants { 24 | const val REPS = 1000 25 | const val PROXY_PORT = 9505 26 | const val DEFAULT_TIMEOUT = 3 27 | const val DEFAULT_CHUNK_SIZE = 5 28 | 29 | private const val TRAVIS_FILE = "etc/test-configs/travis.conf" 30 | private const val JUNIT_FILE = "etc/test-configs/junit-test.conf" 31 | private const val GH_PREFIX = "https://raw.githubusercontent.com/pambrose/prometheus-proxy/master/" 32 | 33 | val CONFIG_ARG = listOf("--config", "${if (File(TRAVIS_FILE).exists()) "" else GH_PREFIX}$TRAVIS_FILE") 34 | 35 | val OPTIONS_CONFIG = "${if (File(JUNIT_FILE).exists()) GH_PREFIX else ""}$JUNIT_FILE" 36 | } 37 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/TestUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.util.getBanner 22 | import io.github.oshai.kotlinlogging.KLogger 23 | import io.github.oshai.kotlinlogging.KotlinLogging 24 | import io.prometheus.TestConstants.PROXY_PORT 25 | import io.prometheus.agent.AgentOptions 26 | import io.prometheus.common.Utils.getVersionDesc 27 | import io.prometheus.proxy.ProxyOptions 28 | import kotlinx.coroutines.CoroutineExceptionHandler 29 | import kotlinx.serialization.KSerializer 30 | import kotlinx.serialization.Serializable 31 | import kotlinx.serialization.descriptors.PrimitiveKind 32 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 33 | import kotlinx.serialization.descriptors.SerialDescriptor 34 | import kotlinx.serialization.encodeToString 35 | import kotlinx.serialization.encoding.Decoder 36 | import kotlinx.serialization.encoding.Encoder 37 | import kotlinx.serialization.json.Json 38 | import java.nio.channels.ClosedSelectorException 39 | 40 | @Serializable(with = CustomEnumSerializer::class) 41 | enum class MyEnum( 42 | val type: String, 43 | ) { 44 | A("a"), 45 | B("b"), 46 | } 47 | 48 | object CustomEnumSerializer : KSerializer { 49 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MyEnum", PrimitiveKind.STRING) 50 | 51 | override fun serialize( 52 | encoder: Encoder, 53 | value: MyEnum, 54 | ) { 55 | encoder.encodeString(value.type) 56 | } 57 | 58 | override fun deserialize(decoder: Decoder): MyEnum { 59 | val value = decoder.decodeString() 60 | return MyEnum.entries.find { it.type == value } 61 | ?: throw IllegalArgumentException("Unknown enum value: $value") 62 | } 63 | } 64 | 65 | object TestUtils { 66 | private val logger = KotlinLogging.logger {} 67 | 68 | @JvmStatic 69 | fun main(args: Array) { 70 | println(Json.encodeToString(MyEnum.A)) 71 | } 72 | 73 | fun startProxy( 74 | serverName: String = "", 75 | adminEnabled: Boolean = false, 76 | debugEnabled: Boolean = false, 77 | metricsEnabled: Boolean = false, 78 | argv: List = emptyList(), 79 | ): Proxy { 80 | logger.apply { 81 | info { getBanner("banners/proxy.txt", logger) } 82 | info { getVersionDesc(false) } 83 | } 84 | 85 | val proxyOptions = ProxyOptions( 86 | mutableListOf() 87 | .apply { 88 | addAll(TestConstants.CONFIG_ARG) 89 | addAll(argv) 90 | add("-Dproxy.admin.enabled=$adminEnabled") 91 | add("-Dproxy.admin.debugEnabled=$debugEnabled") 92 | add("-Dproxy.metrics.enabled=$metricsEnabled") 93 | }, 94 | ) 95 | return Proxy( 96 | options = proxyOptions, 97 | proxyHttpPort = PROXY_PORT, 98 | inProcessServerName = serverName, 99 | testMode = true, 100 | ) { startSync() } 101 | } 102 | 103 | fun startAgent( 104 | serverName: String = "", 105 | adminEnabled: Boolean = false, 106 | debugEnabled: Boolean = false, 107 | metricsEnabled: Boolean = false, 108 | scrapeTimeoutSecs: Int = -1, 109 | chunkContentSizeKbs: Int = -1, 110 | argv: List = emptyList(), 111 | ): Agent { 112 | logger.apply { 113 | info { getBanner("banners/agent.txt", logger) } 114 | info { getVersionDesc(false) } 115 | } 116 | 117 | val agentOptions = AgentOptions( 118 | args = mutableListOf() 119 | .apply { 120 | addAll(TestConstants.CONFIG_ARG) 121 | addAll(argv) 122 | add("-Dagent.admin.enabled=$adminEnabled") 123 | add("-Dagent.admin.debugEnabled=$debugEnabled") 124 | add("-Dagent.metrics.enabled=$metricsEnabled") 125 | if (scrapeTimeoutSecs != -1) 126 | add("-Dagent.scrapeTimeoutSecs=$scrapeTimeoutSecs") 127 | if (chunkContentSizeKbs != -1) 128 | add("-Dagent.chunkContentSizeKbs=$chunkContentSizeKbs") 129 | }, 130 | exitOnMissingConfig = false, 131 | ) 132 | return Agent(options = agentOptions, inProcessServerName = serverName, testMode = true) { startSync() } 133 | } 134 | } 135 | 136 | fun exceptionHandler(logger: KLogger) = 137 | CoroutineExceptionHandler { _, e -> 138 | if (e is ClosedSelectorException) 139 | logger.info { "CoroutineExceptionHandler caught: $e" } 140 | else 141 | logger.warn(e) { "CoroutineExceptionHandler caught: $e" } 142 | } 143 | 144 | fun String.withPrefix(prefix: String = "http://localhost:") = if (this.startsWith(prefix)) this else (prefix + this) 145 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/TlsNoMutualAuthTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.util.simpleClassName 22 | import io.prometheus.TestConstants.DEFAULT_CHUNK_SIZE 23 | import io.prometheus.TestConstants.DEFAULT_TIMEOUT 24 | import io.prometheus.TestUtils.startAgent 25 | import io.prometheus.TestUtils.startProxy 26 | import io.prometheus.common.Utils.lambda 27 | import org.junit.jupiter.api.AfterAll 28 | import org.junit.jupiter.api.BeforeAll 29 | 30 | class TlsNoMutualAuthTest : 31 | CommonTests( 32 | ProxyCallTestArgs( 33 | agent = agent, 34 | startPort = 10200, 35 | caller = simpleClassName, 36 | ), 37 | ) { 38 | companion object : CommonCompanion() { 39 | @JvmStatic 40 | @BeforeAll 41 | fun setUp() = 42 | setItUp( 43 | proxySetup = lambda { 44 | startProxy( 45 | serverName = "nomutualauth", 46 | argv = listOf( 47 | "--agent_port", 48 | "50440", 49 | "--cert", 50 | "testing/certs/server1.pem", 51 | "--key", 52 | "testing/certs/server1.key", 53 | ), 54 | ) 55 | }, 56 | agentSetup = lambda { 57 | startAgent( 58 | serverName = "nomutualauth", 59 | scrapeTimeoutSecs = DEFAULT_TIMEOUT, 60 | chunkContentSizeKbs = DEFAULT_CHUNK_SIZE, 61 | argv = listOf( 62 | "--proxy", 63 | "localhost:50440", 64 | "--trust", 65 | "testing/certs/ca.pem", 66 | "--override", 67 | "foo.test.google.fr", 68 | ), 69 | ) 70 | }, 71 | ) 72 | 73 | @JvmStatic 74 | @AfterAll 75 | fun takeDown() = takeItDown() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/kotlin/io/prometheus/TlsWithMutualAuthTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2020 Paul Ambrose (pambrose@mac.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") 18 | 19 | package io.prometheus 20 | 21 | import com.github.pambrose.common.util.simpleClassName 22 | import io.prometheus.TestConstants.DEFAULT_CHUNK_SIZE 23 | import io.prometheus.TestConstants.DEFAULT_TIMEOUT 24 | import io.prometheus.TestUtils.startAgent 25 | import io.prometheus.TestUtils.startProxy 26 | import io.prometheus.common.Utils.lambda 27 | import org.junit.jupiter.api.AfterAll 28 | import org.junit.jupiter.api.BeforeAll 29 | 30 | class TlsWithMutualAuthTest : 31 | CommonTests( 32 | ProxyCallTestArgs( 33 | agent = agent, 34 | startPort = 10800, 35 | caller = simpleClassName, 36 | ), 37 | ) { 38 | companion object : CommonCompanion() { 39 | @JvmStatic 40 | @BeforeAll 41 | fun setUp() = 42 | setItUp( 43 | proxySetup = lambda { 44 | startProxy( 45 | serverName = "withmutualauth", 46 | argv = listOf( 47 | "--agent_port", 48 | "50440", 49 | "--cert", 50 | "testing/certs/server1.pem", 51 | "--key", 52 | "testing/certs/server1.key", 53 | "--trust", 54 | "testing/certs/ca.pem", 55 | ), 56 | ) 57 | }, 58 | agentSetup = lambda { 59 | startAgent( 60 | serverName = "withmutualauth", 61 | scrapeTimeoutSecs = DEFAULT_TIMEOUT, 62 | chunkContentSizeKbs = DEFAULT_CHUNK_SIZE, 63 | argv = listOf( 64 | "--proxy", 65 | "localhost:50440", 66 | "--cert", 67 | "testing/certs/client.pem", 68 | "--key", 69 | "testing/certs/client.key", 70 | "--trust", 71 | "testing/certs/ca.pem", 72 | "--override", 73 | "foo.test.google.fr", 74 | ), 75 | ) 76 | }, 77 | ) 78 | 79 | @JvmStatic 80 | @AfterAll 81 | fun takeDown() = takeItDown() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%file:%line] - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /testing/certs/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla 5 | Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 6 | YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT 7 | BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 8 | +L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu 9 | g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd 10 | Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV 11 | HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau 12 | sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m 13 | oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG 14 | Dfcog5wrJytaQ6UA0wE= 15 | -----END CERTIFICATE----- 16 | -------------------------------------------------------------------------------- /testing/certs/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICeQIBADANBgkqhkiG9w0BAQEFAASCAmMwggJfAgEAAoGBAOxUR9uhvhbeVUIM 3 | s5WbH0px0mehl2+6sZpNjzvE2KimZpHzMJHukVH0Ffkvhs0b8+S5Ut9VNUAqd3IM 4 | JCCAEGtRNoQhM1t9Yr2zAckSvbRacp+FL/Cj9eDmyo00KsVGaeefA4Dh4OW+ZhkT 5 | NKcldXqkSuj1sEf244JZYuqZp6/tAgMBAAECgYEAi2NSVqpZMafE5YYUTcMGe6QS 6 | k2jtpsqYgggI2RnLJ/2tNZwYI5pwP8QVSbnMaiF4gokD5hGdrNDfTnb2v+yIwYEH 7 | 0w8+oG7Z81KodsiZSIDJfTGsAZhVNwOz9y0VD8BBZZ1/274Zh52AUKLjZS/ZwIbS 8 | W2ywya855dPnH/wj+0ECQQD9X8D920kByTNHhBG18biAEZ4pxs9f0OAG8333eVcI 9 | w2lJDLsYDZrCB2ocgA3lUdozlzPC7YDYw8reg0tkiRY5AkEA7sdNzOeQsQRn7++5 10 | 0bP9DtT/iON1gbfxRzCfCfXdoOtfQWIzTePWtURt9X/5D9NofI0Rg5W2oGy/MLe5 11 | /sXHVQJBAIup5XrJDkQywNZyAUU2ecn2bCWBFjwtqd+LBmuMciI9fOKsZtEKZrz/ 12 | U0lkeMRoSwvXE8wmGLjjrAbdfohrXFkCQQDZEx/LtIl6JINJQiswVe0tWr6k+ASP 13 | 1WXoTm+HYpoF/XUvv9LccNF1IazFj34hwRQwhx7w/V52Ieb+p0jUMYGxAkEAjDhd 14 | 9pBO1fKXWiXzi9ZKfoyTNcUq3eBSVKwPG2nItg5ycXengjT5sgcWDnciIzW7BIVI 15 | JiqOszq9GWESErAatg== 16 | -----END PRIVATE KEY----- 17 | -------------------------------------------------------------------------------- /testing/certs/client.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC6TCCAlKgAwIBAgIBCjANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET 3 | MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ 4 | dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTEwMDEwOTU4WhcNMjUxMTA3 5 | MDEwOTU4WjBaMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8G 6 | A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRMwEQYDVQQDDAp0ZXN0Y2xp 7 | ZW50MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDsVEfbob4W3lVCDLOVmx9K 8 | cdJnoZdvurGaTY87xNiopmaR8zCR7pFR9BX5L4bNG/PkuVLfVTVAKndyDCQggBBr 9 | UTaEITNbfWK9swHJEr20WnKfhS/wo/Xg5sqNNCrFRmnnnwOA4eDlvmYZEzSnJXV6 10 | pEro9bBH9uOCWWLqmaev7QIDAQABo4HCMIG/MAkGA1UdEwQCMAAwCwYDVR0PBAQD 11 | AgXgMB0GA1UdDgQWBBQAdbW5Vml/CnYwqdP3mOHDARU+8zBwBgNVHSMEaTBnoVqk 12 | WDBWMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMY 13 | SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2GCCQCRxhke 14 | HRoqBzAJBgNVHREEAjAAMAkGA1UdEgQCMAAwDQYJKoZIhvcNAQELBQADgYEAf4MM 15 | k+sdzd720DfrQ0PF2gDauR3M9uBubozDuMuF6ufAuQBJSKGQEGibXbUelrwHmnql 16 | UjTyfolVcxEBVaF4VFHmn7u6vP7S1NexIDdNUHcULqxIb7Tzl8JYq8OOHD2rQy4H 17 | s8BXaVIzw4YcaCGAMS0iDX052Sy7e2JhP8Noxvo= 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /testing/certs/server1.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOHDFScoLCVJpYDD 3 | M4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1BgzkWF+slf 4 | 3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd9N8YwbBY 5 | AckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAECgYAn7qGnM2vbjJNBm0VZCkOkTIWm 6 | V10okw7EPJrdL2mkre9NasghNXbE1y5zDshx5Nt3KsazKOxTT8d0Jwh/3KbaN+YY 7 | tTCbKGW0pXDRBhwUHRcuRzScjli8Rih5UOCiZkhefUTcRb6xIhZJuQy71tjaSy0p 8 | dHZRmYyBYO2YEQ8xoQJBAPrJPhMBkzmEYFtyIEqAxQ/o/A6E+E4w8i+KM7nQCK7q 9 | K4JXzyXVAjLfyBZWHGM2uro/fjqPggGD6QH1qXCkI4MCQQDmdKeb2TrKRh5BY1LR 10 | 81aJGKcJ2XbcDu6wMZK4oqWbTX2KiYn9GB0woM6nSr/Y6iy1u145YzYxEV/iMwff 11 | DJULAkB8B2MnyzOg0pNFJqBJuH29bKCcHa8gHJzqXhNO5lAlEbMK95p/P2Wi+4Hd 12 | aiEIAF1BF326QJcvYKmwSmrORp85AkAlSNxRJ50OWrfMZnBgzVjDx3xG6KsFQVk2 13 | ol6VhqL6dFgKUORFUWBvnKSyhjJxurlPEahV6oo6+A+mPhFY8eUvAkAZQyTdupP3 14 | XEFQKctGz+9+gKkemDp7LBBMEMBXrGTLPhpEfcjv/7KPdnFHYmhYeBTBnuVmTVWe 15 | F98XJ7tIFfJq 16 | -----END PRIVATE KEY----- 17 | -------------------------------------------------------------------------------- /testing/certs/server1.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICnDCCAgWgAwIBAgIBBzANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJBVTET 3 | MBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQ 4 | dHkgTHRkMQ8wDQYDVQQDEwZ0ZXN0Y2EwHhcNMTUxMTA0MDIyMDI0WhcNMjUxMTAx 5 | MDIyMDI0WjBlMQswCQYDVQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNV 6 | BAcTB0NoaWNhZ28xFTATBgNVBAoTDEV4YW1wbGUsIENvLjEaMBgGA1UEAxQRKi50 7 | ZXN0Lmdvb2dsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOHDFSco 8 | LCVJpYDDM4HYtIdV6Ake/sMNaaKdODjDMsux/4tDydlumN+fm+AjPEK5GHhGn1Bg 9 | zkWF+slf3BxhrA/8dNsnunstVA7ZBgA/5qQxMfGAq4wHNVX77fBZOgp9VlSMVfyd 10 | 9N8YwbBYAckOeUQadTi2X1S6OgJXgQ0m3MWhAgMBAAGjazBpMAkGA1UdEwQCMAAw 11 | CwYDVR0PBAQDAgXgME8GA1UdEQRIMEaCECoudGVzdC5nb29nbGUuZnKCGHdhdGVy 12 | em9vaS50ZXN0Lmdvb2dsZS5iZYISKi50ZXN0LnlvdXR1YmUuY29thwTAqAEDMA0G 13 | CSqGSIb3DQEBCwUAA4GBAJFXVifQNub1LUP4JlnX5lXNlo8FxZ2a12AFQs+bzoJ6 14 | hM044EDjqyxUqSbVePK0ni3w1fHQB5rY9yYC5f8G7aqqTY1QOhoUk8ZTSTRpnkTh 15 | y4jjdvTZeLDVBlueZUTDRmy2feY5aZIU18vFDK08dTG0A87pppuv1LNIR3loveU8 16 | -----END CERTIFICATE----- 17 | --------------------------------------------------------------------------------