Skip to content

Enhance health check #241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/registry.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,19 @@ GET /v0/health
Returns the health status of the service:
```json
{
"status": "ok"
"status": "ok",
"github_client_id": "client_id_here",
"database": {
"status": "connected",
"type": "mongodb",
"collection_count": 1
},
"uptime": "2h30m15s",
"version": "v0.1.0",
"memory": {
"alloc": "2.1 MB",
"sys": "8.5 MB"
}
}
```

Expand Down
4 changes: 3 additions & 1 deletion cmd/registry/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/modelcontextprotocol/registry/internal/api"
"github.com/modelcontextprotocol/registry/internal/appinfo"
"github.com/modelcontextprotocol/registry/internal/auth"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/database"
Expand Down Expand Up @@ -41,6 +42,7 @@ func main() {
)

// Initialize configuration
appinfo.Initialize()
cfg := config.NewConfig()

// Initialize services based on environment
Expand Down Expand Up @@ -96,7 +98,7 @@ func main() {
authService := auth.NewAuthService(cfg)

// Initialize HTTP server
server := api.NewServer(cfg, registryService, authService)
server := api.NewServer(cfg, registryService, authService, db)

// Start server in a goroutine so it doesn't block signal handling
go func() {
Expand Down
52 changes: 49 additions & 3 deletions internal/api/handlers/v0/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,69 @@ package v0

import (
"encoding/json"
"fmt"
"net/http"
"runtime"

"github.com/modelcontextprotocol/registry/internal/appinfo"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/database"
)

const (
DBStatusConnected = "connected"
DBStatusDisconnected = "disconnected"
)

type DatabaseType string
type HealthResponse struct {
Status string `json:"status"`
GitHubClientID string `json:"github_client_id"`
Status string `json:"status"`
GitHubClientID string `json:"github_client_id"`
Database *DatabaseHealth `json:"database"`
Uptime string `json:"uptime"`
Version string `json:"version"`
Memory *MemoryStats `json:"memory"`
}

type DatabaseHealth struct {
Status string `json:"status"`
Type DatabaseType `json:"type"`
CollectionCount int `json:"collection_count"`
}

type MemoryStats struct {
Alloc string `json:"alloc"`
Sys string `json:"sys"`
}

// HealthHandler returns a handler for health check endpoint
func HealthHandler(cfg *config.Config) http.HandlerFunc {
func HealthHandler(cfg *config.Config, db database.Database) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
connInfo := db.Connection()
dbStatus := DBStatusDisconnected
if connInfo.IsConnected {
dbStatus = DBStatusConnected
}
databaseHealth := DatabaseHealth{
Status: dbStatus,
Type: DatabaseType(connInfo.Type),
CollectionCount: connInfo.CollectionCount,
}

var m runtime.MemStats
runtime.ReadMemStats(&m)

if err := json.NewEncoder(w).Encode(HealthResponse{
Status: "ok",
GitHubClientID: cfg.GithubClientID,
Database: &databaseHealth,
Uptime: appinfo.GetUptimeString(),
Version: cfg.Version,
Memory: &MemoryStats{
Alloc: fmt.Sprintf("%.1f MB", float64(m.Alloc)/1024/1024),
Sys: fmt.Sprintf("%.1f MB", float64(m.Sys)/1024/1024),
},
}); err != nil {
http.Error(w, "Failed to encode response", http.StatusInternalServerError)
}
Expand Down
83 changes: 75 additions & 8 deletions internal/api/handlers/v0/health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,98 @@

v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/model"
"github.com/stretchr/testify/assert"
)

// Mock database for testing
// Supports passing dbType
type mockDatabase struct {
dbType string
}

// Implement List method to satisfy database.Database interface
func (m *mockDatabase) List(ctx context.Context, filter map[string]any, cursor string, limit int) ([]*model.Server, string, error) {

Check failure on line 24 in internal/api/handlers/v0/health_test.go

View workflow job for this annotation

GitHub Actions / Build, Lint, and Validate

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
return []*model.Server{}, "", nil
}

func (m *mockDatabase) Connection() *database.ConnectionInfo {
return &database.ConnectionInfo{
IsConnected: true,
Type: database.ConnectionType(m.dbType),
CollectionCount: 0,
}
}

func (m *mockDatabase) GetByID(ctx context.Context, id string) (*model.ServerDetail, error) {

Check failure on line 36 in internal/api/handlers/v0/health_test.go

View workflow job for this annotation

GitHub Actions / Build, Lint, and Validate

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
return nil, database.ErrNotFound
}

// Implement Publish method to satisfy database.Database interface
func (m *mockDatabase) Publish(ctx context.Context, serverDetail *model.ServerDetail) error {

Check failure on line 41 in internal/api/handlers/v0/health_test.go

View workflow job for this annotation

GitHub Actions / Build, Lint, and Validate

unused-parameter: parameter 'ctx' seems to be unused, consider removing or renaming it as _ (revive)
return nil
}

func (m *mockDatabase) ImportSeed(ctx context.Context, seedFilePath string) error {
return nil
}

func (m *mockDatabase) Close() error {
return nil
}

func TestHealthHandler(t *testing.T) {
// Test cases
testCases := []struct {
name string
config *config.Config
dbType string
expectedStatus int
expectedBody v0.HealthResponse
}{
{
name: "returns health status with github client id",
name: "returns health status with github client id (memory)",
config: &config.Config{
GithubClientID: "test-github-client-id",
},
dbType: "memory",
expectedStatus: http.StatusOK,
expectedBody: v0.HealthResponse{
Status: "ok",
GitHubClientID: "test-github-client-id",
},
},
{
name: "works with empty github client id",
name: "returns health status with github client id (mongo)",
config: &config.Config{
GithubClientID: "test-github-client-id",
},
dbType: "mongo",
expectedStatus: http.StatusOK,
expectedBody: v0.HealthResponse{
Status: "ok",
GitHubClientID: "test-github-client-id",
},
},
{
name: "works with empty github client id (memory)",
config: &config.Config{
GithubClientID: "",
},
dbType: "memory",
expectedStatus: http.StatusOK,
expectedBody: v0.HealthResponse{
Status: "ok",
GitHubClientID: "",
},
},
{
name: "works with empty github client id (mongo)",
config: &config.Config{
GithubClientID: "",
},
dbType: "mongo",
expectedStatus: http.StatusOK,
expectedBody: v0.HealthResponse{
Status: "ok",
Expand All @@ -46,8 +111,8 @@

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create handler with the test config
handler := v0.HealthHandler(tc.config)
// Create handler with the test config and mock database
handler := v0.HealthHandler(tc.config, &mockDatabase{dbType: tc.dbType})

// Create request
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "/health", nil)
Expand All @@ -73,19 +138,20 @@
assert.NoError(t, err)

// Check the response body
assert.Equal(t, tc.expectedBody, resp)
assert.Equal(t, tc.expectedBody.Status, resp.Status)
assert.Equal(t, tc.expectedBody.GitHubClientID, resp.GitHubClientID)
})
}
}

// TestHealthHandlerIntegration tests the handler with actual HTTP requests
// Integration test using memory database type
func TestHealthHandlerIntegration(t *testing.T) {
// Create test server
cfg := &config.Config{
GithubClientID: "integration-test-client-id",
}

server := httptest.NewServer(v0.HealthHandler(cfg))
server := httptest.NewServer(v0.HealthHandler(cfg, &mockDatabase{dbType: "memory"}))
defer server.Close()

// Send request to the test server
Expand Down Expand Up @@ -118,5 +184,6 @@
Status: "ok",
GitHubClientID: "integration-test-client-id",
}
assert.Equal(t, expectedResp, healthResp)
assert.Equal(t, expectedResp.Status, healthResp.Status)
assert.Equal(t, expectedResp.GitHubClientID, healthResp.GitHubClientID)
}
5 changes: 3 additions & 2 deletions internal/api/router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import (

"github.com/modelcontextprotocol/registry/internal/auth"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/service"
)

// New creates a new router with all API versions registered
func New(cfg *config.Config, registry service.RegistryService, authService auth.Service) *http.ServeMux {
func New(cfg *config.Config, registry service.RegistryService, authService auth.Service, db database.Database) *http.ServeMux {
mux := http.NewServeMux()

// Register routes for all API versions
RegisterV0Routes(mux, cfg, registry, authService)
RegisterV0Routes(mux, cfg, registry, authService, db)

return mux
}
4 changes: 3 additions & 1 deletion internal/api/router/v0.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import (
v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
"github.com/modelcontextprotocol/registry/internal/auth"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/service"
)

// RegisterV0Routes registers all v0 API routes to the provided router
func RegisterV0Routes(
mux *http.ServeMux, cfg *config.Config, registry service.RegistryService, authService auth.Service,
db database.Database,
) {
// Register v0 endpoints
mux.HandleFunc("/v0/health", v0.HealthHandler(cfg))
mux.HandleFunc("/v0/health", v0.HealthHandler(cfg, db))
mux.HandleFunc("/v0/servers", v0.ServersHandler(registry))
mux.HandleFunc("/v0/servers/{id}", v0.ServersDetailHandler(registry))
mux.HandleFunc("/v0/ping", v0.PingHandler(cfg))
Expand Down
5 changes: 3 additions & 2 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/modelcontextprotocol/registry/internal/api/router"
"github.com/modelcontextprotocol/registry/internal/auth"
"github.com/modelcontextprotocol/registry/internal/config"
"github.com/modelcontextprotocol/registry/internal/database"
"github.com/modelcontextprotocol/registry/internal/service"
)

Expand All @@ -22,9 +23,9 @@ type Server struct {
}

// NewServer creates a new HTTP server
func NewServer(cfg *config.Config, registryService service.RegistryService, authService auth.Service) *Server {
func NewServer(cfg *config.Config, registryService service.RegistryService, authService auth.Service, db database.Database) *Server {
// Create router with all API versions registered
mux := router.New(cfg, registryService, authService)
mux := router.New(cfg, registryService, authService, db)

server := &Server{
config: cfg,
Expand Down
22 changes: 22 additions & 0 deletions internal/appinfo/appinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package appinfo

import "time"

// Application start time
var startTime = time.Now()
var Version = "v0.1.0"

// Initialize sets the application start time
func Initialize() {
startTime = time.Now()
}

// GetUptime returns the time the application has been running
func GetUptime() time.Duration {
return time.Since(startTime).Truncate(time.Second)
}

// GetUptimeString returns a formatted uptime string, e.g. "2h30m15s"
func GetUptimeString() string {
return GetUptime().String()
}
4 changes: 4 additions & 0 deletions internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type Database interface {
Publish(ctx context.Context, serverDetail *model.ServerDetail) error
// ImportSeed imports initial data from a seed file
ImportSeed(ctx context.Context, seedFilePath string) error
// Connection returns information about the database connection
Connection() *ConnectionInfo
// Close closes the database connection
Close() error
}
Expand All @@ -46,6 +48,8 @@ type ConnectionInfo struct {
Type ConnectionType
// IsConnected indicates whether the database is currently connected
IsConnected bool
// CollectionCount indicates the number of collections in the database
CollectionCount int
// Raw provides access to the underlying connection object, which will vary by implementation
// For MongoDB, this will be *mongo.Client
// For MemoryDB, this will be map[string]*model.MCPRegistry
Expand Down
Loading
Loading