Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ vendor/
.idea/
.env
venom*
prometheus-data/
37 changes: 37 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1.6
# Build stage
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder

ARG TARGETOS
ARG TARGETARCH

WORKDIR /app

# Copy go mod files
COPY go.mod go.sum ./

# Download dependencies
RUN go mod download

# Copy source code
COPY *.go ./

# Build the binary with static linking
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags="-w -s -extldflags '-static'" -o cloudflare_exporter .

# Final stage
FROM alpine:latest

# Install ca-certificates for HTTPS calls to Cloudflare API
RUN apk --no-cache add ca-certificates

WORKDIR /root/

# Copy the binary from builder
COPY --from=builder /app/cloudflare_exporter .

# Expose metrics port
EXPOSE 8080

# Run the exporter
ENTRYPOINT ["./cloudflare_exporter"]
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ The exporter can be configured using env variables or command flags.
| `SCRAPE_INTERVAL` | scrape interval in seconds (will query cloudflare every SCRAPE_INTERVAL seconds), default `60` |
| `METRICS_DENYLIST` | (Optional) cloudflare-exporter metrics to not export, comma delimited list of cloudflare-exporter metrics. If not set, all metrics are exported |
| `ENABLE_PPROF` | (Optional) enable pprof profiling endpoints at `/debug/pprof/`. Accepts `true` or `false`, default `false`. **Warning**: Only enable in development/debugging environments |
| `ENABLE_PATH_METRICS` | (Optional) enable path-level request metrics grouped by zone, path, and status. Accepts `true` or `false`, default `false`. Disabled for free-tier zones. |
| `PATH_METRICS_LIMIT` | (Optional) maximum number of paths to track per zone. Default `100`, min `1`, max `1000`. Only used when `ENABLE_PATH_METRICS=true`. |
| `PATH_METRICS_STATUS_FILTER` | (Optional) filter which HTTP status codes to collect for path metrics. Supports individual codes (`404,500`), ranges (`300-499`), or combinations (`404,500-599`). Empty (default) collects all status codes. Only used when `ENABLE_PATH_METRICS=true`. |
| `ZONE_<NAME>` | `DEPRECATED since 0.0.5` (optional) Zone ID. Add zones you want to scrape by adding env vars in this format. You can find the zone ids in Cloudflare dashboards. |
| `LOG_LEVEL` | Set loglevel. Options are error, warn, info, debug. default `error` |

Expand All @@ -86,6 +89,9 @@ Corresponding flags:
-scrape_interval=60: scrape interval in seconds, defaults to 60
-metrics_denylist="": cloudflare-exporter metrics to not export, comma delimited list
-enable_pprof=false: enable pprof profiling endpoints at /debug/pprof/
-enable_path_metrics=false: enable path-level metrics grouped by zone, path, and status
-path_metrics_limit=100: maximum number of paths to track per zone (1-1000)
-path_metrics_status_filter="": HTTP status codes to collect (e.g., '404,500' or '300-499' or '404,500-599')
-log_level="error": log level(error,warn,info,debug)
```

Expand Down Expand Up @@ -114,6 +120,7 @@ Note: `ZONE_<name>` configuration is not supported as flag.
# HELP cloudflare_zone_requests_ssl_encrypted Number of encrypted requests for zone
# HELP cloudflare_zone_requests_status Number of request for zone per HTTP status
# HELP cloudflare_zone_requests_status_country_host Count of requests for zone per edge HTTP status per country per host
# HELP cloudflare_zone_requests_status_path Number of requests for zone per HTTP status per path
# HELP cloudflare_zone_requests_browser_map_page_views_count Number of successful requests for HTML pages per zone
# HELP cloudflare_zone_requests_total Number of requests for zone
# HELP cloudflare_zone_threats_country Threats per zone per country
Expand Down Expand Up @@ -179,6 +186,24 @@ Disable non-free metrics:
docker run --rm -p 8080:8080 -e CF_API_TOKEN=${CF_API_TOKEN} -e FREE_TIER=true ghcr.io/lablabs/cloudflare_exporter
```

Enable path-level metrics:

```
docker run --rm -p 8080:8080 -e CF_API_TOKEN=${CF_API_TOKEN} -e ENABLE_PATH_METRICS=true -e PATH_METRICS_LIMIT=50 ghcr.io/lablabs/cloudflare_exporter
```

Filter path metrics to only collect errors and redirects (3xx and 4xx/5xx):

```
docker run --rm -p 8080:8080 -e CF_API_TOKEN=${CF_API_TOKEN} -e ENABLE_PATH_METRICS=true -e PATH_METRICS_STATUS_FILTER="300-599" ghcr.io/lablabs/cloudflare_exporter
```

Filter path metrics to only collect specific status codes (404 and 500):

```
docker run --rm -p 8080:8080 -e CF_API_TOKEN=${CF_API_TOKEN} -e ENABLE_PATH_METRICS=true -e PATH_METRICS_STATUS_FILTER="404,500" ghcr.io/lablabs/cloudflare_exporter
```

Access help:

```
Expand Down
30 changes: 29 additions & 1 deletion charts/cloudflare-exporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ The following table lists the configurable parameters of the Cloudflare-exporter
| `image.repository` | | `"ghcr.io/lablabs/cloudflare_exporter"` |
| `image.pullPolicy` | | `"Always"` |
| `image.tag` | | `"0.0.2"` |
| `env` | | `[]` |
| `env` | Environment variables for the exporter | `[]` |
| `secretRef` | The name of a secret with environment variables | `""` |
| `pathMetrics.enabled` | Enable path-level metrics grouped by zone, path, and HTTP status code | `false` |
| `pathMetrics.limit` | Maximum number of paths to track per zone (1-1000) | `100` |
| `imagePullSecrets` | | `[]` |
| `nameOverride` | | `""` |
| `fullnameOverride` | | `""` |
Expand All @@ -49,7 +51,33 @@ The following table lists the configurable parameters of the Cloudflare-exporter
| `tolerations` | | `[]` |
| `affinity` | | `{}` |

## Usage Examples

### Enable Path Metrics

To enable path-level metrics for tracking HTTP requests by zone, path, and status code:

```yaml
pathMetrics:
enabled: true
limit: 100
```

Note: Path metrics can generate significant cardinality depending on your traffic patterns. The `limit` parameter controls the maximum number of unique paths tracked per zone (top N by request count). This feature is not recommended for free-tier Cloudflare accounts.

### Configure with Custom Environment Variables

```yaml
env:
- name: CF_API_TOKEN
value: "your-api-token"
- name: FREE_TIER
value: "false"

pathMetrics:
enabled: true
limit: 50
```

## Contributing and reporting issues

Expand Down
6 changes: 6 additions & 0 deletions charts/cloudflare-exporter/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ spec:
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- if .Values.pathMetrics.enabled }}
- name: ENABLE_PATH_METRICS
value: "true"
- name: PATH_METRICS_LIMIT
value: {{ .Values.pathMetrics.limit | quote }}
{{- end }}
{{- toYaml .Values.env | nindent 12 }}
{{- if .Values.secretRef }}
envFrom:
Expand Down
26 changes: 26 additions & 0 deletions charts/cloudflare-exporter/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,34 @@ image:
pullPolicy: Always
# Overrides the image tag whose default is the chart appVersion.
# tag: latest

# Environment variables for the exporter
# Example configuration:
# env:
# - name: CF_API_KEY
# value: "your-api-key"
# - name: CF_API_EMAIL
# value: "your-email@example.com"
# - name: ENABLE_PATH_METRICS
# value: "true"
# - name: PATH_METRICS_LIMIT
# value: "100"
env: []

# Reference to a secret containing environment variables
# Useful for sensitive data like CF_API_KEY or CF_API_TOKEN
secretRef: ""

# Path metrics configuration
# Enable path-level metrics grouped by zone, path, and HTTP status code
# Note: This feature is disabled by default and not recommended for free-tier accounts
# as it can generate significant cardinality
pathMetrics:
# Enable path metrics collection
enabled: false
# Maximum number of paths to track per zone (1-1000)
# Higher values increase cardinality and resource usage
limit: 100
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
Expand Down
64 changes: 64 additions & 0 deletions cloudflare.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ type cloudflareResponseLogpushZone struct {
} `json:"viewer"`
}

type cloudflareResponsePathMetrics struct {
Viewer struct {
Zones []zoneRespPathMetrics `json:"zones"`
} `json:"viewer"`
}

type zoneRespPathMetrics struct {
HTTPRequestsPathStatusGroups []struct {
Count uint64 `json:"count"`
Dimensions struct {
ClientRequestPath string `json:"clientRequestPath"`
EdgeResponseStatus uint16 `json:"edgeResponseStatus"`
} `json:"dimensions"`
} `json:"httpRequestsAdaptiveGroups"`
ZoneTag string `json:"zoneTag"`
}

type logpushResponse struct {
LogpushHealthAdaptiveGroups []struct {
Count uint64 `json:"count"`
Expand Down Expand Up @@ -851,6 +868,53 @@ func fetchLogpushZone(zoneIDs []string) (*cloudflareResponseLogpushZone, error)
return &resp, nil
}

func fetchPathMetrics(zoneIDs []string, limit int) (*cloudflareResponsePathMetrics, error) {
request := graphql.NewRequest(`
query ($zoneIDs: [String!], $mintime: Time!, $maxtime: Time!, $limit: Int!) {
viewer {
zones(filter: { zoneTag_in: $zoneIDs }) {
zoneTag
httpRequestsAdaptiveGroups(
limit: $limit
filter: {
datetime_geq: $mintime,
datetime_lt: $maxtime,
requestSource: "eyeball"
}
orderBy: [count_DESC]
) {
count
dimensions {
clientRequestPath
edgeResponseStatus
}
}
}
}
}
`)

now, now1mAgo := GetTimeRange()
request.Var("limit", limit)
request.Var("maxtime", now)
request.Var("mintime", now1mAgo)
request.Var("zoneIDs", zoneIDs)

gql.Mu.RLock()
defer gql.Mu.RUnlock()

ctx, cancel := context.WithTimeout(context.Background(), cftimeout)
defer cancel()

var resp cloudflareResponsePathMetrics
if err := gql.Client.Run(ctx, request, &resp); err != nil {
log.Errorf("error fetching path metrics, err:%v", err)
return nil, err
}

return &resp, nil
}

func fetchR2Account(accountID string) (*cloudflareResponseR2Account, error) {
request := graphql.NewRequest(`query($accountID: String!, $limit: Int!, $date: String!) {
viewer {
Expand Down
47 changes: 47 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
version: '3.8'

services:
cloudflare-exporter:
build:
context: .
dockerfile: Dockerfile
image: cloudflare-exporter:latest
container_name: cloudflare-exporter
ports:
- "8090:8080"
environment:
- CF_API_TOKEN=${CF_API_TOKEN}
- ENABLE_PATH_METRICS=true
- PATH_METRICS_LIMIT=100
- PATH_METRICS_STATUS_FILTER=300-503
restart: unless-stopped
networks:
- monitoring

prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/usr/share/prometheus/console_libraries'
- '--web.console.templates=/usr/share/prometheus/consoles'
- '--web.enable-lifecycle'
restart: unless-stopped
networks:
- monitoring
depends_on:
- cloudflare-exporter

volumes:
prometheus-data:
driver: local

networks:
monitoring:
driver: bridge
3 changes: 3 additions & 0 deletions env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Cloudflare API Key
# Get your API key from: https://dash.cloudflare.com/profile/api-tokens
CF_API_KEY=your_cloudflare_api_key_here
32 changes: 32 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func fetchMetrics() {
go fetchZoneColocationAnalytics(filteredZones, &wg)
go fetchLoadBalancerAnalytics(filteredZones, &wg)
go fetchLogpushAnalyticsForZone(filteredZones, &wg)
go fetchZonePathAnalytics(filteredZones, &wg)
} else if zoneCount > cfgraphqlreqlimit {
for s := 0; s < zoneCount; s += cfgraphqlreqlimit {
e := s + cfgraphqlreqlimit
Expand All @@ -140,6 +141,7 @@ func fetchMetrics() {
go fetchZoneColocationAnalytics(filteredZones[s:e], &wg)
go fetchLoadBalancerAnalytics(filteredZones[s:e], &wg)
go fetchLogpushAnalyticsForZone(filteredZones[s:e], &wg)
go fetchZonePathAnalytics(filteredZones[s:e], &wg)
}
}

Expand Down Expand Up @@ -264,6 +266,31 @@ func main() {
viper.BindEnv("enable_pprof")
viper.SetDefault("enable_pprof", false)

flags.Bool("enable_path_metrics", false, "enable path-level metrics grouped by zone, path, and status")
viper.BindEnv("enable_path_metrics")
viper.SetDefault("enable_path_metrics", false)

flags.Int("path_metrics_limit", 100, "maximum number of paths to track per zone (1-1000)")
viper.BindEnv("path_metrics_limit")
viper.SetDefault("path_metrics_limit", 100)

flags.String("path_metrics_status_filter", "", "HTTP status codes to collect for path metrics (e.g., '404,500' or '300-499' or '404,500-599'). Empty means all statuses.")
viper.BindEnv("path_metrics_status_filter")
viper.SetDefault("path_metrics_status_filter", "")

// Path normalization (to reduce cardinality of path metrics)
flags.Bool("path_normalize_enabled", false, "enable normalization of path labels for path metrics")
viper.BindEnv("path_normalize_enabled")
viper.SetDefault("path_normalize_enabled", true)

flags.Int("path_keep_segments", 0, "keep only the first N path segments (0 keeps all)")
viper.BindEnv("path_keep_segments")
viper.SetDefault("path_keep_segments", 1)

flags.Bool("path_collapse_uuid", false, "collapse UUID-like path segments to :uuid")
viper.BindEnv("path_collapse_uuid")
viper.SetDefault("path_collapse_uuid", false)

viper.BindPFlags(flags)

logLevel := viper.GetString("log_level")
Expand Down Expand Up @@ -291,6 +318,11 @@ func main() {

cftimeout = viper.GetDuration("cf_timeout")

// Validate path metrics limit
if viper.GetInt("path_metrics_limit") < 1 || viper.GetInt("path_metrics_limit") > 1000 {
log.Fatal("path_metrics_limit must be between 1 and 1000")
}

if len(viper.GetString("cf_api_token")) > 0 {
cfclient = cf.NewClient(
cfoption.WithAPIToken(viper.GetString("cf_api_token")),
Expand Down
Loading