├── .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 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/copyright/Apache.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/detekt.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/ktlint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/.idea/php.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.run/Agent (no auth).run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.run/Proxy (auth).run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.run/Proxy (no auth).run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
--------------------------------------------------------------------------------