diff --git a/pkg/gofr/container/container.go b/pkg/gofr/container/container.go index 36bfdd736e..53354302e5 100644 --- a/pkg/gofr/container/container.go +++ b/pkg/gofr/container/container.go @@ -116,6 +116,8 @@ func (c *Container) Create(conf config.Config) { c.metricsManager = metrics.NewMetricsManager(exporters.Prometheus(c.GetAppName(), c.GetAppVersion()), c.Logger) + exporters.SendFrameworkStartupTelemetry(c.GetAppName(), c.GetAppVersion()) + // Register framework metrics c.registerFrameworkMetrics() diff --git a/pkg/gofr/metrics/exporters/telemetry.go b/pkg/gofr/metrics/exporters/telemetry.go new file mode 100644 index 0000000000..c20c6a9113 --- /dev/null +++ b/pkg/gofr/metrics/exporters/telemetry.go @@ -0,0 +1,99 @@ +package exporters + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "os" + "runtime" + "time" + + "github.com/google/uuid" + + "gofr.dev/pkg/gofr/version" +) + +const ( + defaultTelemetryEndpoint = "https://gofr.dev/telemetry/v1/metrics" + defaultAppName = "gofr-app" + requestTimeout = 10 * time.Second +) + +// TelemetryData represents the JSON telemetry payload. +type TelemetryData struct { + Timestamp string `json:"timestamp"` + EventID string `json:"event_id"` + Source string `json:"source"` + ServiceName string `json:"service_name,omitempty"` + ServiceVersion string `json:"service_version,omitempty"` + RawDataSize int `json:"raw_data_size"` + FrameworkVersion string `json:"framework_version,omitempty"` + GoVersion string `json:"go_version,omitempty"` + OS string `json:"os,omitempty"` + Architecture string `json:"architecture,omitempty"` + StartupTime string `json:"startup_time,omitempty"` +} + +// SendFrameworkStartupTelemetry sends telemetry data. +func SendFrameworkStartupTelemetry(appName, appVersion string) { + if os.Getenv("GOFR_TELEMETRY") == "false" { + return + } + + go sendTelemetryData(appName, appVersion) +} + +func sendTelemetryData(appName, appVersion string) { + if appName == "" { + appName = defaultAppName + } + + if appVersion == "" { + appVersion = "unknown" + } + + now := time.Now().UTC() + + data := TelemetryData{ + Timestamp: now.Format(time.RFC3339), + EventID: uuid.New().String(), + Source: "gofr-framework", + ServiceName: appName, + ServiceVersion: appVersion, + RawDataSize: 0, + FrameworkVersion: version.Framework, + GoVersion: runtime.Version(), + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + StartupTime: now.Format(time.RFC3339), + } + + sendToEndpoint(&data, defaultTelemetryEndpoint) +} + +func sendToEndpoint(data *TelemetryData, endpoint string) { + jsonData, err := json.Marshal(data) + if err != nil { + return + } + + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonData)) + if err != nil { + return + } + + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 10 * time.Second} + + resp, err := client.Do(req) + if err != nil { + return + } + + resp.Body.Close() +} diff --git a/pkg/gofr/metrics/exporters/telemetry_test.go b/pkg/gofr/metrics/exporters/telemetry_test.go new file mode 100644 index 0000000000..0a918c29d2 --- /dev/null +++ b/pkg/gofr/metrics/exporters/telemetry_test.go @@ -0,0 +1,87 @@ +package exporters + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "runtime" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + + "gofr.dev/pkg/gofr/version" +) + +func TestSendFrameworkStartupTelemetry_Disabled(t *testing.T) { + t.Setenv("GOFR_TELEMETRY", "true") + + requestMade := false + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + requestMade = true + + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + SendFrameworkStartupTelemetry("test-app", "1.0.0") + time.Sleep(100 * time.Millisecond) + + assert.False(t, requestMade, "Expected no telemetry when disabled") +} + +func TestSendFrameworkStartupTelemetry_DefaultValues(t *testing.T) { + var receivedData TelemetryData + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + + err = json.Unmarshal(body, &receivedData) + assert.NoError(t, err) + + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + // Test with empty values to verify defaults. + testSendTelemetryData("", "", server.URL) + time.Sleep(100 * time.Millisecond) + + assert.Equal(t, defaultAppName, receivedData.ServiceName) + assert.Equal(t, "unknown", receivedData.ServiceVersion) + assert.Equal(t, "gofr-framework", receivedData.Source) + assert.Equal(t, 0, receivedData.RawDataSize) +} + +// Helper function that replicates sendTelemetryData but with configurable endpoint. +func testSendTelemetryData(appName, appVersion, endpoint string) { + if appName == "" { + appName = defaultAppName + } + + if appVersion == "" { + appVersion = "unknown" + } + + now := time.Now().UTC() + + data := TelemetryData{ + Timestamp: now.Format(time.RFC3339), + EventID: uuid.New().String(), + Source: "gofr-framework", + ServiceName: appName, + ServiceVersion: appVersion, + RawDataSize: 0, + FrameworkVersion: version.Framework, + GoVersion: runtime.Version(), + OS: runtime.GOOS, + Architecture: runtime.GOARCH, + StartupTime: now.Format(time.RFC3339), + } + + sendToEndpoint(&data, endpoint) +}