diff --git a/pkg/gofr/gofr.go b/pkg/gofr/gofr.go index 20d7867db..5cfeb1923 100644 --- a/pkg/gofr/gofr.go +++ b/pkg/gofr/gofr.go @@ -42,6 +42,11 @@ const ( gofrTraceExporter = "gofr" gofrTracerURL = "https://tracer.gofr.dev" checkPortTimeout = 2 * time.Second + gofrHost = "https://gofr.dev" + startServerPing = "/api/ping/up" + shutServerPing = "/api/ping/down" + pingTimeout = 5 * time.Second + defaultTelemetry = "true" ) // App is the main application in the GoFr framework. @@ -173,9 +178,17 @@ func (a *App) Run() { shutdownCtx, done := context.WithTimeout(context.WithoutCancel(ctx), shutDownTimeout) defer done() + if a.hasTelemetry() { + a.sendTelemetry(http.DefaultClient, false) + } + _ = a.Shutdown(shutdownCtx) }() + if a.hasTelemetry() { + go a.sendTelemetry(http.DefaultClient, true) + } + wg := sync.WaitGroup{} // Start Metrics Server @@ -224,6 +237,37 @@ func (a *App) Run() { wg.Wait() } +func (a *App) hasTelemetry() bool { + return a.Config.GetOrDefault("GOFR_TELEMETRY", defaultTelemetry) == "true" +} + +func (a *App) sendTelemetry(client *http.Client, isStart bool) { + url := fmt.Sprint(gofrHost, shutServerPing) + + if isStart { + url = fmt.Sprint(gofrHost, startServerPing) + + a.container.Info("GoFr records the number of active servers. Set GOFR_TELEMETRY=false in configs to disable it.") + } + + ctx, cancel := context.WithTimeout(context.Background(), pingTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, http.NoBody) + if err != nil { + return + } + + req.Header.Set("Connection", "close") + + resp, err := client.Do(req) + if err != nil { + return + } + + resp.Body.Close() +} + // Shutdown stops the service(s) and close the application. // It shuts down the HTTP, gRPC, Metrics servers and closes the container's active connections to datasources. func (a *App) Shutdown(ctx context.Context) error { diff --git a/pkg/gofr/gofr_test.go b/pkg/gofr/gofr_test.go index 541e1e020..d45cbe93d 100644 --- a/pkg/gofr/gofr_test.go +++ b/pkg/gofr/gofr_test.go @@ -71,6 +71,54 @@ func TestGoFr_isPortAvailable(t *testing.T) { } } +// mockRoundTripper is a mock implementation of http.RoundTripper. +type mockRoundTripper struct { + lastRequest *http.Request // Store the last request for assertions + mockResponse *http.Response + mockError error +} + +// RoundTrip mocks the HTTP request and stores the request for verification. +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + m.lastRequest = req // Store the request for assertions + return m.mockResponse, m.mockError +} + +func TestPingGoFr(t *testing.T) { + tests := []struct { + name string + input bool + expectedURL string + }{ + {"Ping Start Server", true, gofrHost + startServerPing}, + {"Ping Shut Server", false, gofrHost + shutServerPing}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockTransport := &mockRoundTripper{ + mockResponse: &http.Response{ + StatusCode: http.StatusOK, + Body: http.NoBody, + }, + mockError: nil, + } + + mockClient := &http.Client{Transport: mockTransport} + + _ = testutil.NewServerConfigs(t) + + a := New() + + a.sendTelemetry(mockClient, tt.input) + + assert.NotNil(t, mockTransport.lastRequest, "Request should not be nil") + assert.Equal(t, tt.expectedURL, mockTransport.lastRequest.URL.String(), "Unexpected request URL") + assert.Equal(t, http.MethodPost, mockTransport.lastRequest.Method, "Unexpected HTTP method") + }) + } +} + func TestGofr_ServerRoutes(t *testing.T) { _ = testutil.NewServerConfigs(t)