Skip to content
Merged
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
30 changes: 17 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,22 +221,26 @@ Check out the docker-compose folder

### Kubernetes/Helm

You can install the included [Helm](https://docs.helm.sh/install/) chart to your k8s cluster with:
Deploy PageSpeed Exporter to Kubernetes using the included [Helm](https://helm.sh/) chart. First, create a secret with your API key (only required if monitoring more than 2 targets/sec):

```bash
kubectl create secret generic pagespeed-configuration-secret \
--from-literal=PAGESPEED_API_KEY=your-api-key-here
```
$ helm install helm/pagespeed-exporter
```

And then, to quickly test it:
Install the chart with your target URLs:

```bash
helm install pagespeed-exporter ./helm/pagespeed-exporter \
--set 'config.targets={https://www.example.com,https://www.yoursite.com}' \
--set config.parallel=true
```
$ kubectl get pods
pagespeed-exporter-riotous-dragonfly-6b99955999-hj2kw 1/1 Running 0 1m

$ kubectl exec -ti pagespeed-exporter-riotous-dragonfly-6b99955999-hj2kw -- sh
# apk add curl
# curl localhost:9271/metrics
pagespeed_lighthouse_audit_score{audit="first-contentful-paint",host="https://www.google.com",path="/",strategy="mobile"} 1
pagespeed_lighthouse_audit_score{audit="first-contentful-paint",host="https://www.google.com",path="/webhp",strategy="desktop"} 1
pagespeed_lighthouse_audit_score{audit="first-contentful-paint",host="https://www.google.com",path="/webhp",strategy="mobile"} 1
...
Test the deployment:

```bash
kubectl port-forward svc/pagespeed-exporter 9271:9271
curl http://localhost:9271/metrics
```

For detailed configuration options, installation examples, and Prometheus integration, see the [Helm Chart README](helm/pagespeed-exporter/README.md).
64 changes: 0 additions & 64 deletions collector/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,76 +160,12 @@ func collectLoadingExperience(prefix string, lexp *pagespeedonline.PagespeedApiL
for k, v := range lexp.Metrics {
name := strings.TrimSuffix(strings.ToLower(k), "_ms")

// Export P75 percentile (existing metric - unchanged)
ch <- prometheus.MustNewConstMetric(
prometheus.NewDesc(fqname(prefix, "metrics", name, "duration_seconds"), "Percentile metrics for "+strings.Replace(name, "_", " ", -1), nil, constLables),
prometheus.GaugeValue,
float64(v.Percentile)/1000)

// Export category distribution ratios (NEW)
if len(v.Distributions) >= 3 {
categories := []string{"fast", "average", "slow"}
for i, dist := range v.Distributions {
if i >= 3 {
break
}
ch <- prometheus.MustNewConstMetric(
prometheus.NewDesc(fqname(prefix, "metrics", name, "category_ratio"), "Proportion of users experiencing "+categories[i]+" performance", []string{"category"}, constLables),
prometheus.GaugeValue,
dist.Proportion,
categories[i])
}
}

// Export Core Web Vitals thresholds (NEW)
if len(v.Distributions) >= 2 {
// Determine if this is CLS (uses hundredths) or time-based (uses ms)
isCLS := strings.Contains(strings.ToLower(k), "cumulative_layout_shift")

// Extract good threshold (upper bound of FAST bucket)
if v.Distributions[0].Max > 0 {
goodThreshold := float64(v.Distributions[0].Max)
if isCLS {
goodThreshold = goodThreshold / 100.0 // Convert hundredths to decimal
} else {
goodThreshold = goodThreshold / 1000.0 // Convert ms to seconds
}

metricSuffix := "duration_seconds"
if isCLS {
metricSuffix = "" // CLS is unitless
}

ch <- prometheus.MustNewConstMetric(
prometheus.NewDesc(fqname(prefix, "metrics", name, "threshold", metricSuffix), "Core Web Vitals threshold for "+strings.Replace(name, "_", " ", -1), []string{"threshold"}, constLables),
prometheus.GaugeValue,
goodThreshold,
"good")
}

// Extract poor threshold (upper bound of AVERAGE bucket)
if v.Distributions[1].Max > 0 {
poorThreshold := float64(v.Distributions[1].Max)
if isCLS {
poorThreshold = poorThreshold / 100.0
} else {
poorThreshold = poorThreshold / 1000.0
}

metricSuffix := "duration_seconds"
if isCLS {
metricSuffix = ""
}

ch <- prometheus.MustNewConstMetric(
prometheus.NewDesc(fqname(prefix, "metrics", name, "threshold", metricSuffix), "Core Web Vitals threshold for "+strings.Replace(name, "_", " ", -1), []string{"threshold"}, constLables),
prometheus.GaugeValue,
poorThreshold,
"poor")
}
}
}

}

func collectLighthouseResults(prefix string, cats []string, lhr *pagespeedonline.LighthouseResultV5, constLabels prometheus.Labels, ch chan<- prometheus.Metric) {
Expand Down
99 changes: 99 additions & 0 deletions collector/collector_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package collector

import (
"os"
"path/filepath"
"reflect"
"testing"

"github.com/joho/godotenv"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/api/option"
)

func loadTestEnv() {
envPath := filepath.Join("..", ".env")
_ = godotenv.Load(envPath)
}

func Test_getConstLabels(t *testing.T) {
type args struct {
scrape *ScrapeResult
Expand Down Expand Up @@ -91,3 +100,93 @@ func Test_fqname(t *testing.T) {
})
}
}

func Test_CollectorIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}

loadTestEnv()

var options []option.ClientOption

if apiKey := os.Getenv("PAGESPEED_API_KEY"); apiKey != "" {
options = append(options, option.WithAPIKey(apiKey))
}

if cf := os.Getenv("PAGESPEED_CREDENTIALS_FILE"); cf != "" {
options = append(options, option.WithCredentialsFile(cf))
}

if len(options) == 0 {
t.Skip("skipping integration test unless PAGESPEED_API_KEY or PAGESPEED_CREDENTIALS_FILE is set")
}

config := Config{
ScrapeRequests: []ScrapeRequest{
{
Url: "https://www.example.com",
Strategy: StrategyMobile,
Categories: []string{CategoryPerformance},
},
},
GoogleAPIKey: os.Getenv("PAGESPEED_API_KEY"),
Parallel: false,
}

if cf := os.Getenv("PAGESPEED_CREDENTIALS_FILE"); cf != "" {
config.CredentialsFile = cf
}

coll, err := newCollector(config)
if err != nil {
t.Fatalf("failed to create collector: %v", err)
}

ch := make(chan prometheus.Metric, 100)
done := make(chan bool)

var metrics []prometheus.Metric
go func() {
for m := range ch {
metrics = append(metrics, m)
}
done <- true
}()

coll.Collect(ch)
close(ch)
<-done

if len(metrics) == 0 {
t.Fatal("expected at least one metric to be collected")
}

t.Logf("Successfully collected %d metrics from https://www.example.com", len(metrics))

hasExpectedLabels := false
for _, m := range metrics {
desc := m.Desc().String()
if contains(desc, "host") && contains(desc, "path") && contains(desc, "strategy") {
hasExpectedLabels = true
break
}
}

if !hasExpectedLabels {
t.Error("expected metrics to contain host, path, and strategy labels")
}
}

func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsSubstring(s, substr)))
}

func containsSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.14.0 h1:f+jMrjBPl+DL9nI4IQzLUxMq7XrAqFYB7hBPqMNIe8o=
github.com/googleapis/gax-go/v2 v2.14.0/go.mod h1:lhBCnjdLrWRaPvLWhmc8IS24m9mr07qSYnHncrgo+zk=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down
2 changes: 2 additions & 0 deletions helm/pagespeed-exporter/.helmignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
18 changes: 14 additions & 4 deletions helm/pagespeed-exporter/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
apiVersion: v1
description: Pagespeed Exporter
apiVersion: v2
name: pagespeed-exporter
version: 2.1.6
appVersion: 2.1.6
description: A Helm chart for deploying PageSpeed Exporter to monitor website performance metrics
type: application
version: 0.1.0
appVersion: "3.1.2"
keywords:
- monitoring
- prometheus
- pagespeed
- metrics
maintainers:
- name: Infrastructure Team
sources:
- https://github.com/foomo/pagespeed_exporter
Loading
Loading