Add OpenTelemetry metrics instrumentation (#290)
## Motivation and Context - It is important to have basic observability in place so that availability issues can be figured out easily ## How Has This Been Tested? - Registration of metrics is tested - Prometheus endpoint `/metrics` is included in test cases to test the metrics exposed - Test locally to understand any regression. ## Breaking Changes - No ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [x] New and existing tests pass locally - [x] I have added appropriate error handling - [ ] I have added or updated documentation as needed ## Additional context - Telemetry implementation is currently done by keeping metrics in mind as of now, other types of telemetry can be added on top of it later. - Instrumentation takes care of these problems already: - Cardinality control by limiting latency buckets and not including labels that grows with throughput. - Latency buckets are added as per SLA guidelines - Each metric prefixed with `mcp` to easily identify. - Pulling mechanism is used since the idea is to scrape metrics via an agent like vmagent, prometheus agent or otel-collector. - Metrics follow standard approach of defining counters, histogram and gauges as per opentelemetry guidelines.
This commit is contained in:
parent
12ab632d32
commit
d237fc90f8
11 changed files with 538 additions and 21 deletions
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/modelcontextprotocol/registry/internal/database"
|
||||
"github.com/modelcontextprotocol/registry/internal/model"
|
||||
"github.com/modelcontextprotocol/registry/internal/service"
|
||||
"github.com/modelcontextprotocol/registry/internal/telemetry"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
|
@ -91,8 +92,20 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
shutdownTelemetry, metrics, err := telemetry.InitMetrics(cfg.Version)
|
||||
if err != nil {
|
||||
log.Printf("Failed to initialize metrics: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := shutdownTelemetry(context.Background()); err != nil {
|
||||
log.Printf("Failed to shutdown telemetry: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Initialize HTTP server
|
||||
server := api.NewServer(cfg, registryService)
|
||||
server := api.NewServer(cfg, registryService, metrics)
|
||||
|
||||
// Start server in a goroutine so it doesn't block signal handling
|
||||
go func() {
|
||||
|
|
|
|||
26
go.mod
26
go.mod
|
|
@ -5,29 +5,47 @@ go 1.24.0
|
|||
require (
|
||||
github.com/caarlos0/env/v11 v11.3.1
|
||||
github.com/danielgtaylor/huma/v2 v2.34.1
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.mongodb.org/mongo-driver v1.17.3
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0
|
||||
go.opentelemetry.io/otel v1.37.0
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.59.1
|
||||
go.opentelemetry.io/otel/metric v1.37.0
|
||||
go.opentelemetry.io/otel/sdk v1.37.0
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
55
go.sum
55
go.sum
|
|
@ -1,10 +1,18 @@
|
|||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
|
||||
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/danielgtaylor/huma/v2 v2.34.1 h1:EmOJAbzEGfy0wAq/QMQ1YKfEMBEfE94xdBRLPBP0gwQ=
|
||||
github.com/danielgtaylor/huma/v2 v2.34.1/go.mod h1:ynwJgLk8iGVgoaipi5tgwIQ5yoFNmiu+QdhU7CEEmhk=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
|
|
@ -13,21 +21,34 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
|
||||
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f h1:QQB6SuvGZjK8kdc2YaLJpYhV8fxauOsjE6jgcL6YJ8Q=
|
||||
github.com/prometheus/otlptranslator v0.0.0-20250717125610-8549f4ab4f8f/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
|
||||
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
|
|
@ -45,6 +66,24 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS
|
|||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
||||
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0 h1:ZIt0ya9/y4WyRIzfLC8hQRRsWg0J9M9GyaGtIMiElZI=
|
||||
go.opentelemetry.io/contrib/instrumentation/runtime v0.62.0/go.mod h1:F1aJ9VuiKWOlWwKdTYDUp1aoS0HzQxg38/VLxKmhm5U=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.59.1 h1:HcpSkTkJbggT8bjYP+BjyqPWlD17BH9C5CYNKeDzmcA=
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.59.1/go.mod h1:0FJL+gjuUoM07xzik3KPBaN+nz/CoB15kV6WLMiXZag=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
|
|
@ -62,6 +101,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
@ -74,6 +115,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
|||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import (
|
|||
"net/http"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
"github.com/modelcontextprotocol/registry/internal/telemetry"
|
||||
)
|
||||
|
||||
// HealthBody represents the health check response body
|
||||
|
|
@ -15,7 +19,7 @@ type HealthBody struct {
|
|||
}
|
||||
|
||||
// RegisterHealthEndpoint registers the health check endpoint
|
||||
func RegisterHealthEndpoint(api huma.API, cfg *config.Config) {
|
||||
func RegisterHealthEndpoint(api huma.API, cfg *config.Config, metrics *telemetry.Metrics) {
|
||||
huma.Register(api, huma.Operation{
|
||||
OperationID: "get-health",
|
||||
Method: http.MethodGet,
|
||||
|
|
@ -23,7 +27,10 @@ func RegisterHealthEndpoint(api huma.API, cfg *config.Config) {
|
|||
Summary: "Health check",
|
||||
Description: "Check the health status of the API",
|
||||
Tags: []string{"health"},
|
||||
}, func(_ context.Context, _ *struct{}) (*Response[HealthBody], error) {
|
||||
}, func(ctx context.Context, _ *struct{}) (*Response[HealthBody], error) {
|
||||
// Record the health check metrics
|
||||
recordHealthMetrics(ctx, metrics, "/v0/health", cfg.Version)
|
||||
|
||||
return &Response[HealthBody]{
|
||||
Body: HealthBody{
|
||||
Status: "ok",
|
||||
|
|
@ -32,3 +39,15 @@ func RegisterHealthEndpoint(api huma.API, cfg *config.Config) {
|
|||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
// recordHealthMetrics records the health check metrics
|
||||
func recordHealthMetrics(ctx context.Context, metrics *telemetry.Metrics, path string, version string) {
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String("path", path),
|
||||
attribute.String("version", version),
|
||||
attribute.String("service", telemetry.Namespace),
|
||||
}
|
||||
|
||||
// metric : Up status (1 = healthy, 0 = unhealthy)
|
||||
metrics.Up.Record(ctx, 1, metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
package v0_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/modelcontextprotocol/registry/internal/telemetry"
|
||||
)
|
||||
|
||||
func TestHealthEndpoint(t *testing.T) {
|
||||
|
|
@ -50,8 +53,10 @@ func TestHealthEndpoint(t *testing.T) {
|
|||
mux := http.NewServeMux()
|
||||
api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0"))
|
||||
|
||||
shutdownTelemetry, metrics, _ := telemetry.InitMetrics("test")
|
||||
|
||||
// Register the health endpoint
|
||||
v0.RegisterHealthEndpoint(api, tc.config)
|
||||
v0.RegisterHealthEndpoint(api, tc.config, metrics)
|
||||
|
||||
// Create a test request
|
||||
req := httptest.NewRequest(http.MethodGet, "/v0/health", nil)
|
||||
|
|
@ -60,6 +65,9 @@ func TestHealthEndpoint(t *testing.T) {
|
|||
// Serve the request
|
||||
mux.ServeHTTP(w, req)
|
||||
|
||||
// shut down the metric provider
|
||||
_ = shutdownTelemetry(context.Background())
|
||||
|
||||
// Check the status code
|
||||
assert.Equal(t, tc.expectedStatus, w.Code)
|
||||
|
||||
|
|
|
|||
91
internal/api/handlers/v0/telemetry_test.go
Normal file
91
internal/api/handlers/v0/telemetry_test.go
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
package v0_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
|
||||
"github.com/modelcontextprotocol/registry/internal/api/router"
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
"github.com/modelcontextprotocol/registry/internal/model"
|
||||
"github.com/modelcontextprotocol/registry/internal/telemetry"
|
||||
)
|
||||
|
||||
func mockServerEndpoint(registry *MockRegistryService, serverID string) {
|
||||
serverDetail := &model.ServerDetail{
|
||||
Server: model.Server{
|
||||
ID: serverID,
|
||||
Name: "test-server-detail",
|
||||
Description: "Test server detail",
|
||||
Repository: model.Repository{
|
||||
URL: "https://github.com/example/test-server-detail",
|
||||
Source: "github",
|
||||
ID: "example/test-server-detail",
|
||||
},
|
||||
VersionDetail: model.VersionDetail{
|
||||
Version: "2.0.0",
|
||||
ReleaseDate: "2025-05-27T12:00:00Z",
|
||||
IsLatest: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
registry.Mock.On("GetByID", serverID).Return(serverDetail, nil)
|
||||
}
|
||||
|
||||
func TestPrometheusHandler(t *testing.T) {
|
||||
mockRegistry := new(MockRegistryService)
|
||||
|
||||
serverID := uuid.New().String()
|
||||
mockServerEndpoint(mockRegistry, serverID)
|
||||
|
||||
cfg := config.NewConfig()
|
||||
shutdownTelemetry, metrics, _ := telemetry.InitMetrics("dev")
|
||||
|
||||
mux := http.NewServeMux()
|
||||
api := humago.New(mux, huma.DefaultConfig("Test API", "1.0.0"))
|
||||
|
||||
// Add metrics middleware with options
|
||||
api.UseMiddleware(router.MetricTelemetryMiddleware(metrics,
|
||||
router.WithSkipPaths("/health", "/metrics", "/ping", "/docs"),
|
||||
))
|
||||
v0.RegisterHealthEndpoint(api, cfg, metrics)
|
||||
v0.RegisterServersEndpoints(api, mockRegistry)
|
||||
|
||||
// Add /metrics for Prometheus metrics using promhttp
|
||||
mux.Handle("/metrics", metrics.PrometheusHandler())
|
||||
|
||||
// Create request
|
||||
url := "/v0/servers/" + serverID
|
||||
req := httptest.NewRequest(http.MethodGet, url, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Serve the request
|
||||
mux.ServeHTTP(w, req)
|
||||
|
||||
// Check the status code
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/metrics", nil)
|
||||
w = httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, req)
|
||||
|
||||
// shutdown metrics provider
|
||||
_ = shutdownTelemetry(context.Background())
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code, "Expected status OK for /metrics endpoint")
|
||||
|
||||
body := w.Body.String()
|
||||
// Check if the response body contains expected metrics
|
||||
assert.Contains(t, body, "mcp_registry_http_request_duration_bucket")
|
||||
assert.Contains(t, body, "mcp_registry_http_requests_total")
|
||||
assert.Contains(t, body, "path=\"/v0/servers/{id}\"")
|
||||
}
|
||||
|
|
@ -3,17 +3,98 @@ package router
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
"github.com/danielgtaylor/huma/v2/adapters/humago"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
"github.com/modelcontextprotocol/registry/internal/service"
|
||||
"github.com/modelcontextprotocol/registry/internal/telemetry"
|
||||
)
|
||||
|
||||
// Middleware configuration options
|
||||
type middlewareConfig struct {
|
||||
skipPaths map[string]bool
|
||||
}
|
||||
|
||||
type MiddlewareOption func(*middlewareConfig)
|
||||
|
||||
// getRoutePath extracts the route pattern from the context
|
||||
func getRoutePath(ctx huma.Context) string {
|
||||
// Try to get the operation from context
|
||||
if op := ctx.Operation().Path; op != "" {
|
||||
return ctx.Operation().Path
|
||||
}
|
||||
|
||||
// Fallback to URL path (less ideal for metrics as it includes path parameters)
|
||||
return ctx.URL().Path
|
||||
}
|
||||
|
||||
func MetricTelemetryMiddleware(metrics *telemetry.Metrics, options ...MiddlewareOption) func(huma.Context, func(huma.Context)) {
|
||||
config := &middlewareConfig{
|
||||
skipPaths: make(map[string]bool),
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
opt(config)
|
||||
}
|
||||
|
||||
return func(ctx huma.Context, next func(huma.Context)) {
|
||||
path := ctx.URL().Path
|
||||
|
||||
// Skip instrumentation for specified paths
|
||||
// extract the last part of the path to match against skipPaths
|
||||
pathParts := strings.Split(path, "/")
|
||||
pathToMatch := "/" + pathParts[len(pathParts)-1]
|
||||
if config.skipPaths[pathToMatch] || config.skipPaths[path] {
|
||||
next(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
method := ctx.Method()
|
||||
routePath := getRoutePath(ctx)
|
||||
|
||||
next(ctx)
|
||||
|
||||
duration := time.Since(start).Seconds()
|
||||
statusCode := ctx.Status()
|
||||
|
||||
// Combine common and custom attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String("method", method),
|
||||
attribute.String("path", routePath),
|
||||
attribute.Int("status_code", statusCode),
|
||||
}
|
||||
|
||||
// Record metrics
|
||||
metrics.Requests.Add(ctx.Context(), 1, metric.WithAttributes(attrs...))
|
||||
|
||||
if statusCode >= 400 {
|
||||
metrics.ErrorCount.Add(ctx.Context(), 1, metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
metrics.RequestDuration.Record(ctx.Context(), duration, metric.WithAttributes(attrs...))
|
||||
}
|
||||
}
|
||||
|
||||
// WithSkipPaths allows skipping instrumentation for specific paths
|
||||
func WithSkipPaths(paths ...string) MiddlewareOption {
|
||||
return func(c *middlewareConfig) {
|
||||
for _, path := range paths {
|
||||
c.skipPaths[path] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewHumaAPI creates a new Huma API with all routes registered
|
||||
//
|
||||
//nolint:ireturn // huma.API is the expected interface type for Huma APIs
|
||||
func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http.ServeMux) huma.API {
|
||||
func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http.ServeMux, metrics *telemetry.Metrics) huma.API {
|
||||
// Create Huma API configuration
|
||||
humaConfig := huma.DefaultConfig("MCP Registry API", "1.0.0")
|
||||
humaConfig.Info.Description = "A community driven registry service for Model Context Protocol (MCP) servers."
|
||||
|
|
@ -23,8 +104,16 @@ func NewHumaAPI(cfg *config.Config, registry service.RegistryService, mux *http.
|
|||
// Create a new API using humago adapter for standard library
|
||||
api := humago.New(mux, humaConfig)
|
||||
|
||||
// Add metrics middleware with options
|
||||
api.UseMiddleware(MetricTelemetryMiddleware(metrics,
|
||||
WithSkipPaths("/health", "/metrics", "/ping", "/docs"),
|
||||
))
|
||||
|
||||
// Register routes for all API versions
|
||||
RegisterV0Routes(api, cfg, registry)
|
||||
RegisterV0Routes(api, cfg, registry, metrics)
|
||||
|
||||
// Add /metrics for Prometheus metrics using promhttp
|
||||
mux.Handle("/metrics", metrics.PrometheusHandler())
|
||||
|
||||
// Add redirect from / to /docs
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@ package router
|
|||
|
||||
import (
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
|
||||
v0auth "github.com/modelcontextprotocol/registry/internal/api/handlers/v0/auth"
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
"github.com/modelcontextprotocol/registry/internal/service"
|
||||
"github.com/modelcontextprotocol/registry/internal/telemetry"
|
||||
)
|
||||
|
||||
func RegisterV0Routes(
|
||||
api huma.API, cfg *config.Config, registry service.RegistryService,
|
||||
api huma.API, cfg *config.Config, registry service.RegistryService, metrics *telemetry.Metrics,
|
||||
) {
|
||||
v0.RegisterHealthEndpoint(api, cfg)
|
||||
v0.RegisterHealthEndpoint(api, cfg, metrics)
|
||||
v0.RegisterPingEndpoint(api)
|
||||
v0.RegisterServersEndpoints(api, registry)
|
||||
v0auth.RegisterAuthEndpoints(api, cfg)
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/danielgtaylor/huma/v2"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/api/router"
|
||||
"github.com/modelcontextprotocol/registry/internal/config"
|
||||
"github.com/modelcontextprotocol/registry/internal/service"
|
||||
"github.com/modelcontextprotocol/registry/internal/telemetry"
|
||||
)
|
||||
|
||||
// Server represents the HTTP server
|
||||
|
|
@ -21,10 +23,11 @@ type Server struct {
|
|||
}
|
||||
|
||||
// NewServer creates a new HTTP server
|
||||
func NewServer(cfg *config.Config, registryService service.RegistryService) *Server {
|
||||
func NewServer(cfg *config.Config, registryService service.RegistryService, metrics *telemetry.Metrics) *Server {
|
||||
// Create HTTP mux and Huma API
|
||||
mux := http.NewServeMux()
|
||||
api := router.NewHumaAPI(cfg, registryService, mux)
|
||||
|
||||
api := router.NewHumaAPI(cfg, registryService, mux, metrics)
|
||||
|
||||
server := &Server{
|
||||
config: cfg,
|
||||
|
|
|
|||
147
internal/telemetry/metrics.go
Normal file
147
internal/telemetry/metrics.go
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"go.opentelemetry.io/contrib/instrumentation/runtime"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/prometheus"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
|
||||
)
|
||||
|
||||
const (
|
||||
Namespace = "mcp_registry"
|
||||
)
|
||||
|
||||
type Metrics struct {
|
||||
// Requests tracks the number of HTTP requests
|
||||
Requests metric.Int64Counter
|
||||
|
||||
// RequestDuration tracks the duration of HTTP Requests
|
||||
RequestDuration metric.Float64Histogram
|
||||
|
||||
// ErrorCount tracks the number of errors
|
||||
ErrorCount metric.Int64Counter
|
||||
|
||||
// Up tracks the health of the service
|
||||
Up metric.Int64Gauge
|
||||
}
|
||||
|
||||
// ShutdownFunc is a delegate that shuts down the OpenTelemetry components.
|
||||
type ShutdownFunc func(ctx context.Context) error
|
||||
|
||||
func NewMetrics(meter metric.Meter) (*Metrics, error) {
|
||||
req, err := meter.Int64Counter(
|
||||
Namespace+".http.requests",
|
||||
metric.WithDescription("Total number of HTTP requests"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request counter: %w", err)
|
||||
}
|
||||
|
||||
reqDuration, err := meter.Float64Histogram(
|
||||
Namespace+".http.request.duration",
|
||||
metric.WithDescription("Duration of HTTP requests in seconds"),
|
||||
metric.WithExplicitBucketBoundaries(
|
||||
0.005, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 20.0, 50.0,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request duration histogram: %w", err)
|
||||
}
|
||||
|
||||
errCount, err := meter.Int64Counter(
|
||||
Namespace+".http.errors",
|
||||
metric.WithDescription("Total number of HTTP errors"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create error counter: %w", err)
|
||||
}
|
||||
|
||||
up, err := meter.Int64Gauge(
|
||||
Namespace+".service.up",
|
||||
metric.WithDescription("Service health status (1 for up, 0 for down)"),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create service up gauge: %w", err)
|
||||
}
|
||||
|
||||
return &Metrics{
|
||||
Requests: req,
|
||||
RequestDuration: reqDuration,
|
||||
ErrorCount: errCount,
|
||||
Up: up,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewPrometheusMeterProvider(res *resource.Resource, exp *prometheus.Exporter) (*sdkmetric.MeterProvider, error) {
|
||||
if exp == nil {
|
||||
return nil, errors.New("exporter cannot be nil")
|
||||
}
|
||||
meterProvider := sdkmetric.NewMeterProvider(
|
||||
sdkmetric.WithResource(res),
|
||||
sdkmetric.WithReader(exp),
|
||||
)
|
||||
|
||||
return meterProvider, nil
|
||||
}
|
||||
|
||||
func InitMetrics(version string) (ShutdownFunc, *Metrics, error) {
|
||||
// Initialized the returned shutdownFunc to no-op.
|
||||
shutdown := func(_ context.Context) error { return nil }
|
||||
|
||||
res, err := resource.New(context.Background(),
|
||||
resource.WithAttributes(
|
||||
semconv.ServiceName(Namespace),
|
||||
semconv.ServiceVersion(version),
|
||||
),
|
||||
resource.WithProcessRuntimeDescription(),
|
||||
)
|
||||
if err != nil {
|
||||
return shutdown, nil, fmt.Errorf("failed to create resource: %w", err)
|
||||
}
|
||||
|
||||
res, err = resource.Merge(resource.Default(), res)
|
||||
if err != nil {
|
||||
return shutdown, nil, fmt.Errorf("failed to merge resources: %w", err)
|
||||
}
|
||||
|
||||
exporter, err := prometheus.New()
|
||||
if err != nil {
|
||||
return shutdown, nil, fmt.Errorf("failed to create Prometheus exporter: %w", err)
|
||||
}
|
||||
|
||||
mp, err := NewPrometheusMeterProvider(res, exporter)
|
||||
if err != nil {
|
||||
return shutdown, nil, fmt.Errorf("failed to create Prometheus meter provider: %w", err)
|
||||
}
|
||||
otel.SetMeterProvider(mp)
|
||||
|
||||
// Update the returned shutdownFunc that calls metric provider
|
||||
// shutdown methods and make sure that a non-nil error is returned
|
||||
// if any returned an error.
|
||||
shutdown = func(ctx context.Context) error {
|
||||
var retErr error
|
||||
if err := mp.Shutdown(ctx); err != nil {
|
||||
retErr = err
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
|
||||
meter := mp.Meter(Namespace, metric.WithSchemaURL(semconv.SchemaURL), metric.WithInstrumentationVersion(runtime.Version()))
|
||||
metrics, err := NewMetrics(meter)
|
||||
return shutdown, metrics, err
|
||||
}
|
||||
|
||||
// PrometheusHandler returns the HTTP handler for Prometheus metrics
|
||||
// This handler serves the metrics endpoint for Prometheus to scrape.
|
||||
func (m *Metrics) PrometheusHandler() http.Handler {
|
||||
return promhttp.Handler()
|
||||
}
|
||||
84
internal/telemetry/metrics_test.go
Normal file
84
internal/telemetry/metrics_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package telemetry_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.opentelemetry.io/otel/exporters/prometheus"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/metric/noop"
|
||||
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
|
||||
"go.opentelemetry.io/otel/sdk/resource"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/internal/telemetry"
|
||||
)
|
||||
|
||||
func TestNewMetrics(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
meter metric.Meter
|
||||
expectedMetric string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Meter",
|
||||
meter: noop.NewMeterProvider().Meter("test"),
|
||||
expectedMetric: "http_requests_total",
|
||||
expectedError: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
metrics, err := telemetry.NewMetrics(tt.meter)
|
||||
|
||||
// Ensure the metric is registered correctly
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, metrics)
|
||||
assert.NotNil(t, metrics.Requests)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPrometheusMeterProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
wantErr bool
|
||||
mockPrometheus func() (*prometheus.Exporter, error)
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Successful creation of meter provider",
|
||||
wantErr: false,
|
||||
mockPrometheus: func() (*prometheus.Exporter, error) {
|
||||
return &prometheus.Exporter{
|
||||
Reader: sdkmetric.NewManualReader(),
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Error on resource creation",
|
||||
wantErr: true,
|
||||
expectedError: errors.New("error creating prometheus resource"),
|
||||
mockPrometheus: func() (*prometheus.Exporter, error) {
|
||||
return nil, errors.New("error creating prometheus resource")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
res := resource.NewSchemaless() // Use an empty resource for testing.
|
||||
exp, _ := tt.mockPrometheus()
|
||||
mp, err := telemetry.NewPrometheusMeterProvider(res, exp)
|
||||
|
||||
if tt.wantErr {
|
||||
assert.NotNil(t, err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, mp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue