From 0be388909bc51630d231100cea805e2330181028 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 22 Oct 2024 08:41:27 +0200 Subject: [PATCH 01/19] fix: http client requests --- main.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 5c0280c..42cbf8d 100644 --- a/main.go +++ b/main.go @@ -15,8 +15,8 @@ import ( "syscall" "time" - "github.com/joho/godotenv" "github.com/gin-gonic/gin" + "github.com/joho/godotenv" _ "github.com/mattn/go-sqlite3" "golang.org/x/sync/errgroup" ) @@ -36,7 +36,17 @@ var metricsFile string = "/app/db/metrics.sqlite" var collectorEnabled bool = false var collectorRetentionPeriodDays int = 7 -// HTTP client with connection pooling +// HTTP client (Docker) with connection pooling +var dockerHttpClient = &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + }, + Timeout: 10 * time.Second, +} + +// HTTP client (other) with connection pooling var httpClient = &http.Client{ Transport: &http.Transport{ MaxIdleConns: 100, @@ -266,6 +276,11 @@ func main() { log.Fatal(err) } + // Use Unix socket transport + dockerHttpClient.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", "/var/run/docker.sock") + } + r := gin.Default() r.GET("/api/health", func(c *gin.Context) { c.String(200, "ok") @@ -371,10 +386,5 @@ func makeDockerRequest(url string) (*http.Response, error) { } req.Header.Set("Content-Type", "application/json") - // Use Unix socket transport - httpClient.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - return net.Dial("unix", "/var/run/docker.sock") - } - - return httpClient.Do(req) + return dockerHttpClient.Do(req) } From 0e497fceef38acb7c6d28a9ac61fad743308226b Mon Sep 17 00:00:00 2001 From: Laurence Date: Tue, 22 Oct 2024 10:31:05 +0100 Subject: [PATCH 02/19] enhance: add context directly where we start the docker http client --- main.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 42cbf8d..fbcdfc5 100644 --- a/main.go +++ b/main.go @@ -42,6 +42,9 @@ var dockerHttpClient = &http.Client{ MaxIdleConns: 100, MaxIdleConnsPerHost: 100, IdleConnTimeout: 90 * time.Second, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", "/var/run/docker.sock") + }, }, Timeout: 10 * time.Second, } @@ -276,11 +279,6 @@ func main() { log.Fatal(err) } - // Use Unix socket transport - dockerHttpClient.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - return net.Dial("unix", "/var/run/docker.sock") - } - r := gin.Default() r.GET("/api/health", func(c *gin.Context) { c.String(200, "ok") From 4bed0fac9e08a7d3d4fa00b4d2af44f6123494fe Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 22 Oct 2024 13:41:08 +0200 Subject: [PATCH 03/19] Refactor push.go to include filesystem usage data --- push.go | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/push.go b/push.go index 0152d89..d9056b6 100644 --- a/push.go +++ b/push.go @@ -7,6 +7,7 @@ import ( "io" "log" "net/http" + "syscall" "time" "github.com/docker/docker/api/types" @@ -51,9 +52,16 @@ func getPushData() (map[string]interface{}, error) { log.Printf("Error getting containers data: %v", err) return nil, err } + filesystemUsageRoot, err := filesystemUsageRoot() + if err != nil { + log.Printf("Error getting disk usage: %v", err) + return nil, err + } data := map[string]interface{}{ - "containers": containersData, + "containers": containersData, + "filesystem_usage_root": filesystemUsageRoot, } + fmt.Printf("Pushing data: %v\n", data) jsonData, err := JSON.Marshal(data) if err != nil { log.Printf("Error marshalling data: %v", err) @@ -80,6 +88,22 @@ func getPushData() (map[string]interface{}, error) { return data, nil } +func filesystemUsageRoot() (map[string]interface{}, error) { + fs := syscall.Statfs_t{} + err := syscall.Statfs("/", &fs) + if err != nil { + return nil, err + } + totalSpace := fs.Blocks * uint64(fs.Bsize) + freeSpace := fs.Bfree * uint64(fs.Bsize) + usedSpace := totalSpace - freeSpace + usedPercentage := float64(usedSpace) / float64(totalSpace) * 100 + + return map[string]interface{}{ + "used_percentage": fmt.Sprintf("%d", int(usedPercentage)), + }, nil +} + func containerData() ([]Container, error) { resp, err := makeDockerRequest("/containers/json?all=true") if err != nil { From a60b2e860a10ec6d766d97aa3de5f2f26a7cdd03 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 22 Oct 2024 15:03:47 +0200 Subject: [PATCH 04/19] Refactor push.go to remove debug print statement --- push.go | 1 - 1 file changed, 1 deletion(-) diff --git a/push.go b/push.go index d9056b6..3910c8a 100644 --- a/push.go +++ b/push.go @@ -61,7 +61,6 @@ func getPushData() (map[string]interface{}, error) { "containers": containersData, "filesystem_usage_root": filesystemUsageRoot, } - fmt.Printf("Pushing data: %v\n", data) jsonData, err := JSON.Marshal(data) if err != nil { log.Printf("Error marshalling data: %v", err) From bdf0ff8e3c0d8a63054708548d5dd2e7ba33494a Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 22 Oct 2024 15:12:02 +0200 Subject: [PATCH 05/19] Refactor release-next.yaml to include multiple registries and labels --- .github/workflows/release-next.yaml | 86 +++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release-next.yaml b/.github/workflows/release-next.yaml index ec55566..471eda9 100644 --- a/.github/workflows/release-next.yaml +++ b/.github/workflows/release-next.yaml @@ -1,11 +1,12 @@ -name: Sentinel Release +name: Sentinel Release Development on: push: branches: ['next'] env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/sentinel" jobs: @@ -16,20 +17,33 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: context: . file: Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next-amd64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next-amd64 + labels: | + coolify.managed=true aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -37,20 +51,33 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push - uses: docker/build-push-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: context: . - file: Dockerfile.arm64 + file: Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 + labels: | + coolify.managed=true merge-manifest: runs-on: ubuntu-latest permissions: @@ -58,18 +85,31 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:next + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next-amd64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} \ No newline at end of file From 3f349fb79c0697c254d3002989190ff249daccdc Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 22 Oct 2024 15:14:03 +0200 Subject: [PATCH 06/19] Refactor release-next.yaml to use Dockerfile.arm64 for linux/aarch64 platform --- .github/workflows/release-next.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-next.yaml b/.github/workflows/release-next.yaml index 471eda9..30f89a0 100644 --- a/.github/workflows/release-next.yaml +++ b/.github/workflows/release-next.yaml @@ -70,7 +70,7 @@ jobs: uses: docker/build-push-action@v6 with: context: . - file: Dockerfile + file: Dockerfile.arm64 platforms: linux/aarch64 push: true tags: | From 2e4530c73b90f06bec2e9b44d23ae9112521e251 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 22 Oct 2024 15:14:39 +0200 Subject: [PATCH 07/19] Refactor Dockerfile to use cache for go modules and package installation --- Dockerfile | 12 ++++++++---- Dockerfile.arm64 | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index a9be399..34dc4c1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,22 +2,26 @@ FROM golang:1.23-bullseye AS deps WORKDIR /app COPY go.mod go.sum ./ -RUN go mod download +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download FROM golang:1.23-bullseye AS build WORKDIR /app COPY --from=deps /go/pkg/mod /go/pkg/mod COPY . . -RUN apt-get update && apt-get install -y gcc g++ +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && apt-get install -y gcc g++ ENV CGO_ENABLED=1 \ GOOS=linux \ GOARCH=amd64 -RUN go build -o /app/bin/sentinel ./ +RUN --mount=type=cache,target=/root/.cache/go-build \ + go build -o /app/bin/sentinel ./ FROM debian:bullseye-slim -RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* ENV GIN_MODE=release COPY --from=build /app/ /app COPY --from=build /app/bin/sentinel /app/sentinel diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index ac5f4f8..bef206b 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -2,22 +2,26 @@ FROM golang:1.23-bullseye AS deps WORKDIR /app COPY go.mod go.sum ./ -RUN go mod download +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download FROM golang:1.23-bullseye AS build WORKDIR /app COPY --from=deps /go/pkg/mod /go/pkg/mod COPY . . -RUN apt-get update && apt-get install -y gcc g++ +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && apt-get install -y gcc g++ ENV CGO_ENABLED=1 \ GOOS=linux \ GOARCH=arm64 -RUN go build -o /app/bin/sentinel ./ +RUN --mount=type=cache,target=/root/.cache/go-build \ + go build -o /app/bin/sentinel ./ FROM debian:bullseye-slim -RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* ENV GIN_MODE=release COPY --from=build /app/ /app COPY --from=build /app/bin/sentinel /app/sentinel From 2615e202186f03ffc17f78facf4b178ed1702e55 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 22 Oct 2024 15:16:15 +0200 Subject: [PATCH 08/19] Refactor Dockerfile to use cache for go modules and package installation --- Dockerfile | 12 ++++-------- Dockerfile.arm64 | 12 ++++-------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index 34dc4c1..a9be399 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,26 +2,22 @@ FROM golang:1.23-bullseye AS deps WORKDIR /app COPY go.mod go.sum ./ -RUN --mount=type=cache,target=/go/pkg/mod \ - go mod download +RUN go mod download FROM golang:1.23-bullseye AS build WORKDIR /app COPY --from=deps /go/pkg/mod /go/pkg/mod COPY . . -RUN --mount=type=cache,target=/var/cache/apt \ - apt-get update && apt-get install -y gcc g++ +RUN apt-get update && apt-get install -y gcc g++ ENV CGO_ENABLED=1 \ GOOS=linux \ GOARCH=amd64 -RUN --mount=type=cache,target=/root/.cache/go-build \ - go build -o /app/bin/sentinel ./ +RUN go build -o /app/bin/sentinel ./ FROM debian:bullseye-slim -RUN --mount=type=cache,target=/var/cache/apt \ - apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* ENV GIN_MODE=release COPY --from=build /app/ /app COPY --from=build /app/bin/sentinel /app/sentinel diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index bef206b..ac5f4f8 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -2,26 +2,22 @@ FROM golang:1.23-bullseye AS deps WORKDIR /app COPY go.mod go.sum ./ -RUN --mount=type=cache,target=/go/pkg/mod \ - go mod download +RUN go mod download FROM golang:1.23-bullseye AS build WORKDIR /app COPY --from=deps /go/pkg/mod /go/pkg/mod COPY . . -RUN --mount=type=cache,target=/var/cache/apt \ - apt-get update && apt-get install -y gcc g++ +RUN apt-get update && apt-get install -y gcc g++ ENV CGO_ENABLED=1 \ GOOS=linux \ GOARCH=arm64 -RUN --mount=type=cache,target=/root/.cache/go-build \ - go build -o /app/bin/sentinel ./ +RUN go build -o /app/bin/sentinel ./ FROM debian:bullseye-slim -RUN --mount=type=cache,target=/var/cache/apt \ - apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* +RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* ENV GIN_MODE=release COPY --from=build /app/ /app COPY --from=build /app/bin/sentinel /app/sentinel From 0c7785ec5fef63859055a57381ec157abd3fc5af Mon Sep 17 00:00:00 2001 From: Laurence Date: Tue, 22 Oct 2024 17:19:17 +0100 Subject: [PATCH 09/19] enhance: Code qol and improve debug --- container.go | 4 +- cpu.go | 2 +- main.go | 110 ++++++++++++++++++++++----------------------------- memory.go | 2 +- 4 files changed, 51 insertions(+), 67 deletions(-) diff --git a/container.go b/container.go index db54550..c9c41a8 100644 --- a/container.go +++ b/container.go @@ -84,7 +84,7 @@ func setupContainerRoutes(r *gin.Engine) { return } timeInt, _ := strconv.ParseInt(usage.Time, 10, 64) - if gin.Mode() == gin.DebugMode { + if debug { usage.HumanFriendlyTime = time.UnixMilli(timeInt).Format(layout) } usages = append(usages, usage) @@ -153,7 +153,7 @@ func setupContainerRoutes(r *gin.Engine) { return } timeInt, _ := strconv.ParseInt(usage.Time, 10, 64) - if gin.Mode() == gin.DebugMode { + if debug { usage.HumanFriendlyTime = time.UnixMilli(timeInt).Format(layout) } usages = append(usages, usage) diff --git a/cpu.go b/cpu.go index 1ea7e6f..2d1d9b2 100644 --- a/cpu.go +++ b/cpu.go @@ -82,7 +82,7 @@ func setupCpuRoutes(r *gin.Engine) { return } timeInt, _ := strconv.ParseInt(usage.Time, 10, 64) - if gin.Mode() == gin.DebugMode { + if debug { usage.HumanFriendlyTime = time.UnixMilli(timeInt).Format(layout) } usages = append(usages, usage) diff --git a/main.go b/main.go index fbcdfc5..495ea64 100644 --- a/main.go +++ b/main.go @@ -106,9 +106,6 @@ func main() { log.Printf("Error loading .env file: %v", err) } } - if gin.Mode() == gin.DebugMode { - metricsFile = "./db/metrics.sqlite" - } debugFromEnv := os.Getenv("DEBUG") if debugFromEnv != "" { var err error @@ -117,8 +114,16 @@ func main() { log.Printf("Error parsing DEBUG: %v", err) } } + + debug = debug || gin.Mode() == gin.DebugMode + endpointFromEnv := os.Getenv("PUSH_ENDPOINT") if debug { log.Printf("[%s] Debug is enabled.", time.Now().Format("2006-01-02 15:04:05")) + metricsFile = "./db/metrics.sqlite" + + if endpointFromEnv == "" { + endpoint = "http://localhost:8000" + } } tokenFromEnv := os.Getenv("TOKEN") @@ -127,68 +132,51 @@ func main() { } token = tokenFromEnv - endpointFromEnv := os.Getenv("PUSH_ENDPOINT") - if gin.Mode() == gin.DebugMode { - if endpointFromEnv == "" { - endpoint = "http://localhost:8000" - } else { - endpoint = endpointFromEnv - } - } else { - if endpointFromEnv == "" { - log.Fatal("PUSH_ENDPOINT environment variable is required") - } else { - endpoint = endpointFromEnv - } + if endpoint == "" && endpointFromEnv == "" { + log.Fatal("PUSH_ENDPOINT environment variable is required") } + + if endpoint == "" { + endpoint = endpointFromEnv + } + pushUrl = endpoint + pushPath - if os.Getenv("PUSH_INTERVAL_SECONDS") != "" { - pushIntervalSecondsFromEnv := os.Getenv("PUSH_INTERVAL_SECONDS") - if pushIntervalSecondsFromEnv != "" { - pushIntervalSecondsInt, err := strconv.Atoi(pushIntervalSecondsFromEnv) - if err != nil { - log.Printf("Error converting PUSH_INTERVAL_SECONDS to int: %v", err) - } else { - pushIntervalSeconds = pushIntervalSecondsInt - } + if pushIntervalSecondsFromEnv := os.Getenv("PUSH_INTERVAL_SECONDS"); pushIntervalSecondsFromEnv != "" { + pushIntervalSecondsInt, err := strconv.Atoi(pushIntervalSecondsFromEnv) + if err != nil { + log.Printf("Error converting PUSH_INTERVAL_SECONDS to int: %v", err) + } else { + pushIntervalSeconds = pushIntervalSecondsInt } } - if os.Getenv("COLLECTOR_ENABLED") != "" { - collectorEnabledFromEnv := os.Getenv("COLLECTOR_ENABLED") - if collectorEnabledFromEnv != "" { - var err error - collectorEnabled, err = strconv.ParseBool(collectorEnabledFromEnv) - if err != nil { - log.Printf("Error parsing COLLECTOR_ENABLED: %v", err) - } + if collectorEnabledFromEnv := os.Getenv("COLLECTOR_ENABLED"); collectorEnabledFromEnv != "" { + var err error + collectorEnabled, err = strconv.ParseBool(collectorEnabledFromEnv) + if err != nil { + log.Printf("Error parsing COLLECTOR_ENABLED: %v", err) } } - if os.Getenv("COLLECTOR_REFRESH_RATE_SECONDS") != "" { - refreshRateSecondsFromEnv := os.Getenv("COLLECTOR_REFRESH_RATE_SECONDS") - if refreshRateSecondsFromEnv != "" { - refreshRateSecondsInt, err := strconv.Atoi(refreshRateSecondsFromEnv) - if err != nil { - log.Printf("Error converting REFRESH_RATE_SECONDS to int: %v", err) + if refreshRateSecondsFromEnv := os.Getenv("COLLECTOR_REFRESH_RATE_SECONDS"); refreshRateSecondsFromEnv != "" { + refreshRateSecondsInt, err := strconv.Atoi(refreshRateSecondsFromEnv) + if err != nil { + log.Printf("Error converting REFRESH_RATE_SECONDS to int: %v", err) + } else { + if refreshRateSecondsInt > 0 { + refreshRateSeconds = refreshRateSecondsInt } else { - if refreshRateSecondsInt > 0 { - refreshRateSeconds = refreshRateSecondsInt - } else { - log.Printf("COLLECTOR_REFRESH_RATE_SECONDS must be greater than 0, using default value: %d", refreshRateSeconds) - } + log.Printf("COLLECTOR_REFRESH_RATE_SECONDS must be greater than 0, using default value: %d", refreshRateSeconds) } } } - if os.Getenv("COLLECTOR_RETENTION_PERIOD_DAYS") != "" { - collectorRetentionPeriodDaysFromEnv := os.Getenv("COLLECTOR_RETENTION_PERIOD_DAYS") - if collectorRetentionPeriodDaysFromEnv != "" { - collectorRetentionPeriodDaysInt, err := strconv.Atoi(collectorRetentionPeriodDaysFromEnv) - if err != nil { - log.Printf("Error converting COLLECTOR_RETENTION_PERIOD_DAYS to int: %v", err) - } else { - collectorRetentionPeriodDays = collectorRetentionPeriodDaysInt - } + if collectorRetentionPeriodDaysFromEnv := os.Getenv("COLLECTOR_RETENTION_PERIOD_DAYS"); collectorRetentionPeriodDaysFromEnv != "" { + collectorRetentionPeriodDaysInt, err := strconv.Atoi(collectorRetentionPeriodDaysFromEnv) + if err != nil { + log.Printf("Error converting COLLECTOR_RETENTION_PERIOD_DAYS to int: %v", err) + } else { + collectorRetentionPeriodDays = collectorRetentionPeriodDaysInt + } } @@ -284,18 +272,9 @@ func main() { c.String(200, "ok") }) - if gin.Mode() == gin.DebugMode { + if debug { setupPushRoute(r) setupDebugRoutes(r) - setupCpuRoutes(r) - setupContainerRoutes(r) - setupMemoryRoutes(r) - } else { - setupCpuRoutes(r) - setupContainerRoutes(r) - setupMemoryRoutes(r) - } - if debug { r.GET("/debug/pprof", func(c *gin.Context) { pprof.Index(c.Writer, c.Request) }) @@ -321,6 +300,11 @@ func main() { pprof.Handler("block").ServeHTTP(c.Writer, c.Request) }) } + + setupCpuRoutes(r) + setupContainerRoutes(r) + setupMemoryRoutes(r) + group, gCtx := errgroup.WithContext(context.Background()) group.Go(func() error { return HandleSignals(gCtx) diff --git a/memory.go b/memory.go index fb8e551..95ff610 100644 --- a/memory.go +++ b/memory.go @@ -96,7 +96,7 @@ func setupMemoryRoutes(r *gin.Engine) { return } timeInt, _ := strconv.ParseInt(usage.Time, 10, 64) - if gin.Mode() == gin.DebugMode { + if debug { usage.HumanFriendlyTime = time.UnixMilli(timeInt).Format(layout) } usages = append(usages, usage) From 6b4376e6c4f7a646f83d0080731341da3c5711b5 Mon Sep 17 00:00:00 2001 From: Laurence Date: Sun, 27 Oct 2024 12:16:16 +0000 Subject: [PATCH 10/19] enhance: Major refactoring of repo --- cmd/cmd.go | 208 ++++++++++ go.mod | 2 +- helpers.go | 42 -- json.go | 27 -- main.go | 363 +----------------- pkg/api/api.go | 39 ++ .../api/controller/container.go | 58 ++- pkg/api/controller/controller.go | 63 +++ cpu.go => pkg/api/controller/cpu.go | 33 +- debug.go => pkg/api/controller/debug.go | 68 ++-- memory.go => pkg/api/controller/memory.go | 33 +- pkg/api/controller/push.go | 19 + collector.go => pkg/collector/collector.go | 102 +++-- pkg/config/config.go | 33 ++ pkg/db/database.go | 171 +++++++++ pkg/dockerClient/dockerClient.go | 40 ++ pkg/json/json.go | 23 ++ push.go => pkg/pusher/push.go | 85 ++-- pkg/types/container.go | 22 ++ pkg/utils/helpers.go | 12 + 20 files changed, 816 insertions(+), 627 deletions(-) create mode 100644 cmd/cmd.go delete mode 100644 helpers.go delete mode 100644 json.go create mode 100644 pkg/api/api.go rename container.go => pkg/api/controller/container.go (69%) create mode 100644 pkg/api/controller/controller.go rename cpu.go => pkg/api/controller/cpu.go (66%) rename debug.go => pkg/api/controller/debug.go (66%) rename memory.go => pkg/api/controller/memory.go (73%) create mode 100644 pkg/api/controller/push.go rename collector.go => pkg/collector/collector.go (61%) create mode 100644 pkg/config/config.go create mode 100644 pkg/db/database.go create mode 100644 pkg/dockerClient/dockerClient.go create mode 100644 pkg/json/json.go rename push.go => pkg/pusher/push.go (58%) create mode 100644 pkg/types/container.go create mode 100644 pkg/utils/helpers.go diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..2ceab14 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,208 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "strconv" + "syscall" + "time" + + "github.com/coollabsio/sentinel/pkg/api" + "github.com/coollabsio/sentinel/pkg/collector" + "github.com/coollabsio/sentinel/pkg/config" + "github.com/coollabsio/sentinel/pkg/db" + "github.com/coollabsio/sentinel/pkg/dockerClient" + push "github.com/coollabsio/sentinel/pkg/pusher" + "github.com/gin-gonic/gin" + "github.com/joho/godotenv" + _ "github.com/mattn/go-sqlite3" + "golang.org/x/sync/errgroup" +) + +// HTTP client (Docker) with connection pooling +var dockerHttpClient = dockerClient.New() + +func HandleSignals(ctx context.Context) error { + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGTERM, os.Interrupt) + + select { + case s := <-signalChan: + switch s { + case syscall.SIGTERM: + return errors.New("received SIGTERM") + case os.Interrupt: // cross-platform SIGINT + return errors.New("received interrupt") + } + case <-ctx.Done(): + return ctx.Err() + } + + return nil +} + +func Execute() error { + config := config.NewDefaultConfig() + if _, err := os.Stat(".env"); os.IsNotExist(err) { + log.Println("No .env file found, skipping load") + } else { + err := godotenv.Load() + if err != nil { + log.Printf("Error loading .env file: %v", err) + } + } + debugFromEnv := os.Getenv("DEBUG") + if debugFromEnv != "" { + var err error + config.Debug, err = strconv.ParseBool(debugFromEnv) + if err != nil { + log.Printf("Error parsing DEBUG: %v", err) + } + } + + if config.Debug && gin.Mode() != gin.DebugMode { + gin.SetMode(gin.DebugMode) + } + + endpointFromEnv := os.Getenv("PUSH_ENDPOINT") + if config.Debug { + log.Printf("[%s] Debug is enabled.", time.Now().Format("2006-01-02 15:04:05")) + config.MetricsFile = "./db/metrics.sqlite" + + if endpointFromEnv == "" { + config.Endpoint = "http://localhost:8000" + } + } + + tokenFromEnv := os.Getenv("TOKEN") + if tokenFromEnv == "" { + return fmt.Errorf("TOKEN environment variable is required") + } + config.Token = tokenFromEnv + + if config.Endpoint == "" && endpointFromEnv == "" { + return fmt.Errorf("PUSH_ENDPOINT environment variable is required") + } + + if config.Endpoint == "" { + config.Endpoint = endpointFromEnv + } + + config.PushUrl = config.Endpoint + config.PushPath + + if pushIntervalSecondsFromEnv := os.Getenv("PUSH_INTERVAL_SECONDS"); pushIntervalSecondsFromEnv != "" { + pushIntervalSecondsInt, err := strconv.Atoi(pushIntervalSecondsFromEnv) + if err != nil { + log.Printf("Error converting PUSH_INTERVAL_SECONDS to int: %v", err) + } else { + config.PushIntervalSeconds = pushIntervalSecondsInt + } + } + if collectorEnabledFromEnv := os.Getenv("COLLECTOR_ENABLED"); collectorEnabledFromEnv != "" { + var err error + config.CollectorEnabled, err = strconv.ParseBool(collectorEnabledFromEnv) + if err != nil { + log.Printf("Error parsing COLLECTOR_ENABLED: %v", err) + } + } + if refreshRateSecondsFromEnv := os.Getenv("COLLECTOR_REFRESH_RATE_SECONDS"); refreshRateSecondsFromEnv != "" { + refreshRateSecondsInt, err := strconv.Atoi(refreshRateSecondsFromEnv) + if err != nil { + log.Printf("Error converting REFRESH_RATE_SECONDS to int: %v", err) + } else { + if refreshRateSecondsInt > 0 { + config.RefreshRateSeconds = refreshRateSecondsInt + } else { + log.Printf("COLLECTOR_REFRESH_RATE_SECONDS must be greater than 0, using default value: %d", config.RefreshRateSeconds) + } + } + } + + if collectorRetentionPeriodDaysFromEnv := os.Getenv("COLLECTOR_RETENTION_PERIOD_DAYS"); collectorRetentionPeriodDaysFromEnv != "" { + collectorRetentionPeriodDaysInt, err := strconv.Atoi(collectorRetentionPeriodDaysFromEnv) + if err != nil { + log.Printf("Error converting COLLECTOR_RETENTION_PERIOD_DAYS to int: %v", err) + } else { + config.CollectorRetentionPeriodDays = collectorRetentionPeriodDaysInt + } + } + + database, err := db.New(config) + if err != nil { + return err + } + + defer database.Close() + + if err := database.CreateDefaultTables(); err != nil { + return err + } + + server := api.New(config, database) + pusherService := push.New(config, dockerHttpClient) + collectorService := collector.New(config, database, dockerHttpClient) + + group, gCtx := errgroup.WithContext(context.Background()) + group.Go(func() error { + return HandleSignals(gCtx) + }) + + // TODO: Do we need to run cleanup if collector is disabled? + group.Go(func() error { + database.Run(gCtx) + return nil + }) + + group.Go(func() error { + pusherService.Run(gCtx) + return nil + }) + // Collector + if config.CollectorEnabled { + group.Go(func() error { + collectorService.Run(gCtx) + return nil + }) + } + + group.Go(func() error { + errorChan := make(chan error, 1) + go func() { + defer close(errorChan) + if err := server.Start(); err != nil && err != http.ErrServerClosed { + errorChan <- err + } + }() + select { + case <-gCtx.Done(): + return nil // context cancelled + case err := <-errorChan: + return err + } + }) + if err := group.Wait(); err != nil { + switch err.Error() { + case "received SIGTERM": + log.Println("received SIGTERM shutting down") + case "received interrupt": + log.Println("received interrupt shutting down") + default: + return err // unexpected error we return + } + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := server.Stop(ctx); err != nil { + return err + } + select { + case <-ctx.Done(): + log.Println("server shutdown") + } + return nil +} diff --git a/go.mod b/go.mod index 0d6b621..33802f7 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module sentinel +module github.com/coollabsio/sentinel go 1.23.1 diff --git a/helpers.go b/helpers.go deleted file mode 100644 index 81b3c35..0000000 --- a/helpers.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "log" - "time" -) - -func getUnixTimeInMilliUTC() string { - queryTimeInUnix := time.Now().UTC().UnixMilli() - queryTimeInUnixString := fmt.Sprintf("%d", queryTimeInUnix) - return queryTimeInUnixString -} - -func vacuum() { - go func() { - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in vacuum: %v", r) - } - }() - - _, err := db.Exec("VACUUM") - if err != nil { - log.Printf("Error vacuuming: %v", err) - } - }() -} -func checkpoint() { - go func() { - defer func() { - if r := recover(); r != nil { - log.Printf("Recovered from panic in checkpoint: %v", r) - } - }() - - _, err := db.Exec("CHECKPOINT") - if err != nil { - log.Printf("Error checkpointing: %v", err) - } - }() -} diff --git a/json.go b/json.go deleted file mode 100644 index 7a09e81..0000000 --- a/json.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "io" - - goccyjson "github.com/goccy/go-json" -) - -var JSON jsonWrapper - -type jsonWrapper struct{} - -func (j jsonWrapper) Marshal(v interface{}) ([]byte, error) { - return goccyjson.Marshal(v) -} - -func (j jsonWrapper) Unmarshal(data []byte, v interface{}) error { - return goccyjson.Unmarshal(data, v) -} - -func (j jsonWrapper) NewDecoder(r io.Reader) *goccyjson.Decoder { - return goccyjson.NewDecoder(r) -} - -func (j jsonWrapper) NewEncoder(w io.Writer) *goccyjson.Encoder { - return goccyjson.NewEncoder(w) -} diff --git a/main.go b/main.go index 495ea64..b167837 100644 --- a/main.go +++ b/main.go @@ -1,372 +1,13 @@ package main import ( - "context" - "database/sql" - "errors" "log" - "net" - "net/http" - "net/http/pprof" - "os" - "os/signal" - "path/filepath" - "strconv" - "syscall" - "time" - "github.com/gin-gonic/gin" - "github.com/joho/godotenv" - _ "github.com/mattn/go-sqlite3" - "golang.org/x/sync/errgroup" + "github.com/coollabsio/sentinel/cmd" ) -var debug bool = false -var refreshRateSeconds int = 5 - -var pushEnabled bool = true -var pushIntervalSeconds int = 60 -var pushPath string = "/api/v1/sentinel/push" -var pushUrl string - -var db *sql.DB -var token string -var endpoint string -var metricsFile string = "/app/db/metrics.sqlite" -var collectorEnabled bool = false -var collectorRetentionPeriodDays int = 7 - -// HTTP client (Docker) with connection pooling -var dockerHttpClient = &http.Client{ - Transport: &http.Transport{ - MaxIdleConns: 100, - MaxIdleConnsPerHost: 100, - IdleConnTimeout: 90 * time.Second, - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return net.Dial("unix", "/var/run/docker.sock") - }, - }, - Timeout: 10 * time.Second, -} - -// HTTP client (other) with connection pooling -var httpClient = &http.Client{ - Transport: &http.Transport{ - MaxIdleConns: 100, - MaxIdleConnsPerHost: 100, - IdleConnTimeout: 90 * time.Second, - }, - Timeout: 10 * time.Second, -} - -func Token() gin.HandlerFunc { - return func(c *gin.Context) { - if c.GetHeader("Authorization") != "Bearer "+token { - if gin.Mode() == gin.DebugMode { - if c.Query("token") == token { - c.Next() - return - } - } - c.JSON(401, gin.H{ - "error": "Unauthorized", - }) - c.Abort() - return - } - c.Next() - } -} - -func HandleSignals(ctx context.Context) error { - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, syscall.SIGTERM, os.Interrupt) - - select { - case s := <-signalChan: - switch s { - case syscall.SIGTERM: - return errors.New("received SIGTERM") - case os.Interrupt: // cross-platform SIGINT - return errors.New("received interrupt") - } - case <-ctx.Done(): - return ctx.Err() - } - - return nil -} - func main() { - if _, err := os.Stat(".env"); os.IsNotExist(err) { - log.Println("No .env file found, skipping load") - } else { - err := godotenv.Load() - if err != nil { - log.Printf("Error loading .env file: %v", err) - } - } - debugFromEnv := os.Getenv("DEBUG") - if debugFromEnv != "" { - var err error - debug, err = strconv.ParseBool(debugFromEnv) - if err != nil { - log.Printf("Error parsing DEBUG: %v", err) - } - } - - debug = debug || gin.Mode() == gin.DebugMode - endpointFromEnv := os.Getenv("PUSH_ENDPOINT") - if debug { - log.Printf("[%s] Debug is enabled.", time.Now().Format("2006-01-02 15:04:05")) - metricsFile = "./db/metrics.sqlite" - - if endpointFromEnv == "" { - endpoint = "http://localhost:8000" - } - } - - tokenFromEnv := os.Getenv("TOKEN") - if tokenFromEnv == "" { - log.Fatal("TOKEN environment variable is required") - } - token = tokenFromEnv - - if endpoint == "" && endpointFromEnv == "" { - log.Fatal("PUSH_ENDPOINT environment variable is required") - } - - if endpoint == "" { - endpoint = endpointFromEnv - } - - pushUrl = endpoint + pushPath - - if pushIntervalSecondsFromEnv := os.Getenv("PUSH_INTERVAL_SECONDS"); pushIntervalSecondsFromEnv != "" { - pushIntervalSecondsInt, err := strconv.Atoi(pushIntervalSecondsFromEnv) - if err != nil { - log.Printf("Error converting PUSH_INTERVAL_SECONDS to int: %v", err) - } else { - pushIntervalSeconds = pushIntervalSecondsInt - } - } - if collectorEnabledFromEnv := os.Getenv("COLLECTOR_ENABLED"); collectorEnabledFromEnv != "" { - var err error - collectorEnabled, err = strconv.ParseBool(collectorEnabledFromEnv) - if err != nil { - log.Printf("Error parsing COLLECTOR_ENABLED: %v", err) - } - } - if refreshRateSecondsFromEnv := os.Getenv("COLLECTOR_REFRESH_RATE_SECONDS"); refreshRateSecondsFromEnv != "" { - refreshRateSecondsInt, err := strconv.Atoi(refreshRateSecondsFromEnv) - if err != nil { - log.Printf("Error converting REFRESH_RATE_SECONDS to int: %v", err) - } else { - if refreshRateSecondsInt > 0 { - refreshRateSeconds = refreshRateSecondsInt - } else { - log.Printf("COLLECTOR_REFRESH_RATE_SECONDS must be greater than 0, using default value: %d", refreshRateSeconds) - } - } - } - - if collectorRetentionPeriodDaysFromEnv := os.Getenv("COLLECTOR_RETENTION_PERIOD_DAYS"); collectorRetentionPeriodDaysFromEnv != "" { - collectorRetentionPeriodDaysInt, err := strconv.Atoi(collectorRetentionPeriodDaysFromEnv) - if err != nil { - log.Printf("Error converting COLLECTOR_RETENTION_PERIOD_DAYS to int: %v", err) - } else { - collectorRetentionPeriodDays = collectorRetentionPeriodDaysInt - - } - } - - // create directory based on metricsFile - dir := filepath.Dir(metricsFile) - if err := os.MkdirAll(dir, 0750); err != nil { - log.Fatal(err) - } - // make sure the directory has 0750 permissions - if err := os.Chmod(dir, 0750); err != nil { + if err := cmd.Execute(); err != nil { log.Fatal(err) } - - var err error - db, err = sql.Open("sqlite3", metricsFile) - if err != nil { - log.Fatal(err) - } - defer db.Close() - - // Create tables - // CPU - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS cpu_usage ( - time VARCHAR, - percent VARCHAR, - PRIMARY KEY (time) - )`) - if err != nil { - log.Fatal(err) - } - // Container CPU - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS container_cpu_usage ( - time VARCHAR, - container_id VARCHAR, - percent VARCHAR, - PRIMARY KEY (time, container_id) - )`) - if err != nil { - log.Fatal(err) - } - // Create an index on the container_cpu_usage table for better query performance - _, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_container_cpu_usage_time_container_id ON container_cpu_usage (container_id,time)`) - if err != nil { - log.Fatal(err) - } - - // Memory - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS memory_usage ( - time VARCHAR, - total VARCHAR, - available VARCHAR, - used VARCHAR, - usedPercent VARCHAR, - free VARCHAR, - PRIMARY KEY (time) - )`) - if err != nil { - log.Fatal(err) - } - // Container Memory - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS container_memory_usage ( - time VARCHAR, - container_id VARCHAR, - total VARCHAR, - available VARCHAR, - used VARCHAR, - usedPercent VARCHAR, - free VARCHAR, - PRIMARY KEY (time, container_id) - )`) - if err != nil { - log.Fatal(err) - } - // Create an index on the container_memory_usage table for better query performance - _, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_container_memory_usage_time_container_id ON container_memory_usage (time, container_id)`) - if err != nil { - log.Fatal(err) - } - - // Container Logs - _, err = db.Exec(`CREATE TABLE IF NOT EXISTS container_logs (time VARCHAR, container_id VARCHAR, log VARCHAR)`) - if err != nil { - log.Fatal(err) - } - // Create an index on the container_logs table for better query performance - _, err = db.Exec(`CREATE INDEX IF NOT EXISTS idx_container_logs_time_container_id ON container_logs (time, container_id)`) - if err != nil { - log.Fatal(err) - } - - r := gin.Default() - r.GET("/api/health", func(c *gin.Context) { - c.String(200, "ok") - }) - - if debug { - setupPushRoute(r) - setupDebugRoutes(r) - r.GET("/debug/pprof", func(c *gin.Context) { - pprof.Index(c.Writer, c.Request) - }) - r.GET("/debug/cmdline", func(c *gin.Context) { - pprof.Cmdline(c.Writer, c.Request) - }) - r.GET("/debug/profile", func(c *gin.Context) { - pprof.Profile(c.Writer, c.Request) - }) - r.GET("/debug/symbol", func(c *gin.Context) { - pprof.Symbol(c.Writer, c.Request) - }) - r.GET("/debug/trace", func(c *gin.Context) { - pprof.Trace(c.Writer, c.Request) - }) - r.GET("/debug/heap", func(c *gin.Context) { - pprof.Handler("heap").ServeHTTP(c.Writer, c.Request) - }) - r.GET("/debug/goroutine", func(c *gin.Context) { - pprof.Handler("goroutine").ServeHTTP(c.Writer, c.Request) - }) - r.GET("/debug/block", func(c *gin.Context) { - pprof.Handler("block").ServeHTTP(c.Writer, c.Request) - }) - } - - setupCpuRoutes(r) - setupContainerRoutes(r) - setupMemoryRoutes(r) - - group, gCtx := errgroup.WithContext(context.Background()) - group.Go(func() error { - return HandleSignals(gCtx) - }) - group.Go(func() error { - setupPush(gCtx) - return nil - }) - // Collector - if collectorEnabled { - group.Go(func() error { - collector(gCtx) - return nil - }) - } - cleanup() - srv := &http.Server{ - Addr: ":8888", - Handler: r.Handler(), - } - group.Go(func() error { - errorChan := make(chan error, 1) - go func() { - defer close(errorChan) - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - errorChan <- err - } - }() - select { - case <-gCtx.Done(): - return nil // context cancelled - case err := <-errorChan: - return err - } - }) - if err := group.Wait(); err != nil { - switch err.Error() { - case "received SIGTERM": - log.Println("received SIGTERM shutting down") - case "received interrupt": - log.Println("received interrupt shutting down") - default: - log.Fatal(err) // unexpected error - } - } - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(ctx); err != nil { - log.Fatal(err) // failure/timeout shutting down the server gracefully - } - select { - case <-ctx.Done(): - log.Println("server shutdown") - } -} - -func makeDockerRequest(url string) (*http.Response, error) { - req, err := http.NewRequest("GET", "http://localhost"+url, nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - - return dockerHttpClient.Do(req) } diff --git a/pkg/api/api.go b/pkg/api/api.go new file mode 100644 index 0000000..6e775a4 --- /dev/null +++ b/pkg/api/api.go @@ -0,0 +1,39 @@ +package api + +import ( + "context" + "net/http" + + "github.com/coollabsio/sentinel/pkg/api/controller" + "github.com/coollabsio/sentinel/pkg/config" + "github.com/coollabsio/sentinel/pkg/db" +) + +type Api struct { + controller *controller.Controller + srv *http.Server +} + +func New(config *config.Config, database *db.Database) *Api { + controller := controller.New(config, database) + controller.SetupRoutes() + if config.Debug { + controller.SetupDebugRoutes() + } + srv := &http.Server{ + Addr: config.BindAddr, + Handler: controller.GetEngine().Handler(), + } + return &Api{ + controller: controller, + srv: srv, + } +} + +func (a *Api) Start() error { + return a.srv.ListenAndServe() +} + +func (a *Api) Stop(ctx context.Context) error { + return a.srv.Shutdown(ctx) +} diff --git a/container.go b/pkg/api/controller/container.go similarity index 69% rename from container.go rename to pkg/api/controller/container.go index c9c41a8..978194a 100644 --- a/container.go +++ b/pkg/api/controller/container.go @@ -1,4 +1,4 @@ -package main +package controller import ( "regexp" @@ -9,27 +9,17 @@ import ( "github.com/gin-gonic/gin" ) -type Container struct { - Time string `json:"time"` - ID string `json:"id"` - Image string `json:"image"` - Name string `json:"name"` - State string `json:"state"` - Labels map[string]string `json:"labels"` - HealthStatus string `json:"health_status"` -} - var containerIdRegex = regexp.MustCompile(`[^a-zA-Z0-9]+`) -func setupContainerRoutes(r *gin.Engine) { - r.GET("/api/container/:containerId/cpu/history", func(c *gin.Context) { - containerID := strings.ReplaceAll(c.Param("containerId"), "/", "") +func (c *Controller) setupContainerRoutes() { + c.ginE.GET("/api/container/:containerId/cpu/history", func(ctx *gin.Context) { + containerID := strings.ReplaceAll(ctx.Param("containerId"), "/", "") containerID = containerIdRegex.ReplaceAllString(containerID, "") - from := c.Query("from") + from := ctx.Query("from") if from == "" { from = "1970-01-01T00:00:01Z" } - to := c.Query("to") + to := ctx.Query("to") if to == "" { to = time.Now().UTC().Format("2006-01-02T15:04:05Z") } @@ -38,13 +28,13 @@ func setupContainerRoutes(r *gin.Engine) { layout := "2006-01-02T15:04:05Z" if from != "" { if _, err := time.Parse(layout, from); err != nil { - c.JSON(400, gin.H{"error": "Invalid 'from' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) + ctx.JSON(400, gin.H{"error": "Invalid 'from' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) return } } if to != "" { if _, err := time.Parse(layout, to); err != nil { - c.JSON(400, gin.H{"error": "Invalid 'to' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) + ctx.JSON(400, gin.H{"error": "Invalid 'to' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) return } } @@ -68,9 +58,9 @@ func setupContainerRoutes(r *gin.Engine) { params = append(params, toTime.UnixMilli()) } query += " ORDER BY CAST(time AS BIGINT) ASC" - rows, err := db.Query(query, params...) + rows, err := c.database.Query(query, params...) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } defer rows.Close() @@ -80,25 +70,25 @@ func setupContainerRoutes(r *gin.Engine) { var usage CpuUsage var containerID string if err := rows.Scan(&usage.Time, &containerID, &usage.Percent); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } timeInt, _ := strconv.ParseInt(usage.Time, 10, 64) - if debug { + if gin.Mode() == gin.DebugMode { usage.HumanFriendlyTime = time.UnixMilli(timeInt).Format(layout) } usages = append(usages, usage) } - c.JSON(200, usages) + ctx.JSON(200, usages) }) - r.GET("/api/container/:containerId/memory/history", func(c *gin.Context) { - containerID := strings.ReplaceAll(c.Param("containerId"), "/", "") + c.ginE.GET("/api/container/:containerId/memory/history", func(ctx *gin.Context) { + containerID := strings.ReplaceAll(ctx.Param("containerId"), "/", "") containerID = regexp.MustCompile(`[^a-zA-Z0-9]+`).ReplaceAllString(containerID, "") - from := c.Query("from") + from := ctx.Query("from") if from == "" { from = "1970-01-01T00:00:01Z" } - to := c.Query("to") + to := ctx.Query("to") if to == "" { to = time.Now().UTC().Format("2006-01-02T15:04:05Z") } @@ -107,13 +97,13 @@ func setupContainerRoutes(r *gin.Engine) { layout := "2006-01-02T15:04:05Z" if from != "" { if _, err := time.Parse(layout, from); err != nil { - c.JSON(400, gin.H{"error": "Invalid 'from' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) + ctx.JSON(400, gin.H{"error": "Invalid 'from' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) return } } if to != "" { if _, err := time.Parse(layout, to); err != nil { - c.JSON(400, gin.H{"error": "Invalid 'to' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) + ctx.JSON(400, gin.H{"error": "Invalid 'to' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) return } } @@ -137,9 +127,9 @@ func setupContainerRoutes(r *gin.Engine) { params = append(params, toTime.UnixMilli()) } query += " ORDER BY CAST(time AS BIGINT) ASC" - rows, err := db.Query(query, params...) + rows, err := c.database.Query(query, params...) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } defer rows.Close() @@ -149,15 +139,15 @@ func setupContainerRoutes(r *gin.Engine) { var usage MemUsage var containerID string if err := rows.Scan(&usage.Time, &containerID, &usage.Total, &usage.Available, &usage.Used, &usage.UsedPercent, &usage.Free); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } timeInt, _ := strconv.ParseInt(usage.Time, 10, 64) - if debug { + if gin.Mode() == gin.DebugMode { usage.HumanFriendlyTime = time.UnixMilli(timeInt).Format(layout) } usages = append(usages, usage) } - c.JSON(200, usages) + ctx.JSON(200, usages) }) } diff --git a/pkg/api/controller/controller.go b/pkg/api/controller/controller.go new file mode 100644 index 0000000..ee7bcd6 --- /dev/null +++ b/pkg/api/controller/controller.go @@ -0,0 +1,63 @@ +package controller + +import ( + "net/http/pprof" + + "github.com/coollabsio/sentinel/pkg/config" + "github.com/coollabsio/sentinel/pkg/db" + "github.com/gin-gonic/gin" +) + +type Controller struct { + database *db.Database + ginE *gin.Engine + config *config.Config +} + +func New(config *config.Config, database *db.Database) *Controller { + return &Controller{ + database: database, + ginE: gin.Default(), + config: config, + } +} + +func (c *Controller) GetEngine() *gin.Engine { + return c.ginE +} + +func (c *Controller) SetupRoutes() { + c.setupContainerRoutes() + c.setupMemoryRoutes() + c.setupCpuRoutes() +} + +// TODO: Implement c.setupPushRoutes() +func (c *Controller) SetupDebugRoutes() { + c.setupDebugRoutes() + debugGroup := c.ginE.Group("/debug") + debugGroup.GET("/pprof", func(c *gin.Context) { + pprof.Index(c.Writer, c.Request) + }) + debugGroup.GET("/cmdline", func(c *gin.Context) { + pprof.Cmdline(c.Writer, c.Request) + }) + debugGroup.GET("/profile", func(c *gin.Context) { + pprof.Profile(c.Writer, c.Request) + }) + debugGroup.GET("/symbol", func(c *gin.Context) { + pprof.Symbol(c.Writer, c.Request) + }) + debugGroup.GET("/trace", func(c *gin.Context) { + pprof.Trace(c.Writer, c.Request) + }) + debugGroup.GET("/heap", func(c *gin.Context) { + pprof.Handler("heap").ServeHTTP(c.Writer, c.Request) + }) + debugGroup.GET("/goroutine", func(c *gin.Context) { + pprof.Handler("goroutine").ServeHTTP(c.Writer, c.Request) + }) + debugGroup.GET("/block", func(c *gin.Context) { + pprof.Handler("block").ServeHTTP(c.Writer, c.Request) + }) +} diff --git a/cpu.go b/pkg/api/controller/cpu.go similarity index 66% rename from cpu.go rename to pkg/api/controller/cpu.go index 2d1d9b2..c6f05a2 100644 --- a/cpu.go +++ b/pkg/api/controller/cpu.go @@ -1,9 +1,10 @@ -package main +package controller import ( "strconv" "time" + "github.com/coollabsio/sentinel/pkg/utils" "github.com/gin-gonic/gin" "github.com/shirou/gopsutil/cpu" ) @@ -14,22 +15,22 @@ type CpuUsage struct { HumanFriendlyTime string `json:"human_friendly_time,omitempty"` } -func setupCpuRoutes(r *gin.Engine) { - r.GET("/api/cpu/current", func(c *gin.Context) { - queryTimeInUnixString := getUnixTimeInMilliUTC() +func (c *Controller) setupCpuRoutes() { + c.ginE.GET("/api/cpu/current", func(ctx *gin.Context) { + queryTimeInUnixString := utils.GetUnixTimeInMilliUTC() overallPercentage, err := cpu.Percent(0, false) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } - c.JSON(200, gin.H{"time": queryTimeInUnixString, "percent": overallPercentage[0]}) + ctx.JSON(200, gin.H{"time": queryTimeInUnixString, "percent": overallPercentage[0]}) }) - r.GET("/api/cpu/history", func(c *gin.Context) { - from := c.Query("from") + c.ginE.GET("/api/cpu/history", func(ctx *gin.Context) { + from := ctx.Query("from") if from == "" { from = "1970-01-01T00:00:00Z" } - to := c.Query("to") + to := ctx.Query("to") if to == "" { to = time.Now().UTC().Format("2006-01-02T15:04:05Z") } @@ -38,13 +39,13 @@ func setupCpuRoutes(r *gin.Engine) { layout := "2006-01-02T15:04:05Z" if from != "" { if _, err := time.Parse(layout, from); err != nil { - c.JSON(400, gin.H{"error": "Invalid 'from' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) + ctx.JSON(400, gin.H{"error": "Invalid 'from' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) return } } if to != "" { if _, err := time.Parse(layout, to); err != nil { - c.JSON(400, gin.H{"error": "Invalid 'to' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) + ctx.JSON(400, gin.H{"error": "Invalid 'to' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) return } } @@ -67,9 +68,9 @@ func setupCpuRoutes(r *gin.Engine) { params = append(params, toTime.UnixMilli()) } query += " ORDER BY CAST(time AS BIGINT) ASC" - rows, err := db.Query(query, params...) + rows, err := c.database.Query(query, params...) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } defer rows.Close() @@ -78,15 +79,15 @@ func setupCpuRoutes(r *gin.Engine) { for rows.Next() { var usage CpuUsage if err := rows.Scan(&usage.Time, &usage.Percent); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } timeInt, _ := strconv.ParseInt(usage.Time, 10, 64) - if debug { + if gin.Mode() == gin.DebugMode { usage.HumanFriendlyTime = time.UnixMilli(timeInt).Format(layout) } usages = append(usages, usage) } - c.JSON(200, usages) + ctx.JSON(200, usages) }) } diff --git a/debug.go b/pkg/api/controller/debug.go similarity index 66% rename from debug.go rename to pkg/api/controller/debug.go index f9284c0..79bc99d 100644 --- a/debug.go +++ b/pkg/api/controller/debug.go @@ -1,4 +1,4 @@ -package main +package controller import ( "fmt" @@ -7,45 +7,45 @@ import ( "math/rand/v2" "time" + "github.com/coollabsio/sentinel/pkg/db" "github.com/gin-gonic/gin" "github.com/shirou/gopsutil/mem" ) -func setupDebugRoutes(r *gin.Engine) { - r.GET("/api/export/cpu_usage/csv", func(c *gin.Context) { - rows, err := db.Query("COPY cpu_usage TO 'output/cpu_usage.csv';") +func (c *Controller) setupDebugRoutes() { + c.ginE.GET("/api/export/cpu_usage/csv", func(ctx *gin.Context) { + rows, err := c.database.Query("COPY cpu_usage TO 'output/cpu_usage.csv';") if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } defer rows.Close() - }) - r.GET("/api/load/cpu", func(c *gin.Context) { - createTestCpuData() - c.String(200, "ok, cpu load running in the background") + c.ginE.GET("/api/load/cpu", func(ctx *gin.Context) { + createTestCpuData(c.database) + ctx.String(200, "ok, cpu load running in the background") }) - r.GET("/api/load/memory", func(c *gin.Context) { - createTestMemoryData() - c.String(200, "ok, memory load running in the background") + c.ginE.GET("/api/load/memory", func(ctx *gin.Context) { + createTestMemoryData(c.database) + ctx.String(200, "ok, memory load running in the background") }) - r.GET("/api/vacuum", func(c *gin.Context) { - vacuum() - c.String(200, "ok") + c.ginE.GET("/api/vacuum", func(ctx *gin.Context) { + c.database.Vacuum() + ctx.String(200, "ok") }) - r.GET("/api/checkpoint", func(c *gin.Context) { - checkpoint() - c.String(200, "ok") + c.ginE.GET("/api/checkpoint", func(ctx *gin.Context) { + c.database.Checkpoint() + ctx.String(200, "ok") }) - r.GET("/api/stats", func(c *gin.Context) { + c.ginE.GET("/api/stats", func(ctx *gin.Context) { var count int var storageUsage int64 - err := db.QueryRow("SELECT COUNT(*), SUM(LENGTH(time) + LENGTH(percent)) FROM cpu_usage").Scan(&count, &storageUsage) + err := c.database.QueryRow("SELECT COUNT(*), SUM(LENGTH(time) + LENGTH(percent)) FROM cpu_usage").Scan(&count, &storageUsage) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } @@ -54,12 +54,12 @@ func setupDebugRoutes(r *gin.Engine) { // add memory stats memory, err := mem.VirtualMemory() if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } // Query to get table names and their sizes - rows, err := db.Query(` + rows, err := c.database.Query(` SELECT table_name, SUM(estimated_size) AS total_size @@ -68,7 +68,7 @@ func setupDebugRoutes(r *gin.Engine) { ORDER BY total_size DESC `) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } defer rows.Close() @@ -93,11 +93,11 @@ func setupDebugRoutes(r *gin.Engine) { } if err := rows.Err(); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } - c.JSON(200, gin.H{ + ctx.JSON(200, gin.H{ "row_count": count, "storage_usage_kb": fmt.Sprintf("%.2f", storageKB), "storage_usage_mb": fmt.Sprintf("%.2f", storageKB/1024), @@ -107,11 +107,11 @@ func setupDebugRoutes(r *gin.Engine) { }) } -func createTestCpuData() { +func createTestCpuData(database *db.Database) { go func() { defer func() { - checkpoint() - vacuum() + database.Checkpoint() + database.Vacuum() }() numberOfRows := 10000 @@ -129,7 +129,7 @@ func createTestCpuData() { timestamp := fmt.Sprintf("%d", randomDate.UnixNano()/int64(time.Millisecond)) percent := fmt.Sprintf("%.1f", rand.Float64()*100) - _, err := db.Exec(`INSERT INTO cpu_usage (time, percent) VALUES (?, ?)`, timestamp, percent) + _, err := database.Exec(`INSERT INTO cpu_usage (time, percent) VALUES (?, ?)`, timestamp, percent) results <- err } }() @@ -150,11 +150,11 @@ func createTestCpuData() { }() } -func createTestMemoryData() { +func createTestMemoryData(database *db.Database) { go func() { defer func() { - checkpoint() - vacuum() + database.Checkpoint() + database.Vacuum() }() numberOfRows := 10000 @@ -184,7 +184,7 @@ func createTestMemoryData() { log.Printf("Error getting memory usage: %v", err) continue } - _, err = db.Exec(`INSERT INTO memory_usage (time, total, available, used, usedPercent, free) VALUES (?, ?, ?, ?, ?, ?)`, usage.Time, usage.Total, usage.Available, usage.Used, usage.UsedPercent, usage.Free) + _, err = database.Exec(`INSERT INTO memory_usage (time, total, available, used, usedPercent, free) VALUES (?, ?, ?, ?, ?, ?)`, usage.Time, usage.Total, usage.Available, usage.Used, usage.UsedPercent, usage.Free) results <- err } }() diff --git a/memory.go b/pkg/api/controller/memory.go similarity index 73% rename from memory.go rename to pkg/api/controller/memory.go index 95ff610..ed953aa 100644 --- a/memory.go +++ b/pkg/api/controller/memory.go @@ -1,10 +1,11 @@ -package main +package controller import ( "math" "strconv" "time" + "github.com/coollabsio/sentinel/pkg/utils" "github.com/gin-gonic/gin" "github.com/shirou/gopsutil/mem" ) @@ -19,12 +20,12 @@ type MemUsage struct { HumanFriendlyTime string `json:"human_friendly_time,omitempty"` } -func setupMemoryRoutes(r *gin.Engine) { - r.GET("/api/memory/current", func(c *gin.Context) { - queryTimeInUnixString := getUnixTimeInMilliUTC() +func (c *Controller) setupMemoryRoutes() { + c.ginE.GET("/api/memory/current", func(ctx *gin.Context) { + queryTimeInUnixString := utils.GetUnixTimeInMilliUTC() memory, err := mem.VirtualMemory() if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } @@ -36,14 +37,14 @@ func setupMemoryRoutes(r *gin.Engine) { UsedPercent: math.Round(memory.UsedPercent*100) / 100, Free: memory.Free, } - c.JSON(200, usage) + ctx.JSON(200, usage) }) - r.GET("/api/memory/history", func(c *gin.Context) { - from := c.Query("from") + c.ginE.GET("/api/memory/history", func(ctx *gin.Context) { + from := ctx.Query("from") if from == "" { from = "1970-01-01T00:00:00Z" } - to := c.Query("to") + to := ctx.Query("to") if to == "" { to = time.Now().UTC().Format("2006-01-02T15:04:05Z") } @@ -52,13 +53,13 @@ func setupMemoryRoutes(r *gin.Engine) { layout := "2006-01-02T15:04:05Z" if from != "" { if _, err := time.Parse(layout, from); err != nil { - c.JSON(400, gin.H{"error": "Invalid 'from' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) + ctx.JSON(400, gin.H{"error": "Invalid 'from' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) return } } if to != "" { if _, err := time.Parse(layout, to); err != nil { - c.JSON(400, gin.H{"error": "Invalid 'to' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) + ctx.JSON(400, gin.H{"error": "Invalid 'to' date format. Use YYYY-MM-DDTHH:MM:SSZ"}) return } } @@ -81,9 +82,9 @@ func setupMemoryRoutes(r *gin.Engine) { params = append(params, toTime.UnixMilli()) } query += " ORDER BY CAST(time AS BIGINT) ASC" - rows, err := db.Query(query, params...) + rows, err := c.database.Query(query, params...) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } defer rows.Close() @@ -92,15 +93,15 @@ func setupMemoryRoutes(r *gin.Engine) { for rows.Next() { var usage MemUsage if err := rows.Scan(&usage.Time, &usage.Total, &usage.Available, &usage.Used, &usage.UsedPercent, &usage.Free); err != nil { - c.JSON(500, gin.H{"error": err.Error()}) + ctx.JSON(500, gin.H{"error": err.Error()}) return } timeInt, _ := strconv.ParseInt(usage.Time, 10, 64) - if debug { + if gin.Mode() == gin.DebugMode { usage.HumanFriendlyTime = time.UnixMilli(timeInt).Format(layout) } usages = append(usages, usage) } - c.JSON(200, usages) + ctx.JSON(200, usages) }) } diff --git a/pkg/api/controller/push.go b/pkg/api/controller/push.go new file mode 100644 index 0000000..058b63b --- /dev/null +++ b/pkg/api/controller/push.go @@ -0,0 +1,19 @@ +package controller + +//Comment out till I figure out how to pass pusher to controller context + +// func (c *Controller) setupPushRoute() { +// c.ginE.POST("/api/push", func(ctx *gin.Context) { +// incomingToken := ctx.GetHeader("Authorization") +// if incomingToken != "Bearer "+c.config.Token { +// ctx.JSON(401, gin.H{"error": "Unauthorized"}) +// return +// } +// data, err := push.GetPushData() +// if err != nil { +// ctx.JSON(500, gin.H{"error": err.Error()}) +// return +// } +// ctx.JSON(200, data) +// }) +// } diff --git a/collector.go b/pkg/collector/collector.go similarity index 61% rename from collector.go rename to pkg/collector/collector.go index a5b4123..e82183f 100644 --- a/collector.go +++ b/pkg/collector/collector.go @@ -1,4 +1,4 @@ -package main +package collector import ( "context" @@ -9,24 +9,35 @@ import ( "sync" "time" - "github.com/docker/docker/api/types" + "github.com/coollabsio/sentinel/pkg/config" + "github.com/coollabsio/sentinel/pkg/db" + "github.com/coollabsio/sentinel/pkg/dockerClient" + "github.com/coollabsio/sentinel/pkg/json" + "github.com/coollabsio/sentinel/pkg/types" + "github.com/coollabsio/sentinel/pkg/utils" + dockerTypes "github.com/docker/docker/api/types" "github.com/shirou/gopsutil/cpu" "github.com/shirou/gopsutil/mem" ) -type ContainerMetrics struct { - Name string `json:"name"` - Time string `json:"time"` - CPUUsagePercentage float64 `json:"cpu_usage_percentage"` - MemoryUsagePercentage float64 `json:"memory_usage_percentage"` - MemoryUsed uint64 `json:"memory_used"` - MemoryAvailable uint64 `json:"available_memory"` +type Collector struct { + config *config.Config + client *dockerClient.DockerClient + database *db.Database } -func collector(ctx context.Context) { - fmt.Printf("[%s] Starting metrics recorder with refresh rate of %d seconds and retention period of %d days.\n", time.Now().Format("2006-01-02 15:04:05"), refreshRateSeconds, collectorRetentionPeriodDays) +func New(config *config.Config, database *db.Database, dockerClient *dockerClient.DockerClient) *Collector { + return &Collector{ + config: config, + client: dockerClient, + database: database, + } +} - ticker := time.NewTicker(time.Duration(refreshRateSeconds) * time.Second) +func (c *Collector) Run(ctx context.Context) { + fmt.Printf("[%s] Starting metrics recorder with refresh rate of %d seconds and retention period of %d days.\n", time.Now().Format("2006-01-02 15:04:05"), c.config.RefreshRateSeconds, c.config.CollectorRetentionPeriodDays) + + ticker := time.NewTicker(time.Duration(c.config.RefreshRateSeconds) * time.Second) defer ticker.Stop() for { @@ -42,7 +53,7 @@ func collector(ctx context.Context) { } }() - queryTimeInUnixString := getUnixTimeInMilliUTC() + queryTimeInUnixString := utils.GetUnixTimeInMilliUTC() // CPU usage overallPercentage, err := cpu.Percent(0, false) @@ -51,12 +62,12 @@ func collector(ctx context.Context) { return } - _, err = db.Exec(`INSERT INTO cpu_usage (time, percent) VALUES (?, ?)`, queryTimeInUnixString, fmt.Sprintf("%.2f", overallPercentage[0])) + _, err = c.database.Exec(`INSERT INTO cpu_usage (time, percent) VALUES (?, ?)`, queryTimeInUnixString, fmt.Sprintf("%.2f", overallPercentage[0])) if err != nil { log.Printf("Error inserting CPU usage into database: %v", err) } - collectContainerMetrics(queryTimeInUnixString) + c.collectContainerMetrics(queryTimeInUnixString) // Memory usage memory, err := mem.VirtualMemory() @@ -65,7 +76,7 @@ func collector(ctx context.Context) { return } - _, err = db.Exec(`INSERT INTO memory_usage (time, total, available, used, usedPercent, free) VALUES (?, ?, ?, ?, ?, ?)`, + _, err = c.database.Exec(`INSERT INTO memory_usage (time, total, available, used, usedPercent, free) VALUES (?, ?, ?, ?, ?, ?)`, queryTimeInUnixString, memory.Total, memory.Available, memory.Used, math.Round(memory.UsedPercent*100)/100, memory.Free) if err != nil { log.Printf("Error inserting memory usage into database: %v", err) @@ -74,18 +85,18 @@ func collector(ctx context.Context) { // Cleanup old data totalRowsToKeep := 10 currentTime := time.Now().UTC().UnixMilli() - cutoffTime := currentTime - int64(collectorRetentionPeriodDays*24*60*60*1000) + cutoffTime := currentTime - int64(c.config.CollectorRetentionPeriodDays*24*60*60*1000) cleanupTable := func(tableName string) { var totalRows int - err := db.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", tableName)).Scan(&totalRows) + err := c.database.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", tableName)).Scan(&totalRows) if err != nil { log.Printf("Error counting rows in %s: %v", tableName, err) return } if totalRows > totalRowsToKeep { - _, err = db.Exec(fmt.Sprintf(`DELETE FROM %s WHERE CAST(time AS BIGINT) < ? AND time NOT IN (SELECT time FROM %s ORDER BY time DESC LIMIT ?)`, tableName, tableName), + _, err = c.database.Exec(fmt.Sprintf(`DELETE FROM %s WHERE CAST(time AS BIGINT) < ? AND time NOT IN (SELECT time FROM %s ORDER BY time DESC LIMIT ?)`, tableName, tableName), cutoffTime, totalRowsToKeep) if err != nil { log.Printf("Error deleting old data from %s: %v", tableName, err) @@ -103,10 +114,10 @@ func collector(ctx context.Context) { } } -func collectContainerMetrics(queryTimeInUnixString string) { +func (c *Collector) collectContainerMetrics(queryTimeInUnixString string) { // Container usage // Use makeDockerRequest to interact with Docker API - resp, err := makeDockerRequest("/containers/json?all=true") + resp, err := c.client.MakeRequest("/containers/json?all=true") if err != nil { log.Printf("Error getting containers: %v", err) return @@ -124,26 +135,26 @@ func collectContainerMetrics(queryTimeInUnixString string) { return } - var containers []types.Container - if err := JSON.Unmarshal(containersOutput, &containers); err != nil { + var containers []dockerTypes.Container + if err := json.Unmarshal(containersOutput, &containers); err != nil { log.Printf("Error unmarshalling container list: %v", err) return } var wg sync.WaitGroup - metricsChannel := make(chan ContainerMetrics, len(containers)) + metricsChannel := make(chan types.ContainerMetrics, len(containers)) errChannel := make(chan error, len(containers)) for _, container := range containers { wg.Add(1) - go func(container types.Container) { + go func(container dockerTypes.Container) { defer wg.Done() containerNameFromLabel := container.Labels["coolify.name"] if containerNameFromLabel == "" { containerNameFromLabel = container.Names[0][1:] } - resp, err := makeDockerRequest(fmt.Sprintf("/containers/%s/stats?stream=false", container.ID)) + resp, err := c.client.MakeRequest(fmt.Sprintf("/containers/%s/stats?stream=false", container.ID)) if err != nil { errChannel <- fmt.Errorf("Error getting container stats for %s: %v", containerNameFromLabel, err) return @@ -156,13 +167,13 @@ func collectContainerMetrics(queryTimeInUnixString string) { return } - var v types.StatsJSON - if err := JSON.Unmarshal(statsOutput, &v); err != nil { + var v dockerTypes.StatsJSON + if err := json.Unmarshal(statsOutput, &v); err != nil { errChannel <- fmt.Errorf("Error decoding container stats for %s: %v", containerNameFromLabel, err) return } - metrics := ContainerMetrics{ + metrics := types.ContainerMetrics{ Name: containerNameFromLabel, CPUUsagePercentage: calculateCPUPercent(v), MemoryUsagePercentage: calculateMemoryPercent(v), @@ -185,13 +196,13 @@ func collectContainerMetrics(queryTimeInUnixString string) { } for metrics := range metricsChannel { - _, err = db.Exec(`INSERT INTO container_cpu_usage (time, container_id, percent) VALUES (?, ?, ?)`, + _, err = c.database.Exec(`INSERT INTO container_cpu_usage (time, container_id, percent) VALUES (?, ?, ?)`, queryTimeInUnixString, metrics.Name, fmt.Sprintf("%.2f", metrics.CPUUsagePercentage)) if err != nil { log.Printf("Error inserting container CPU usage into database: %v", err) } - _, err = db.Exec(`INSERT INTO container_memory_usage (time, container_id, total, available, used, usedPercent, free) VALUES (?, ?, ?, ?, ?, ?, ?)`, + _, err = c.database.Exec(`INSERT INTO container_memory_usage (time, container_id, total, available, used, usedPercent, free) VALUES (?, ?, ?, ?, ?, ?, ?)`, queryTimeInUnixString, metrics.Name, metrics.MemoryAvailable, metrics.MemoryAvailable, metrics.MemoryUsed, metrics.MemoryUsagePercentage, metrics.MemoryAvailable-metrics.MemoryUsed) if err != nil { log.Printf("Error inserting container memory usage into database: %v", err) @@ -199,42 +210,19 @@ func collectContainerMetrics(queryTimeInUnixString string) { } } -func cleanup() { - fmt.Printf("[%s] Removing old data.\n", time.Now().Format("2006-01-02 15:04:05")) - - cutoffTime := time.Now().AddDate(0, 0, -collectorRetentionPeriodDays).UnixMilli() - - _, err := db.Exec(`DELETE FROM cpu_usage WHERE CAST(time AS BIGINT) < ?`, cutoffTime) - if err != nil { - log.Printf("Error removing old data: %v", err) - } - - _, err = db.Exec(`DELETE FROM memory_usage WHERE CAST(time AS BIGINT) < ?`, cutoffTime) - if err != nil { - log.Printf("Error removing old memory data: %v", err) - } - - go func() { - for { - time.Sleep(24 * time.Hour) - cleanup() - } - }() -} - -func calculateCPUPercent(stat types.StatsJSON) float64 { +func calculateCPUPercent(stat dockerTypes.StatsJSON) float64 { cpuDelta := float64(stat.CPUStats.CPUUsage.TotalUsage) - float64(stat.PreCPUStats.CPUUsage.TotalUsage) systemDelta := float64(stat.CPUStats.SystemUsage) - float64(stat.PreCPUStats.SystemUsage) numberOfCpus := stat.CPUStats.OnlineCPUs return (cpuDelta / systemDelta) * float64(numberOfCpus) * 100.0 } -func calculateMemoryPercent(stat types.StatsJSON) float64 { +func calculateMemoryPercent(stat dockerTypes.StatsJSON) float64 { usedMemory := float64(stat.MemoryStats.Usage) - float64(stat.MemoryStats.Stats["cache"]) availableMemory := float64(stat.MemoryStats.Limit) return (usedMemory / availableMemory) * 100.0 } -func calculateMemoryUsed(stat types.StatsJSON) uint64 { +func calculateMemoryUsed(stat dockerTypes.StatsJSON) uint64 { return (stat.MemoryStats.Usage) / 1024 / 1024 } diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..7d0a883 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,33 @@ +package config + +type Config struct { + Debug bool + RefreshRateSeconds int + PushEnabled bool + PushIntervalSeconds int + PushPath string + PushUrl string + Token string + Endpoint string + MetricsFile string + CollectorEnabled bool + CollectorRetentionPeriodDays int + BindAddr string +} + +func NewDefaultConfig() *Config { + return &Config{ + Debug: false, + RefreshRateSeconds: 5, + PushEnabled: true, + PushIntervalSeconds: 60, + PushPath: "/api/v1/sentinel/push", + PushUrl: "", + Token: "", + Endpoint: "", + MetricsFile: "/app/db/metrics.sqlite", + CollectorEnabled: false, + CollectorRetentionPeriodDays: 7, + BindAddr: ":8888", + } +} diff --git a/pkg/db/database.go b/pkg/db/database.go new file mode 100644 index 0000000..ab0a065 --- /dev/null +++ b/pkg/db/database.go @@ -0,0 +1,171 @@ +package db + +import ( + "context" + "database/sql" + "fmt" + "log" + "os" + "path/filepath" + "time" + + "github.com/coollabsio/sentinel/pkg/config" +) + +type Database struct { + db *sql.DB + config *config.Config +} + +func New(config *config.Config) (*Database, error) { + // create directory based on metricsFile + dir := filepath.Dir(config.MetricsFile) + if err := os.MkdirAll(dir, 0750); err != nil { + return nil, err + } + // make sure the directory has 0750 permissions + if err := os.Chmod(dir, 0750); err != nil { + return nil, err + } + + db, err := sql.Open("sqlite3", config.MetricsFile) + if err != nil { + return nil, err + } + + return &Database{ + db: db, + config: config, + }, nil +} + +func (d *Database) Run(ctx context.Context) { + ticker := time.NewTicker(24 * time.Hour) + defer ticker.Stop() + + d.Cleanup() + + for { + select { + case <-ctx.Done(): + fmt.Println("Database cleanup stopped") + return + case <-ticker.C: + d.Cleanup() + } + } +} + +func (d *Database) Cleanup() { + fmt.Printf("[%s] Removing old data.\n", time.Now().Format("2006-01-02 15:04:05")) + + cutoffTime := time.Now().AddDate(0, 0, -d.config.CollectorRetentionPeriodDays).UnixMilli() + + _, err := d.db.Exec(`DELETE FROM cpu_usage WHERE CAST(time AS BIGINT) < ?`, cutoffTime) + if err != nil { + log.Printf("Error removing old data: %v", err) + } + + _, err = d.db.Exec(`DELETE FROM memory_usage WHERE CAST(time AS BIGINT) < ?`, cutoffTime) + if err != nil { + log.Printf("Error removing old memory data: %v", err) + } +} + +func (d *Database) Close() error { + return d.db.Close() +} + +func (d *Database) Exec(query string, args ...interface{}) (sql.Result, error) { + return d.db.Exec(query, args...) +} + +func (d *Database) Query(query string, args ...interface{}) (*sql.Rows, error) { + return d.db.Query(query, args...) +} + +func (d *Database) QueryRow(query string, args ...interface{}) *sql.Row { + return d.db.QueryRow(query, args...) +} + +func (d *Database) Vacuum() error { + _, err := d.db.Exec("VACUUM") + return err +} + +func (d *Database) Checkpoint() error { + _, err := d.db.Exec("CHECKPOINT") + return err +} + +func (d *Database) CreateDefaultTables() error { + var err error + _, err = d.db.Exec(`CREATE TABLE IF NOT EXISTS cpu_usage ( + time VARCHAR, + percent VARCHAR, + PRIMARY KEY (time) + )`) + if err != nil { + return err + } + // Container CPU + _, err = d.db.Exec(`CREATE TABLE IF NOT EXISTS container_cpu_usage ( + time VARCHAR, + container_id VARCHAR, + percent VARCHAR, + PRIMARY KEY (time, container_id) + )`) + if err != nil { + return err + } + // Create an index on the container_cpu_usage table for better query performance + _, err = d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_container_cpu_usage_time_container_id ON container_cpu_usage (container_id,time)`) + if err != nil { + return err + } + + // Memory + _, err = d.db.Exec(`CREATE TABLE IF NOT EXISTS memory_usage ( + time VARCHAR, + total VARCHAR, + available VARCHAR, + used VARCHAR, + usedPercent VARCHAR, + free VARCHAR, + PRIMARY KEY (time) + )`) + if err != nil { + return err + } + // Container Memory + _, err = d.db.Exec(`CREATE TABLE IF NOT EXISTS container_memory_usage ( + time VARCHAR, + container_id VARCHAR, + total VARCHAR, + available VARCHAR, + used VARCHAR, + usedPercent VARCHAR, + free VARCHAR, + PRIMARY KEY (time, container_id) + )`) + if err != nil { + return err + } + // Create an index on the container_memory_usage table for better query performance + _, err = d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_container_memory_usage_time_container_id ON container_memory_usage (time, container_id)`) + if err != nil { + return err + } + + // Container Logs + _, err = d.db.Exec(`CREATE TABLE IF NOT EXISTS container_logs (time VARCHAR, container_id VARCHAR, log VARCHAR)`) + if err != nil { + return err + } + // Create an index on the container_logs table for better query performance + _, err = d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_container_logs_time_container_id ON container_logs (time, container_id)`) + if err != nil { + return err + } + return nil +} diff --git a/pkg/dockerClient/dockerClient.go b/pkg/dockerClient/dockerClient.go new file mode 100644 index 0000000..1fb7fea --- /dev/null +++ b/pkg/dockerClient/dockerClient.go @@ -0,0 +1,40 @@ +package dockerClient + +import ( + "context" + "net" + "net/http" + "time" +) + +/* Docker client is a wrapper around http.Client to easily format request to docker socket */ +type DockerClient struct { + httpClient *http.Client +} + +func New() *DockerClient { + return &DockerClient{ + httpClient: &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return net.Dial("unix", "/var/run/docker.sock") + }, + }, + Timeout: 10 * time.Second, + }, + } +} + +func (d *DockerClient) MakeRequest(url string) (*http.Response, error) { + req, err := http.NewRequest("GET", "http://localhost"+url, nil) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + return d.httpClient.Do(req) +} diff --git a/pkg/json/json.go b/pkg/json/json.go new file mode 100644 index 0000000..2242562 --- /dev/null +++ b/pkg/json/json.go @@ -0,0 +1,23 @@ +package json + +import ( + "io" + + goccyjson "github.com/goccy/go-json" +) + +func Marshal(v interface{}) ([]byte, error) { + return goccyjson.Marshal(v) +} + +func Unmarshal(data []byte, v interface{}) error { + return goccyjson.Unmarshal(data, v) +} + +func NewDecoder(r io.Reader) *goccyjson.Decoder { + return goccyjson.NewDecoder(r) +} + +func NewEncoder(w io.Writer) *goccyjson.Encoder { + return goccyjson.NewEncoder(w) +} diff --git a/push.go b/pkg/pusher/push.go similarity index 58% rename from push.go rename to pkg/pusher/push.go index 3910c8a..97f3027 100644 --- a/push.go +++ b/pkg/pusher/push.go @@ -1,4 +1,4 @@ -package main +package push import ( "bytes" @@ -10,28 +10,36 @@ import ( "syscall" "time" - "github.com/docker/docker/api/types" - "github.com/gin-gonic/gin" + "github.com/coollabsio/sentinel/pkg/config" + "github.com/coollabsio/sentinel/pkg/dockerClient" + "github.com/coollabsio/sentinel/pkg/json" + "github.com/coollabsio/sentinel/pkg/types" + dockerTypes "github.com/docker/docker/api/types" ) -func setupPushRoute(r *gin.Engine) { - r.POST("/api/push", func(c *gin.Context) { - incomingToken := c.GetHeader("Authorization") - if incomingToken != "Bearer "+token { - c.JSON(401, gin.H{"error": "Unauthorized"}) - return - } - data, err := getPushData() - if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return - } - c.JSON(200, data) - }) +type Pusher struct { + config *config.Config + client *http.Client + dockerClient *dockerClient.DockerClient +} + +func New(config *config.Config, dockerClient *dockerClient.DockerClient) *Pusher { + return &Pusher{ + config: config, + client: &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: 100, + MaxIdleConnsPerHost: 100, + IdleConnTimeout: 90 * time.Second, + }, + Timeout: 10 * time.Second, + }, + dockerClient: dockerClient, + } } -func setupPush(ctx context.Context) { - ticker := time.NewTicker(time.Duration(pushIntervalSeconds) * time.Second) +func (p *Pusher) Run(ctx context.Context) { + ticker := time.NewTicker(time.Duration(p.config.PushIntervalSeconds) * time.Second) defer ticker.Stop() for { @@ -40,14 +48,14 @@ func setupPush(ctx context.Context) { fmt.Println("Push operation stopped") return case <-ticker.C: - getPushData() + p.GetPushData() } } } -func getPushData() (map[string]interface{}, error) { - fmt.Printf("[%s] Pushing to [%s]\n", time.Now().Format("2006-01-02 15:04:05"), pushUrl) - containersData, err := containerData() +func (p *Pusher) GetPushData() (map[string]interface{}, error) { + fmt.Printf("[%s] Pushing to [%s]\n", time.Now().Format("2006-01-02 15:04:05"), p.config.PushUrl) + containersData, err := p.containerData() if err != nil { log.Printf("Error getting containers data: %v", err) return nil, err @@ -61,28 +69,27 @@ func getPushData() (map[string]interface{}, error) { "containers": containersData, "filesystem_usage_root": filesystemUsageRoot, } - jsonData, err := JSON.Marshal(data) + jsonData, err := json.Marshal(data) if err != nil { log.Printf("Error marshalling data: %v", err) return nil, err } - req, err := http.NewRequest("POST", pushUrl, bytes.NewBuffer(jsonData)) + req, err := http.NewRequest("POST", p.config.PushUrl, bytes.NewBuffer(jsonData)) if err != nil { log.Printf("Error creating request: %v", err) return nil, err } req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+token) - resp, err := httpClient.Do(req) + req.Header.Set("Authorization", "Bearer "+p.config.Token) + resp, err := p.client.Do(req) if err != nil { - log.Printf("Error pushing to [%s]: %v", pushUrl, err) + log.Printf("Error pushing to [%s]: %v", p.config.PushUrl, err) return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { - body, _ := io.ReadAll(resp.Body) - log.Printf("Error pushing to [%s]: status code %d, response: %s", pushUrl, resp.StatusCode, string(body)) + log.Printf("Error pushing to [%s]: status code %d", p.config.PushUrl, resp.StatusCode) } return data, nil } @@ -103,8 +110,8 @@ func filesystemUsageRoot() (map[string]interface{}, error) { }, nil } -func containerData() ([]Container, error) { - resp, err := makeDockerRequest("/containers/json?all=true") +func (p *Pusher) containerData() ([]types.Container, error) { + resp, err := p.dockerClient.MakeRequest("/containers/json?all=true") if err != nil { log.Printf("Error getting containers: %v", err) return nil, err @@ -117,15 +124,15 @@ func containerData() ([]Container, error) { return nil, err } - var containers []types.Container - if err := JSON.Unmarshal(containersOutput, &containers); err != nil { + var containers []dockerTypes.Container + if err := json.Unmarshal(containersOutput, &containers); err != nil { log.Printf("Error unmarshalling container list: %v", err) return nil, err } - var containersData []Container + var containersData []types.Container for _, container := range containers { - resp, err := makeDockerRequest(fmt.Sprintf("/containers/%s/json", container.ID)) + resp, err := p.dockerClient.MakeRequest(fmt.Sprintf("/containers/%s/json", container.ID)) if err != nil { log.Printf("Error inspecting container %s: %v", container.ID, err) continue @@ -138,8 +145,8 @@ func containerData() ([]Container, error) { continue } - var inspectData types.ContainerJSON - if err := JSON.Unmarshal(inspectOutput, &inspectData); err != nil { + var inspectData dockerTypes.ContainerJSON + if err := json.Unmarshal(inspectOutput, &inspectData); err != nil { log.Printf("Error unmarshalling inspect data for container %s: %v", container.ID, err) continue } @@ -149,7 +156,7 @@ func containerData() ([]Container, error) { healthStatus = inspectData.State.Health.Status } - containersData = append(containersData, Container{ + containersData = append(containersData, types.Container{ Time: time.Now().Format("2006-01-02T15:04:05Z"), ID: container.ID, Image: container.Image, diff --git a/pkg/types/container.go b/pkg/types/container.go new file mode 100644 index 0000000..48af32d --- /dev/null +++ b/pkg/types/container.go @@ -0,0 +1,22 @@ +package types + +/* Package contains types shared between collector and pusher */ + +type ContainerMetrics struct { + Name string `json:"name"` + Time string `json:"time"` + CPUUsagePercentage float64 `json:"cpu_usage_percentage"` + MemoryUsagePercentage float64 `json:"memory_usage_percentage"` + MemoryUsed uint64 `json:"memory_used"` + MemoryAvailable uint64 `json:"available_memory"` +} + +type Container struct { + Time string `json:"time"` + ID string `json:"id"` + Image string `json:"image"` + Name string `json:"name"` + State string `json:"state"` + Labels map[string]string `json:"labels"` + HealthStatus string `json:"health_status"` +} diff --git a/pkg/utils/helpers.go b/pkg/utils/helpers.go new file mode 100644 index 0000000..fe502d0 --- /dev/null +++ b/pkg/utils/helpers.go @@ -0,0 +1,12 @@ +package utils + +import ( + "fmt" + "time" +) + +func GetUnixTimeInMilliUTC() string { + queryTimeInUnix := time.Now().UTC().UnixMilli() + queryTimeInUnixString := fmt.Sprintf("%d", queryTimeInUnix) + return queryTimeInUnixString +} From a3cb138568aa38acb88161683ae694d1adfa1854 Mon Sep 17 00:00:00 2001 From: Laurence Date: Sun, 27 Oct 2024 12:22:22 +0000 Subject: [PATCH 11/19] enhance: rename pusher folder to push, update links --- cmd/cmd.go | 2 +- pkg/{pusher => push}/push.go | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pkg/{pusher => push}/push.go (100%) diff --git a/cmd/cmd.go b/cmd/cmd.go index 2ceab14..2173dc0 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -17,7 +17,7 @@ import ( "github.com/coollabsio/sentinel/pkg/config" "github.com/coollabsio/sentinel/pkg/db" "github.com/coollabsio/sentinel/pkg/dockerClient" - push "github.com/coollabsio/sentinel/pkg/pusher" + "github.com/coollabsio/sentinel/pkg/push" "github.com/gin-gonic/gin" "github.com/joho/godotenv" _ "github.com/mattn/go-sqlite3" diff --git a/pkg/pusher/push.go b/pkg/push/push.go similarity index 100% rename from pkg/pusher/push.go rename to pkg/push/push.go From fd895302366c01d505ee9f6e5b633de817575747 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 29 Oct 2024 12:45:21 +0100 Subject: [PATCH 12/19] Refactor debug routes in controller package --- pkg/api/controller/debug.go | 131 +----------------------------------- 1 file changed, 1 insertion(+), 130 deletions(-) diff --git a/pkg/api/controller/debug.go b/pkg/api/controller/debug.go index 79bc99d..9f6e2b3 100644 --- a/pkg/api/controller/debug.go +++ b/pkg/api/controller/debug.go @@ -3,43 +3,12 @@ package controller import ( "fmt" "log" - "math" - "math/rand/v2" - "time" - "github.com/coollabsio/sentinel/pkg/db" "github.com/gin-gonic/gin" "github.com/shirou/gopsutil/mem" ) func (c *Controller) setupDebugRoutes() { - c.ginE.GET("/api/export/cpu_usage/csv", func(ctx *gin.Context) { - rows, err := c.database.Query("COPY cpu_usage TO 'output/cpu_usage.csv';") - if err != nil { - ctx.JSON(500, gin.H{"error": err.Error()}) - return - } - defer rows.Close() - }) - c.ginE.GET("/api/load/cpu", func(ctx *gin.Context) { - createTestCpuData(c.database) - ctx.String(200, "ok, cpu load running in the background") - }) - c.ginE.GET("/api/load/memory", func(ctx *gin.Context) { - createTestMemoryData(c.database) - ctx.String(200, "ok, memory load running in the background") - }) - - c.ginE.GET("/api/vacuum", func(ctx *gin.Context) { - c.database.Vacuum() - ctx.String(200, "ok") - }) - - c.ginE.GET("/api/checkpoint", func(ctx *gin.Context) { - c.database.Checkpoint() - ctx.String(200, "ok") - }) - c.ginE.GET("/api/stats", func(ctx *gin.Context) { var count int var storageUsage int64 @@ -105,102 +74,4 @@ func (c *Controller) setupDebugRoutes() { "table_sizes": tables, }) }) -} - -func createTestCpuData(database *db.Database) { - go func() { - defer func() { - database.Checkpoint() - database.Vacuum() - }() - - numberOfRows := 10000 - numWorkers := 10 - jobs := make(chan int, numberOfRows) - results := make(chan error, numberOfRows) - - // Start worker goroutines - for w := 0; w < numWorkers; w++ { - go func() { - for range jobs { - // Generate a random date within the last month - now := time.Now() - randomDate := now.AddDate(0, 0, -(rand.Int() % 31)) - - timestamp := fmt.Sprintf("%d", randomDate.UnixNano()/int64(time.Millisecond)) - percent := fmt.Sprintf("%.1f", rand.Float64()*100) - _, err := database.Exec(`INSERT INTO cpu_usage (time, percent) VALUES (?, ?)`, timestamp, percent) - results <- err - } - }() - } - - // Send jobs to workers - for i := 0; i < numberOfRows; i++ { - jobs <- i - } - close(jobs) - - // Collect results - for i := 0; i < numberOfRows; i++ { - if err := <-results; err != nil { - log.Printf("Error inserting test data: %v", err) - } - } - }() -} - -func createTestMemoryData(database *db.Database) { - go func() { - defer func() { - database.Checkpoint() - database.Vacuum() - }() - - numberOfRows := 10000 - numWorkers := 10 - jobs := make(chan int, numberOfRows) - results := make(chan error, numberOfRows) - - // Start worker goroutines - for w := 0; w < numWorkers; w++ { - go func() { - for range jobs { - // Generate a random date within the last month - now := time.Now() - randomDate := now.AddDate(0, 0, -(rand.Int() % 31)) - - timestamp := fmt.Sprintf("%d", randomDate.UnixNano()/int64(time.Millisecond)) - memory, err := mem.VirtualMemory() - usage := MemUsage{ - Time: timestamp, - Total: memory.Total, - Available: memory.Available, - Used: memory.Used, - UsedPercent: math.Round(memory.UsedPercent*100) / 100, - Free: memory.Free, - } - if err != nil { - log.Printf("Error getting memory usage: %v", err) - continue - } - _, err = database.Exec(`INSERT INTO memory_usage (time, total, available, used, usedPercent, free) VALUES (?, ?, ?, ?, ?, ?)`, usage.Time, usage.Total, usage.Available, usage.Used, usage.UsedPercent, usage.Free) - results <- err - } - }() - } - - // Send jobs to workers - for i := 0; i < numberOfRows; i++ { - jobs <- i - } - close(jobs) - - // Collect results - for i := 0; i < numberOfRows; i++ { - if err := <-results; err != nil { - log.Printf("Error inserting test data: %v", err) - } - } - }() -} +} \ No newline at end of file From 882d3eb542138e4c01e8d35335493f09549b42a3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 29 Oct 2024 12:45:26 +0100 Subject: [PATCH 13/19] Refactor debug routes in controller package and improve debug configuration --- cmd/cmd.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/cmd.go b/cmd/cmd.go index 2173dc0..7a039f1 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -72,6 +72,8 @@ func Execute() error { endpointFromEnv := os.Getenv("PUSH_ENDPOINT") if config.Debug { log.Printf("[%s] Debug is enabled.", time.Now().Format("2006-01-02 15:04:05")) + } + if gin.Mode() == gin.DebugMode { config.MetricsFile = "./db/metrics.sqlite" if endpointFromEnv == "" { From 5125d23d2313ca277c3bf4825e7edc541e089a01 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 29 Oct 2024 12:57:10 +0100 Subject: [PATCH 14/19] Refactor Docker image build and publish workflow --- .github/workflows/release-next.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/release-next.yaml b/.github/workflows/release-next.yaml index 30f89a0..1b6e9fc 100644 --- a/.github/workflows/release-next.yaml +++ b/.github/workflows/release-next.yaml @@ -109,6 +109,13 @@ jobs: --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next-amd64 \ --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next-amd64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next + - uses: sarisia/actions-status-discord@v1 if: always() with: From 7844d680817bc975f1833a7ac92efeb180b02b96 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 29 Oct 2024 13:06:35 +0100 Subject: [PATCH 15/19] Refactor Docker image build and publish workflow add /api/health --- Dockerfile | 24 ++++++++++-------------- pkg/api/controller/controller.go | 7 +++++++ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index a9be399..ee1ba20 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,20 @@ -FROM golang:1.23-bullseye AS deps +FROM golang:1.23-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download -FROM golang:1.23-bullseye AS build - -WORKDIR /app -COPY --from=deps /go/pkg/mod /go/pkg/mod COPY . . -RUN apt-get update && apt-get install -y gcc g++ -ENV CGO_ENABLED=1 \ - GOOS=linux \ - GOARCH=amd64 +RUN --mount=type=cache,target=/var/cache/apk \ + apk update && \ + apk add gcc g++ && \ + CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o /app/sentinel ./ -RUN go build -o /app/bin/sentinel ./ +FROM alpine:latest +RUN --mount=type=cache,target=/var/cache/apk \ + apk update && \ + apk add ca-certificates curl -FROM debian:bullseye-slim -RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* ENV GIN_MODE=release -COPY --from=build /app/ /app -COPY --from=build /app/bin/sentinel /app/sentinel +COPY --from=builder /app/sentinel /app/sentinel CMD ["/app/sentinel"] diff --git a/pkg/api/controller/controller.go b/pkg/api/controller/controller.go index ee7bcd6..8dfa881 100644 --- a/pkg/api/controller/controller.go +++ b/pkg/api/controller/controller.go @@ -27,11 +27,18 @@ func (c *Controller) GetEngine() *gin.Engine { } func (c *Controller) SetupRoutes() { + c.setupHealthRoutes() c.setupContainerRoutes() c.setupMemoryRoutes() c.setupCpuRoutes() } +func (c *Controller) setupHealthRoutes() { + c.ginE.GET("/health", func(c *gin.Context) { + c.String(200, "ok") + }) +} + // TODO: Implement c.setupPushRoutes() func (c *Controller) SetupDebugRoutes() { c.setupDebugRoutes() From a1ec5c025530c09c73351e41b19c726c1cfa3ff7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 29 Oct 2024 13:10:27 +0100 Subject: [PATCH 16/19] Refactor Docker image build and publish workflow for ARM64 architecture --- Dockerfile.arm64 | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index ac5f4f8..cf7f83c 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -1,24 +1,20 @@ -FROM golang:1.23-bullseye AS deps +FROM golang:1.23-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download -FROM golang:1.23-bullseye AS build - -WORKDIR /app -COPY --from=deps /go/pkg/mod /go/pkg/mod COPY . . -RUN apt-get update && apt-get install -y gcc g++ -ENV CGO_ENABLED=1 \ - GOOS=linux \ - GOARCH=arm64 +RUN --mount=type=cache,target=/var/cache/apk \ + apk update && \ + apk add gcc g++ && \ + CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -o /app/sentinel ./ -RUN go build -o /app/bin/sentinel ./ +FROM alpine:latest +RUN --mount=type=cache,target=/var/cache/apk \ + apk update && \ + apk add ca-certificates curl -FROM debian:bullseye-slim -RUN apt-get update && apt-get install -y ca-certificates curl && rm -rf /var/lib/apt/lists/* ENV GIN_MODE=release -COPY --from=build /app/ /app -COPY --from=build /app/bin/sentinel /app/sentinel -CMD ["/app/sentinel"] \ No newline at end of file +COPY --from=builder /app/sentinel /app/sentinel +CMD ["/app/sentinel"] From 278eab809e370aede2c4da058df9ef0fc21b0dd9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 29 Oct 2024 13:17:58 +0100 Subject: [PATCH 17/19] Refactor debug routes in controller package and improve debug configuration --- cmd/cmd.go | 14 ++++++++------ pkg/api/controller/controller.go | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 7a039f1..eeabea4 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -65,6 +65,14 @@ func Execute() error { } } + if gin.Mode() == gin.DebugMode { + config.MetricsFile = "./db/metrics.sqlite" + + if endpointFromEnv == "" { + config.Endpoint = "http://localhost:8000" + } + } + if config.Debug && gin.Mode() != gin.DebugMode { gin.SetMode(gin.DebugMode) } @@ -73,13 +81,7 @@ func Execute() error { if config.Debug { log.Printf("[%s] Debug is enabled.", time.Now().Format("2006-01-02 15:04:05")) } - if gin.Mode() == gin.DebugMode { - config.MetricsFile = "./db/metrics.sqlite" - if endpointFromEnv == "" { - config.Endpoint = "http://localhost:8000" - } - } tokenFromEnv := os.Getenv("TOKEN") if tokenFromEnv == "" { diff --git a/pkg/api/controller/controller.go b/pkg/api/controller/controller.go index 8dfa881..7fa6f15 100644 --- a/pkg/api/controller/controller.go +++ b/pkg/api/controller/controller.go @@ -34,7 +34,7 @@ func (c *Controller) SetupRoutes() { } func (c *Controller) setupHealthRoutes() { - c.ginE.GET("/health", func(c *gin.Context) { + c.ginE.GET("/api/health", func(c *gin.Context) { c.String(200, "ok") }) } From 569dd991e4fc71feccf7adb2898204469240afb5 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 29 Oct 2024 13:22:22 +0100 Subject: [PATCH 18/19] fix endpoint --- cmd/cmd.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index eeabea4..521817d 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -65,6 +65,7 @@ func Execute() error { } } + endpointFromEnv := os.Getenv("PUSH_ENDPOINT") if gin.Mode() == gin.DebugMode { config.MetricsFile = "./db/metrics.sqlite" @@ -75,10 +76,6 @@ func Execute() error { if config.Debug && gin.Mode() != gin.DebugMode { gin.SetMode(gin.DebugMode) - } - - endpointFromEnv := os.Getenv("PUSH_ENDPOINT") - if config.Debug { log.Printf("[%s] Debug is enabled.", time.Now().Format("2006-01-02 15:04:05")) } From 9cd273c74f0317612f949e465c4910a4cba9e108 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 29 Oct 2024 14:24:21 +0100 Subject: [PATCH 19/19] fix: prod release workflow --- .github/workflows/release.yaml | 102 ++++++++++++++++++++------------- 1 file changed, 61 insertions(+), 41 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b47cadb..f238b46 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,11 +1,12 @@ -name: Sentinel Release +name: Sentinel Release Development on: release: types: [released] env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/sentinel" jobs: @@ -16,27 +17,33 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) - id: meta - uses: docker/metadata-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=semver,pattern={{version}} - - name: Build and push - uses: docker/build-push-action@v5 + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: context: . file: Dockerfile platforms: linux/amd64 push: true - tags: ${{ steps.meta.outputs.tags }} + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}-amd64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}-amd64 + labels: | + coolify.managed=true aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -44,27 +51,33 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) - id: meta - uses: docker/metadata-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=semver,pattern={{version}}-aarch64 - - name: Build and push - uses: docker/build-push-action@v5 + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: context: . file: Dockerfile.arm64 platforms: linux/aarch64 push: true - tags: ${{ steps.meta.outputs.tags }}-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}-aarch64 + labels: | + coolify.managed=true merge-manifest: runs-on: ubuntu-latest permissions: @@ -72,26 +85,33 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) - id: meta - uses: docker/metadata-action@v5 + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=semver,pattern={{version}} - - name: Create & publish manifest + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}-amd64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }} + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ fromJSON(steps.meta.outputs.json).tags[0] }}-aarch64 --tag ${{ fromJSON(steps.meta.outputs.json).tags[0] }} - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ No newline at end of file + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}-amd64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }} \ No newline at end of file