Janus is a multi-tenant observability proxy that provides label-based access control and query enforcement for Prometheus, Loki, and Tempo. It acts as a security layer between users and your observability stack, ensuring that users can only access data they're authorized to see.
- Multi-tenant Access Control: Enforces tenant isolation using JWT tokens and header-based authentication
- Label-based Security: Controls access to observability data based on labels (namespace, service, pod, etc.)
- Query Enforcement: Automatically enhances and validates queries to ensure compliance with access policies
- Proxy Layer: Transparently forwards requests to Prometheus (via Thanos), Loki, and Tempo
- High Performance: Built with Spring WebFlux for reactive, non-blocking I/O
- Prometheus/Thanos: Metrics queries and range queries
- Loki: Log queries with LogQL
- Tempo: Distributed tracing with TraceQL
Pre-built container images are available from the GitHub Container Registry:
docker pull ghcr.io/evoila/janus:latestJanus uses semantic versioning. The following tags are available:
| Tag | Example | Description |
|---|---|---|
latest |
ghcr.io/evoila/janus:latest |
Latest release |
<major> |
ghcr.io/evoila/janus:0 |
Latest release within a major version |
<major>.<minor> |
ghcr.io/evoila/janus:0.1 |
Latest release within a minor version |
<major>.<minor>.<patch> |
ghcr.io/evoila/janus:0.1.0 |
Specific release |
sha-<short-sha> |
ghcr.io/evoila/janus:sha-0257f1f |
Specific commit |
| Variable | Default | Description |
|---|---|---|
SPRING_PROFILES_ACTIVE |
all |
Spring profile to activate (see profiles section below) |
LOKI_URL |
http://localhost:8081 |
Loki service URL |
THANOS_URL |
http://localhost:10902 |
Thanos/Prometheus URL |
TEMPO_URL |
http://localhost:3100 |
Tempo service URL |
LABEL_STORE_TYPE |
configmap |
Label store type (config or configmap) |
LABEL_STORE_CONFIGMAP_PATH |
./local-configmap.yaml |
Path to ConfigMap file |
OAUTH2_ISSUER_URI |
Keycloak URL | OAuth2 issuer URI |
OAUTH2_JWK_SET_URI |
Keycloak JWKS URL | OAuth2 JWK Set URI for token validation |
OAUTH2_CUSTOM_CA_PATH |
(empty) | Path to custom CA certificate for OAuth2/Keycloak SSL connections |
Janus includes OpenTelemetry configuration in application.yml, but tracing is not currently active. The previous implementation using the OpenTelemetry Java Agent has been removed. Tracing may be reimplemented using the OpenTelemetry SDK in a future release.
| Variable | Default | Description |
|---|---|---|
OTEL_SERVICE_NAME |
janus |
Service name for telemetry |
OTEL_EXPORTER_OTLP_ENDPOINT |
http://localhost:4317 |
OTLP exporter endpoint |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc |
OTLP protocol (grpc/http) |
Janus supports different deployment modes through Spring profiles. Each profile starts only the specified service:
| Profile | Description | Port | Service |
|---|---|---|---|
all |
Default: Starts all services (Loki, Thanos, Tempo) | 8080 |
Complete proxy |
loki |
Starts only Loki proxy service | 8081 |
Loki only |
thanos |
Starts only Thanos/Prometheus proxy service | 8083 |
Thanos only |
tempo |
Starts only Tempo proxy service | 8085 |
Tempo only |
# Start all services (default)
./mvnw spring-boot:run
# Start only Loki proxy
SPRING_PROFILES_ACTIVE=loki ./mvnw spring-boot:run
# Start only Thanos proxy
SPRING_PROFILES_ACTIVE=thanos ./mvnw spring-boot:run
# Start only Tempo proxy
SPRING_PROFILES_ACTIVE=tempo ./mvnw spring-boot:run
# Using JAR file
java -jar target/janus-0.0.1-SNAPSHOT.jar --spring.profiles.active=lokiEach profile has its own configuration in application.yml:
---
# Profile for running all services together (monolithic)
spring:
config:
activate:
on-profile: all
server:
port: 8080
---
# Profile for running only Loki service
spring:
config:
activate:
on-profile: loki
server:
port: 8081
---
# Profile for running only Thanos service
spring:
config:
activate:
on-profile: thanos
server:
port: 8083
---
# Profile for running only Tempo service
spring:
config:
activate:
on-profile: tempo
server:
port: 8085The main configuration is in src/main/resources/application.yml:
proxy:
config:
enforcement:
enabled: true
tenant-claim: groups
skip-for-admins: true
admin-group: admin
services:
loki:
url: ${LOKI_URL:http://localhost:8081}
type: logql
thanos:
url: ${THANOS_URL:http://localhost:10902}
type: promql
tempo:
url: ${TEMPO_URL:http://localhost:3100}
type: traceqlJanus uses a hierarchical configuration system defined in a ConfigMap (or local YAML file):
# Global admin access
admin:
labels:
- "*"
header:
- X-Scope-OrgID: tenant1
cluster-wide: []
# Service-specific configurations
thanos:
tenant-header-constraints:
order-service-team:
header:
- X-Scope-OrgID: tenant1
user-label-constraints:
order-service-team:
labels:
- "*"
namespace:
- demo
service:
- "~.+"- Request Reception: Janus receives queries from clients (e.g., Grafana)
- Authentication: Validates JWT tokens and extracts tenant information
- Label Extraction: Parses the query to identify labels and constraints
- Policy Lookup: Finds applicable policies based on tenant and service type
- Query Enhancement: Automatically adds required labels to ensure compliance
- Validation: Verifies that the enhanced query meets access requirements
- Forwarding: Sends the enhanced query to the backend service
Janus supports any labels that your observability stack uses. The labels and their allowed values are fully configurable through the ConfigMap. Here are some common examples:
Prometheus/Thanos (Metrics)
thanos:
user-label-constraints:
team-a:
labels:
- namespace
- service
- pod
- instance
- job
- environment
- region
namespace:
- "team-a-namespace"
- "shared-namespace"
environment:
- "production"
- "staging"Loki (Logs)
loki:
user-label-constraints:
team-b:
labels:
- k8s_namespace_name
- service_name
- pod_name
- container_name
- log_level
- application
k8s_namespace_name:
- "team-b-namespace"
log_level:
- "ERROR"
- "WARN"Tempo (Traces)
tempo:
user-label-constraints:
team-c:
labels:
- resource.service.name
- resource.namespace
- resource.pod.name
- span.kind
- http.method
resource.service.name:
- "api-service"
- "web-service"- Dynamic: Labels are not hardcoded - you can configure any label your observability stack uses
- Flexible Values: Support for exact matches, regex patterns, and wildcards
- Service-Specific: Different label sets can be configured for each service (Prometheus, Loki, Tempo)
- Tenant-Based: Different teams/tenants can have different label access policies
- Runtime Updates: ConfigMap changes are automatically detected and applied without restart
Janus supports powerful wildcard and pattern matching features for flexible access control:
admin:
labels:
- "*" # Allows access to ALL labels without restrictionsThis configuration grants unrestricted access to all labels. Users can query any label without enforcement.
thanos:
user-label-constraints:
system-team:
labels:
- "*" # All labels allowed
service:
- "*" # Any service name
namespace:
- "*-system-*" # Namespaces containing "system"
- "ingress-nginx" # Exact match
- "otel-*" # Namespaces starting with "otel"
- "*-controller" # Namespaces ending with "controller"| Pattern | Description | Examples |
|---|---|---|
"*" |
Any value (wildcard) | service, pod, namespace |
"*-system-*" |
Contains "system" (wildcard) | my-system-app, system-monitoring |
"otel-*" |
Starts with "otel" (wildcard) | otel-collector, otel-agent |
"*-controller" |
Ends with "controller" (wildcard) | kube-controller, app-controller |
"~.+" |
Regex: one or more characters | Any non-empty value |
"~.*" |
Regex: zero or more characters | Any value (including empty) |
"~5.." |
Regex: starts with "5" | 500, 501, 502 |
loki:
user-label-constraints:
monitoring-team:
labels:
- "*"
k8s_namespace_name:
- "prod-*" # Production namespaces
- "staging-*" # Staging namespaces
- "monitoring" # Exact match
log_level:
- "ERROR"
- "WARN"
- "INFO"
application:
- "*-api" # API applications
- "web-*" # Web applications
- "backend-*" # Backend servicesJanus supports different operators for label matching:
=(equals): Exact match=~(regex match): Pattern matching!=(not equals): Exclude exact value!~(regex not match): Exclude pattern
# Examples with operators
thanos:
user-label-constraints:
team-a:
namespace:
- "demo" # namespace="demo"
- "prod-*" # namespace=~"prod-.*"
status:
- "!~5.." # status!~"5.." (exclude 5xx errors - prefix notation)
- "!=error" # status!="error" (prefix notation)For complex regex patterns with anchors (^, $), character classes ([a-z]), or other advanced regex features, use the ~ prefix to mark values as explicit regex patterns:
thanos:
user-label-constraints:
# Team with access to services matching complex patterns
regex-team:
labels:
- "*"
namespace:
- "~^[a-z]+-service$" # Matches: order-service, payment-service
- "~^demo-.*$" # Matches: demo-app, demo-api, demo-service
- "~.*-production$" # Matches: app-production, api-production
# Multi-pattern access (union of patterns)
multi-access-team:
labels:
- "*"
namespace:
- "~^demo.*$" # Starts with "demo"
- "~.*-service$" # Ends with "-service"Key differences between wildcards and explicit regex:
| Config Value | Type | Stored As | Query Generated |
|---|---|---|---|
"demo" |
Exact match | demo |
namespace="demo" |
"demo-*" |
Wildcard | demo-* |
namespace=~"demo-.*" |
"~^demo.*$" |
Explicit regex | ^demo.*$ |
namespace=~"^demo.*$" |
"~^[a-z]+-service$" |
Explicit regex | ^[a-z]+-service$ |
namespace=~"^[a-z]+-service$" |
Note: Values with
~prefix must be quoted in YAML. The~is stripped during config loading and the remaining pattern is used as-is in queries.
- Java 25 or higher
- Maven 3.9+
- Access to Prometheus/Thanos, Loki, and Tempo services
-
Clone and Build
git clone https://github.com/evoila/janus.git cd janus ./mvnw clean package -
Configure Services
# Copy the example configuration cp config-example.yaml ./local-configmap.yaml # Edit the file to match your environment
-
Run Janus
# Run as JAR java -jar target/janus-0.0.1-SNAPSHOT.jar # Or run with Maven ./mvnw spring-boot:run
-
Test the Setup
# Health check curl http://localhost:8080/actuator/health # Test query with GET (most common) curl "http://localhost:8080/api/v1/query?query=up" \ -H "Authorization: Bearer YOUR_JWT_TOKEN" # Test query with POST (for long queries) curl -X POST http://localhost:8080/api/v1/query \ -H "Authorization: Bearer YOUR_JWT_TOKEN" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "query=up"
# Build native image
./mvnw -Pnative native:compile
# Run with Docker
docker build -f Dockerfile.native-build -t janus .
docker run -p 8080:8080 janus- JWT Token Validation: Validates OAuth2 JWT tokens
- Tenant Extraction: Extracts tenant information from JWT claims
- Header-based Routing: Uses custom headers for tenant identification
- Label-based Access Control: Controls access based on data labels
- Query Enforcement: Automatically enhances queries with required labels
- Admin Bypass: Allows admin users to bypass restrictions
- Wildcard Support: Supports regex patterns for flexible matching
- Request Logging: Logs all incoming requests and responses
- Query Enhancement Tracking: Tracks how queries are modified
- Performance Metrics: Exposes metrics via Prometheus endpoint
Janus includes comprehensive test coverage:
- Unit Tests: Core logic, query parsing, label enforcement
- Integration Tests: WireMock-based tests against simulated Prometheus/Thanos, Loki, and Tempo backends
- E2E Tests: Full request/response cycle testing with realistic configurations
| Backend | Tested | Notes |
|---|---|---|
| Prometheus | β Automated | E2E tests + production usage |
| Loki | β Automated | E2E tests + production usage |
| Tempo | β Automated | E2E tests + production usage |
| Thanos | Production usage (Prometheus-compatible) | |
| Mimir | β Untested | Should work (Prometheus-compatible API) |
| VictoriaMetrics | β Untested | Should work (Prometheus-compatible API) |
Janus is actively used in multiple production environments with Thanos, Loki, and Tempo. Mimir and VictoriaMetrics use Prometheus-compatible APIs and should work without modifications, but are not part of our test suite.
Note: Thanos is not included in automated E2E tests due to its API compatibility with Prometheus (making separate tests redundant) and the complexity of its multi-component architecture for test environments.
# Run all tests
./mvnw test
# Run only unit tests
./mvnw test -DexcludedGroups=e2e
# Run only E2E tests
./mvnw test -Dgroups=e2e# Test simple query (GET)
curl "http://localhost:8080/api/v1/query?query=up" \
-H "Authorization: Bearer YOUR_TOKEN"
# Test range query (GET)
curl "http://localhost:8080/api/v1/query_range?query=up&start=1234567890&end=1234567899&step=15s" \
-H "Authorization: Bearer YOUR_TOKEN"
# Test query with labels (GET - URL encoded)
curl "http://localhost:8080/api/v1/query?query=http_requests_total%7Bnamespace%3D%22demo%22%7D" \
-H "Authorization: Bearer YOUR_TOKEN"
# Test with POST (for long/complex queries)
curl -X POST http://localhost:8080/api/v1/query \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'query=sum(rate(http_requests_total{namespace="demo"}[5m]))'src/
βββ main/java/com/evoila/janus/
β βββ app/ # Application configuration
β βββ common/ # Shared components
β β βββ config/ # Configuration classes
β β βββ controller/ # Base controllers
β β βββ enforcement/ # Label enforcement logic
β β βββ labelstore/ # Label configuration storage
β β βββ pipeline/ # Request processing pipeline
β β βββ service/ # Core services
β βββ loki/ # Loki-specific components
β βββ proxy/ # HTTP client configuration
β βββ security/ # Security configuration
β βββ tempo/ # Tempo-specific components
β βββ thanos/ # Thanos-specific components
βββ test/ # Test resources
# Compile
./mvnw compile
# Package
./mvnw package
# Run tests
./mvnw test
# Format code
./mvnw spotless:apply- SonarQube: Static code analysis
- Spotless: Code formatting with Google Java Format
- JaCoCo: Code coverage reporting
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the GNU Affero General Public License v3.0 (AGPLv3).
For issues and questions:
- Create an issue in the repository
- Check the documentation
- Review the configuration examples
Janus - Secure your observability stack with confidence!
Why "Janus"? Every observability tool seems to be named after a god Prometheus, Loki, Thanos. We needed a gatekeeper, so we went with the Roman god of doorways and gates. Plus, he has two faces, which felt appropriate for a proxy that sees both sides of every request.

