diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml deleted file mode 100644 index d4a5437..0000000 --- a/.github/workflows/build-cli.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: 📋 Build CLI - -on: - pull_request: - types: [ synchronize, opened, reopened, ready_for_review ] - -concurrency: - group: build-cli-${{ github.head_ref }} - cancel-in-progress: true - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - os: ubuntu-latest - arch: amd64 - - os: ubuntu-24.04-arm - arch: arm64 - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Build and push - run: | - IMAGE=${{ env.REGISTRY }}/${{ github.repository }}:latest - docker build -t ${IMAGE}-${{matrix.arch}} -f tracing/cli/Dockerfile tracing diff --git a/.github/workflows/build-extension.yml b/.github/workflows/build-extension.yml deleted file mode 100644 index 9c267a1..0000000 --- a/.github/workflows/build-extension.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: 📋 Build Extension - -on: - pull_request: - types: [ synchronize, opened, reopened, ready_for_review ] - -concurrency: - group: build-extension-${{ github.head_ref }} - cancel-in-progress: true - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, ubuntu-24.04-arm] - php: ["8.2", "8.3", "8.4", "8.5"] - include: - - os: ubuntu-latest - arch: amd64 - - os: ubuntu-24.04-arm - arch: arm64 - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Build - run: | - IMAGE=${{ env.REGISTRY }}/${{ github.repository }}:latest - docker build \ - -t ${IMAGE}-php${{ matrix.php }}-${{ matrix.arch }} \ - --build-arg=PHP_VERSION=${{ matrix.php }} \ - extension\ diff --git a/.github/workflows/build-sidecar.yml b/.github/workflows/build-sidecar.yml deleted file mode 100644 index be5488a..0000000 --- a/.github/workflows/build-sidecar.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: 📋 Build Sidecar - -on: - pull_request: - types: [ synchronize, opened, reopened, ready_for_review ] - -concurrency: - group: build-sidecar-${{ github.head_ref }} - cancel-in-progress: true - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - os: ubuntu-latest - arch: amd64 - - os: ubuntu-24.04-arm - arch: arm64 - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Build and push - run: | - IMAGE=${{ env.REGISTRY }}/${{ github.repository }}:latest - docker build -t ${IMAGE}-${{matrix.arch}} -f tracing/sidecar/Dockerfile tracing diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..1db40f5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,36 @@ +name: 📋 Build + +on: + pull_request: + types: [ synchronize, opened, reopened, ready_for_review ] + +concurrency: + group: build-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: âŦ‡ī¸ Git clone the repository + uses: actions/checkout@v4 + + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: 🐋 Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: đŸ—ī¸ Build Docker image + uses: docker/bake-action@v6 + with: + source: . + push: false diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml deleted file mode 100644 index 45df478..0000000 --- a/.github/workflows/release-cli.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: "📋 Release CLI" - -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+[0-9A-Za-z]?' - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - include: - - os: ubuntu-latest - arch: amd64 - - os: ubuntu-24.04-arm - arch: arm64 - - permissions: - contents: read - packages: write - attestations: write - id-token: write - - steps: - - name: âŦ‡ī¸ Git clone the repository - uses: actions/checkout@v4 - - - name: 🔐 Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: đŸ“Ļ Build and Push - run: | - IMAGE=${{ env.REGISTRY }}/${{ github.repository }}:${{ github.ref_name }}-${{ matrix.arch }} - docker build --no-cache -t ${IMAGE} -f tracing/cli/Dockerfile tracing - docker push ${IMAGE} - - manifest: - name: Manifest - runs-on: ubuntu-latest - needs: build - - permissions: - contents: read - packages: write - attestations: write - id-token: write - - steps: - - name: 🔐 Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: â˜ī¸ Push - run: | - IMAGE=${{ env.REGISTRY }}/${{ github.repository }}:${{ github.ref_name }} - docker manifest create ${IMAGE} --amend ${IMAGE}-arm64 --amend ${IMAGE}-amd64 - docker manifest push ${IMAGE} diff --git a/.github/workflows/release-extension.yml b/.github/workflows/release-extension.yml deleted file mode 100644 index 01132d5..0000000 --- a/.github/workflows/release-extension.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: "📋 Release Extension" - -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+[0-9A-Za-z]?' - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }}-extension - -jobs: - build: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - include: - - os: ubuntu-latest - arch: amd64 - - os: ubuntu-24.04-arm - arch: arm64 - - permissions: - contents: read - packages: write - attestations: write - id-token: write - - steps: - - name: âŦ‡ī¸ Git clone the repository - uses: actions/checkout@v4 - - - name: 🔐 Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: đŸ“Ļ Build - run: | - IMAGE=${{ env.REGISTRY }}/${{ github.repository }}-extension:${{ github.ref_name }} - cd extension - docker build --no-cache --build-arg=PHP_VERSION=8.5 -t ${IMAGE}-php8.5-${{ matrix.arch }} . - docker build --no-cache --build-arg=PHP_VERSION=8.4 -t ${IMAGE}-php8.4-${{ matrix.arch }} . - docker build --no-cache --build-arg=PHP_VERSION=8.3 -t ${IMAGE}-php8.3-${{ matrix.arch }} . - docker build --no-cache --build-arg=PHP_VERSION=8.2 -t ${IMAGE}-php8.2-${{ matrix.arch }} . - - - name: â˜ī¸ Push - run: | - IMAGE=${{ env.REGISTRY }}/${{ github.repository }}-extension:${{ github.ref_name }} - docker push ${IMAGE}-php8.5-${{matrix.arch}} - docker push ${IMAGE}-php8.4-${{matrix.arch}} - docker push ${IMAGE}-php8.3-${{matrix.arch}} - docker push ${IMAGE}-php8.2-${{matrix.arch}} - manifest: - name: Manifest - runs-on: ubuntu-latest - needs: build - - permissions: - contents: read - packages: write - attestations: write - id-token: write - - steps: - - name: 🔐 Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: â˜ī¸ Push - run: | - IMAGE=${{ env.REGISTRY }}/${{ github.repository }}-extension:${{ github.ref_name }} - docker manifest create ${IMAGE}-php8.5 --amend ${IMAGE}-php8.5-arm64 --amend ${IMAGE}-php8.5-amd64 - docker manifest push ${IMAGE}-php8.5 - docker manifest create ${IMAGE}-php8.4 --amend ${IMAGE}-php8.4-arm64 --amend ${IMAGE}-php8.4-amd64 - docker manifest push ${IMAGE}-php8.4 - docker manifest create ${IMAGE}-php8.3 --amend ${IMAGE}-php8.3-arm64 --amend ${IMAGE}-php8.3-amd64 - docker manifest push ${IMAGE}-php8.3 - docker manifest create ${IMAGE}-php8.2 --amend ${IMAGE}-php8.2-arm64 --amend ${IMAGE}-php8.2-amd64 - docker manifest push ${IMAGE}-php8.2 diff --git a/.github/workflows/release-sidecar.yml b/.github/workflows/release-sidecar.yml deleted file mode 100644 index c3ac3e7..0000000 --- a/.github/workflows/release-sidecar.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: "📋 Release Sidecar" - -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+[0-9A-Za-z]?' - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - include: - - os: ubuntu-latest - arch: amd64 - - os: ubuntu-24.04-arm - arch: arm64 - - permissions: - contents: read - packages: write - attestations: write - id-token: write - - steps: - - name: âŦ‡ī¸ Git clone the repository - uses: actions/checkout@v4 - - - name: 🔐 Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: đŸ“Ļ Build and Push - run: | - IMAGE=${{ env.REGISTRY }}/${{ github.repository }}-sidecar:${{ github.ref_name }}-${{ matrix.arch }} - docker build --no-cache -t ${IMAGE} -f tracing/sidecar/Dockerfile tracing - docker push ${IMAGE} - - manifest: - name: Manifest - runs-on: ubuntu-latest - needs: build - - permissions: - contents: read - packages: write - attestations: write - id-token: write - - steps: - - name: 🔐 Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: â˜ī¸ Push - run: | - IMAGE=${{ env.REGISTRY }}/${{ github.repository }}-sidecar:${{ github.ref_name }} - docker manifest create ${IMAGE} --amend ${IMAGE}-arm64 --amend ${IMAGE}-amd64 - docker manifest push ${IMAGE} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..eb0bcd6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +name: "📋 Release CLI" + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+[0-9A-Za-z]?' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write + attestations: write + id-token: write + + steps: + - name: âŦ‡ī¸ Git clone the repository + uses: actions/checkout@v4 + + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + + - name: 🐋 Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: đŸ—ī¸ Build Docker image + uses: docker/bake-action@v6 + with: + source: . diff --git a/docker-bake.hcl b/docker-bake.hcl new file mode 100644 index 0000000..b6badc2 --- /dev/null +++ b/docker-bake.hcl @@ -0,0 +1,83 @@ +variable "PHP_VERSIONS" { + default = ["8.2", "8.3", "8.4", "8.5"] +} + +variable "VERSION" { + default = "latest" +} + +variable "PLATFORMS" { + default = ["linux/amd64", "linux/arm64"] +} + +# Common target: Everything inherits from this +target "_common" { + platforms = PLATFORMS + + secret = [ + # Used to ensure we do not hit rate limiting with Mise. + "id=github_token,env=GITHUB_TOKEN" + ] +} + +group "default" { + targets = [ + "extension", + "cli", + "sidecar", + ] +} + +target "extension" { + inherits = ["_common"] + + name = "app-${replace(PHP_VERSION, ".", "-")}" + + matrix = { + PHP_VERSION = PHP_VERSIONS + } + + context = "./extension" + + contexts = { + from_image = "docker-image://ghcr.io/skpr/php-cli:${PHP_VERSION}-v2-stable" + } + + args = { + PHP_VERSION = PHP_VERSION + } + + tags = [ + "ghcr.io/skpr/compass-extension:${VERSION}-${PHP_VERSION}" + ] +} + +target "cli" { + inherits = ["_common"] + + context = "./tracing" + dockerfile = "./cli/Dockerfile" + + contexts = { + from_image = "docker-image://docker.io/alpine:3.22" + } + + tags = [ + "ghcr.io/skpr/compass:${VERSION}" + ] +} + +target "sidecar" { + inherits = ["_common"] + + context = "./tracing" + dockerfile = "./sidecar/Dockerfile" + + contexts = { + from_image = "docker-image://docker.io/alpine:3.22" + } + + tags = [ + "ghcr.io/skpr/compass-sidecar:${VERSION}" + ] +} diff --git a/extension/Dockerfile b/extension/Dockerfile index 7c1b106..dc9203b 100644 --- a/extension/Dockerfile +++ b/extension/Dockerfile @@ -1,36 +1,36 @@ -ARG PHP_VERSION=8.3 -FROM alpine:3.21 AS build -ARG PHP_VERSION=8.3 +FROM from_image AS build -USER root + ARG PHP_VERSION=8.3 -RUN apk add --no-cache curl && \ - curl -sSL https://packages.skpr.io/php-alpine/skpr.rsa.pub -o /etc/apk/keys/skpr.rsa.pub && \ - echo "https://packages.skpr.io/php-alpine/3.21/php${PHP_VERSION}" >> /etc/apk/repositories + USER root -RUN apk add alpine-sdk clang clang-dev php${PHP_VERSION} php${PHP_VERSION}-dev + RUN apk add --no-cache curl && \ + curl -sSL https://packages.skpr.io/php-alpine/skpr.rsa.pub -o /etc/apk/keys/skpr.rsa.pub && \ + echo "https://packages.skpr.io/php-alpine/3.21/php${PHP_VERSION}" >> /etc/apk/repositories -ENV MISE_DATA_DIR="/mise" -ENV MISE_CONFIG_DIR="/mise" -ENV MISE_CACHE_DIR="/mise/cache" -ENV MISE_INSTALL_PATH="/usr/local/bin/mise" -ENV PATH="/mise/shims:$PATH" + RUN apk add alpine-sdk clang clang-dev php${PHP_VERSION} php${PHP_VERSION}-dev -RUN curl https://mise.run | sh + ENV MISE_DATA_DIR="/mise" + ENV MISE_CONFIG_DIR="/mise" + ENV MISE_CACHE_DIR="/mise/cache" + ENV MISE_INSTALL_PATH="/usr/local/bin/mise" + ENV PATH="/mise/shims:$PATH" -ENV RUSTFLAGS="-C target-feature=-crt-static" -ENV RUST_BACKTRACE=full + RUN curl https://mise.run | sh -WORKDIR /data + ENV RUSTFLAGS="-C target-feature=-crt-static" + ENV RUST_BACKTRACE=full -ADD --chown=skpr:skpr . /data -RUN mise trust . + WORKDIR /data -RUN mise run lint -RUN mise run build -RUN mise run validate + ADD --chown=skpr:skpr . /data + RUN mise trust . -FROM scratch + RUN mise run lint + RUN mise run build + RUN mise run validate -COPY compass.ini /etc/php/conf.d/00_compass.ini -COPY --from=build /data/target/release/libcompass_extension.so /usr/lib/php/modules/compass.so +FROM from_image + + COPY compass.ini /etc/php/conf.d/00_compass.ini + COPY --from=build /data/target/release/libcompass_extension.so /usr/lib/php/modules/compass.so diff --git a/tracing/.devcontainer/Dockerfile b/tracing/.devcontainer/Dockerfile index d32e43c..a4d5605 100644 --- a/tracing/.devcontainer/Dockerfile +++ b/tracing/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21 +FROM alpine:3.22 USER root diff --git a/tracing/cli/Dockerfile b/tracing/cli/Dockerfile index 3b420b7..15436cc 100644 --- a/tracing/cli/Dockerfile +++ b/tracing/cli/Dockerfile @@ -1,41 +1,40 @@ -FROM alpine:3.21 AS build - -USER root - -RUN apk add alpine-sdk \ - bash \ - bpftool \ - clang \ - clang-dev \ - curl \ - git \ - libbpf-dev \ - linux-headers \ - llvm - -ENV MISE_DATA_DIR="/mise" -ENV MISE_CONFIG_DIR="/mise" -ENV MISE_CACHE_DIR="/mise/cache" -ENV MISE_INSTALL_PATH="/usr/local/bin/mise" -ENV PATH="/mise/shims:$PATH" - -RUN curl https://mise.run | sh - -# Make libclang easy to find for bindgen -ENV LIBCLANG_PATH=/usr/lib/llvm19/lib - -ENV GOFLAGS=-buildvcs=false - -WORKDIR /data -ADD --chown=skpr:skpr . /data - -# Check and build. -RUN mise trust . -RUN mise run lint -RUN mise run test -RUN mise run build:cli - -FROM alpine:3.21 -RUN apk add bash -COPY --from=build /data/_output/compass /usr/local/bin/compass -CMD ["compass"] +FROM from_image AS build + + USER root + + RUN apk add alpine-sdk \ + bash \ + bpftool \ + clang \ + clang-dev \ + curl \ + git \ + libbpf-dev \ + linux-headers \ + llvm + + ENV MISE_DATA_DIR="/mise" + ENV MISE_CONFIG_DIR="/mise" + ENV MISE_CACHE_DIR="/mise/cache" + ENV MISE_INSTALL_PATH="/usr/local/bin/mise" + ENV PATH="/mise/shims:$PATH" + + RUN curl https://mise.run | sh + + # Make libclang easy to find for bindgen + ENV LIBCLANG_PATH=/usr/lib/llvm19/lib + + ENV GOFLAGS=-buildvcs=false + + WORKDIR /data + ADD --chown=skpr:skpr . /data + + # Check and build. + RUN mise trust . + RUN mise run build:cli + +FROM from_image + + RUN apk add bash + COPY --from=build /data/_output/compass /usr/local/bin/compass + CMD ["compass"] diff --git a/tracing/cli/app/component/span/span.go b/tracing/cli/app/component/span/span.go index 2626ddc..dc88484 100644 --- a/tracing/cli/app/component/span/span.go +++ b/tracing/cli/app/component/span/span.go @@ -55,7 +55,6 @@ func (c *Component) Render(span Span) string { fill = tidyFill(pre+fill+post, int(c.Blocks), fill) - // return fmt.Sprintf("īŊœ%s%s%sīŊœ %dms", strings.Repeat(" ", pre), strings.Repeat(getBlockWithColor(FractionDuration(span.Duration, c.Duration)), fill), strings.Repeat(" ", post), span.Duration.Milliseconds()) return fmt.Sprintf("īŊœ%s%s%sīŊœ %dms", strings.Repeat(" ", pre), colorForFill(FractionDuration(span.Duration, c.Duration), strings.Repeat(Block, fill)), strings.Repeat(" ", post), span.Duration.Milliseconds()) } diff --git a/tracing/cli/app/events/trace.go b/tracing/cli/app/events/trace.go index 8c0dfd2..8833cdc 100644 --- a/tracing/cli/app/events/trace.go +++ b/tracing/cli/app/events/trace.go @@ -4,7 +4,6 @@ import ( "fmt" "time" - skprtime "github.com/skpr/compass/tracing/cli/app/time" "github.com/skpr/compass/tracing/trace" ) @@ -16,7 +15,7 @@ type Trace struct { // Title of the trace. func (t Trace) Title() string { - return fmt.Sprintf("%dms %s %s", skprtime.NanosecondsToMilliseconds(t.Metadata.ExecutionTime()), t.Metadata.Method, t.Metadata.URI) + return fmt.Sprintf("%dms %s %s", t.Metadata.ExecutionTime().Milliseconds(), t.Metadata.Method, t.Metadata.URI) } // Description of the trace. diff --git a/tracing/cli/app/metadata.go b/tracing/cli/app/metadata.go index 7f2d071..e27815f 100644 --- a/tracing/cli/app/metadata.go +++ b/tracing/cli/app/metadata.go @@ -8,7 +8,6 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/skpr/compass/tracing/cli/app/color" - skprtime "github.com/skpr/compass/tracing/cli/app/time" ) func (m *Model) metadataInit() { @@ -72,7 +71,7 @@ func (m *Model) metadataSetRows() { rows := []table.Row{ {bold.Render("URI"), m.Current.Metadata.URI}, {bold.Render("Method"), m.Current.Metadata.Method}, - {bold.Render("Execution Time"), fmt.Sprintf("%dms", skprtime.NanosecondsToMilliseconds(m.Current.Metadata.ExecutionTime()))}, + {bold.Render("Execution Time"), fmt.Sprintf("%dms", m.Current.Metadata.ExecutionTime().Milliseconds())}, {bold.Render("Function Calls"), fmt.Sprintf("%d", len(m.Current.FunctionCalls))}, {bold.Render("Request ID"), m.Current.Metadata.RequestID}, {bold.Render("Ingestion Time"), m.Current.IngestionTime.Format(time.RFC822)}, diff --git a/tracing/cli/app/spans.go b/tracing/cli/app/spans.go index 4c11fc4..0f39282 100644 --- a/tracing/cli/app/spans.go +++ b/tracing/cli/app/spans.go @@ -1,14 +1,12 @@ package app import ( - "time" - "github.com/charmbracelet/bubbles/table" "github.com/charmbracelet/lipgloss" "github.com/skpr/compass/tracing/cli/app/color" "github.com/skpr/compass/tracing/cli/app/component/span" - "github.com/skpr/compass/tracing/trace/segmented" + "github.com/skpr/compass/tracing/trace/aggregated" ) // SpanLength is how long a span component should be. @@ -59,9 +57,9 @@ func (m *Model) spansSetRows() { return } - trace := segmented.Unmarshal(m.Current.Trace, SpanLength) + trace := aggregated.Unmarshal(m.Current.Trace) - sc := span.New(time.Duration(trace.Metadata.ExecutionTime())*time.Nanosecond, float64(SpanLength)) + sc := span.New(trace.Metadata.ExecutionTime(), float64(SpanLength)) var rows []table.Row @@ -69,8 +67,8 @@ func (m *Model) spansSetRows() { rows = append(rows, []string{ s.Name, sc.Render(span.Span{ - Start: time.Duration(s.Start) * time.Nanosecond, - Duration: time.Duration(s.Length) * time.Nanosecond, + Start: s.Start, + Duration: s.Elapsed, }), }) } diff --git a/tracing/cli/app/time/time.go b/tracing/cli/app/time/time.go deleted file mode 100644 index 0ea6349..0000000 --- a/tracing/cli/app/time/time.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package time for utility time conversions. -package time - -import "time" - -// NanosecondsToMilliseconds for time conversion. -func NanosecondsToMilliseconds(ns int64) int { - return int(float64(ns) / float64(time.Millisecond)) -} diff --git a/tracing/collector/collector.go b/tracing/collector/collector.go index df07509..b73e4dd 100644 --- a/tracing/collector/collector.go +++ b/tracing/collector/collector.go @@ -16,6 +16,7 @@ import ( "golang.org/x/sync/errgroup" "github.com/skpr/compass/tracing/collector/sink" + skprtime "github.com/skpr/compass/tracing/collector/time" "github.com/skpr/compass/tracing/collector/usdt" ) @@ -92,7 +93,7 @@ func Run(ctx context.Context, logger Logger, plugin sink.Interface, options RunO manager, err := NewManager(logger, plugin, Options{ Expire: time.Minute, - }) + }, skprtime.New()) if err != nil { return fmt.Errorf("unable to initialize event manager: %w", err) } @@ -130,7 +131,7 @@ func Run(ctx context.Context, logger Logger, plugin sink.Interface, options RunO continue } - if err := manager.Handle(event); err != nil { + if err := manager.Handle(ctx, event); err != nil { logger.Error("failed to handle event", slog.Any("err", err)) continue } diff --git a/tracing/collector/manager.go b/tracing/collector/manager.go index b1ccc21..5bd314e 100644 --- a/tracing/collector/manager.go +++ b/tracing/collector/manager.go @@ -9,6 +9,7 @@ import ( "golang.org/x/sys/unix" "github.com/skpr/compass/tracing/collector/sink" + skprtime "github.com/skpr/compass/tracing/collector/time" "github.com/skpr/compass/tracing/trace" ) @@ -25,6 +26,8 @@ const ( type Manager struct { // Logger for debugging. logger Logger + // Clock for testing. + clock skprtime.Interface // Consider an interface for the storage. storage *cache.Cache // Plugin for sending completed requests to. @@ -39,9 +42,10 @@ type Options struct { } // NewManager creates a new manager. -func NewManager(logger Logger, plugin sink.Interface, options Options) (*Manager, error) { +func NewManager(logger Logger, plugin sink.Interface, options Options, clock skprtime.Interface) (*Manager, error) { client := &Manager{ logger: logger, + clock: clock, storage: cache.New(options.Expire, options.Expire), plugin: plugin, options: options, @@ -51,7 +55,7 @@ func NewManager(logger Logger, plugin sink.Interface, options Options) (*Manager } // Handle the event and process it. -func (c *Manager) Handle(event bpfEvent) error { +func (c *Manager) Handle(ctx context.Context, event bpfEvent) error { var ( requestID = unix.ByteSliceToString(event.RequestId[:]) ) @@ -75,7 +79,7 @@ func (c *Manager) Handle(event bpfEvent) error { return fmt.Errorf("failed to process function: %w", err) } case EventRequestShutdown: - if err := c.handleRequestShutdown(requestID, event); err != nil { + if err := c.handleRequestShutdown(ctx, requestID, event); err != nil { return fmt.Errorf("failed to process request shutdown: %w", err) } } @@ -89,10 +93,11 @@ func (c *Manager) handleRequestInit(requestID, uri, method string, event bpfEven t := trace.Trace{ Metadata: trace.Metadata{ - RequestID: requestID, - URI: uri, - Method: method, - StartTime: int64(event.Timestamp), + RequestID: requestID, + URI: uri, + Method: method, + StartTime: c.clock.Now(), + MonotonicStart: time.Duration(event.Timestamp), }, } @@ -107,14 +112,17 @@ func (c *Manager) handleFunction(requestID string, event bpfEvent) error { Name: unix.ByteSliceToString(event.FunctionName[:]), // The start time is the event time minus how long it look to execute. // The event is triggerd after a the function is called and we have collected the elapsed time. - StartTime: int64(event.Timestamp - event.Elapsed), - Elapsed: int64(event.Elapsed), + MonotonicEnd: time.Duration(event.Timestamp), + Elapsed: time.Duration(event.Elapsed), } + // We can calculate the start based off the end and the elapsed. + function.MonotonicStart = function.MonotonicEnd - function.Elapsed + c.logger.Debug("function event has been called", "request_id", requestID, "function_name", function.Name, - "start_time", function.StartTime, + "monotonic_end", function.MonotonicEnd, "elapsed", function.Elapsed, ) @@ -133,7 +141,7 @@ func (c *Manager) handleFunction(requestID string, event bpfEvent) error { } // Process the request shutdown event and send the profile to the plugin. -func (c *Manager) handleRequestShutdown(requestID string, event bpfEvent) error { +func (c *Manager) handleRequestShutdown(ctx context.Context, requestID string, event bpfEvent) error { c.logger.Debug("request shutdown event has been called", "request_id", requestID) x, found := c.storage.Get(requestID) @@ -143,7 +151,7 @@ func (c *Manager) handleRequestShutdown(requestID string, event bpfEvent) error t := x.(trace.Trace) - t.Metadata.EndTime = int64(event.Timestamp) + t.Metadata.MonotonicEnd = time.Duration(event.Timestamp) // Cleanup this request after we have processed it. defer c.storage.Delete(requestID) @@ -154,7 +162,7 @@ func (c *Manager) handleRequestShutdown(requestID string, event bpfEvent) error c.logger.Debug("request event has associated functions", "count", len(t.FunctionCalls)) - err := c.plugin.ProcessTrace(context.TODO(), t) + err := c.plugin.ProcessTrace(ctx, t) if err != nil { return fmt.Errorf("failed to send profile data to plugin: %w", err) } diff --git a/tracing/collector/manager_test.go b/tracing/collector/manager_test.go index 4d37ba9..0e2e314 100644 --- a/tracing/collector/manager_test.go +++ b/tracing/collector/manager_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" + skprtime "github.com/skpr/compass/tracing/collector/time" "github.com/skpr/compass/tracing/trace" ) @@ -36,9 +37,11 @@ func TestHandleRequestShutdown(t *testing.T) { // Sink for reviewing the compiled profile. sink := &TestSync{} + now := skprtime.NewMock(time.Now()) + manager, err := NewManager(logger, sink, Options{ Expire: time.Second, - }) + }, now) assert.NoError(t, err) toUint8 := func(val string) [101]uint8 { @@ -59,40 +62,40 @@ func TestHandleRequestShutdown(t *testing.T) { events := []bpfEvent{ { - Type: 1, + Type: EventRequestInit, RequestId: toUint8(requestID), - Timestamp: uint64(3000000), + Timestamp: uint64(100_000_000), }, { - Type: 0, + Type: EventFunction, RequestId: toUint8(requestID), FunctionName: toUint8("Foo::bar"), - Timestamp: uint64(15000000), - Elapsed: uint64(12000000), + Timestamp: uint64(270_000_000), + Elapsed: uint64(120_000_000), }, { - Type: 0, + Type: EventFunction, RequestId: toUint8(requestID), FunctionName: toUint8("Skpr::rocks"), - Timestamp: uint64(13000000), - Elapsed: uint64(8000000), + Timestamp: uint64(350_000_000), + Elapsed: uint64(80_000_000), }, { - Type: 0, + Type: EventFunction, RequestId: toUint8(requestID), FunctionName: toUint8("Baz::boo"), - Timestamp: uint64(10000000), - Elapsed: uint64(4000000), + Timestamp: uint64(390_000_000), + Elapsed: uint64(40_000_000), }, { - Type: 2, + Type: EventRequestShutdown, RequestId: toUint8(requestID), - Timestamp: uint64(15000000), + Timestamp: uint64(390_000_000), }, } for _, event := range events { - err := manager.Handle(event) + err := manager.Handle(context.TODO(), event) assert.NoError(t, err) } @@ -100,25 +103,29 @@ func TestHandleRequestShutdown(t *testing.T) { assert.Equal(t, []trace.Trace{ { Metadata: trace.Metadata{ - RequestID: "123456789", - StartTime: 3000000, - EndTime: 15000000, + RequestID: "123456789", + StartTime: now.Now(), + MonotonicStart: 100_000_000, + MonotonicEnd: 390_000_000, }, FunctionCalls: []trace.FunctionCall{ { - Name: "Foo::bar", - StartTime: 3000000, - Elapsed: 12000000, + Name: "Foo::bar", + MonotonicStart: 150_000_000, + MonotonicEnd: 270_000_000, + Elapsed: 120_000_000, }, { - Name: "Skpr::rocks", - StartTime: 5000000, - Elapsed: 8000000, + Name: "Skpr::rocks", + MonotonicStart: 270_000_000, + MonotonicEnd: 350_000_000, + Elapsed: 80_000_000, }, { - Name: "Baz::boo", - StartTime: 6000000, - Elapsed: 4000000, + Name: "Baz::boo", + MonotonicStart: 350_000_000, + MonotonicEnd: 390_000_000, + Elapsed: 40_000_000, }, }, }, diff --git a/tracing/collector/program/arm64.bpf.c b/tracing/collector/program/arm64.bpf.c index c9fcf96..8725373 100644 --- a/tracing/collector/program/arm64.bpf.c +++ b/tracing/collector/program/arm64.bpf.c @@ -8,7 +8,7 @@ #include #include -char __license[] SEC("license") = "Dual MIT/GPL"; +char LICENSE[] SEC("license") = "GPL"; enum event_type : __u8 { EVENT_TYPE_FUNCTION = 0, diff --git a/tracing/collector/time/client.go b/tracing/collector/time/client.go new file mode 100644 index 0000000..5d5a7ce --- /dev/null +++ b/tracing/collector/time/client.go @@ -0,0 +1,16 @@ +package time + +import "time" + +// Client for interacting with time. +type Client struct{} + +// New client for interacting with time. +func New() Client { + return Client{} +} + +// Now returns the current time. +func (client Client) Now() time.Time { + return time.Now() +} diff --git a/tracing/collector/time/mock.go b/tracing/collector/time/mock.go new file mode 100644 index 0000000..4504b71 --- /dev/null +++ b/tracing/collector/time/mock.go @@ -0,0 +1,20 @@ +package time + +import "time" + +// Mock for testing. +type Mock struct { + now time.Time +} + +// NewMock for testing. +func NewMock(now time.Time) *Mock { + return &Mock{ + now: now, + } +} + +// Now returns the mock current time. +func (m *Mock) Now() time.Time { + return m.now +} diff --git a/tracing/collector/time/time.go b/tracing/collector/time/time.go new file mode 100644 index 0000000..d9ded68 --- /dev/null +++ b/tracing/collector/time/time.go @@ -0,0 +1,8 @@ +package time + +import "time" + +// Interface for interacting with time. +type Interface interface { + Now() time.Time +} diff --git a/tracing/sidecar/Dockerfile b/tracing/sidecar/Dockerfile index 7e27637..a9892b1 100644 --- a/tracing/sidecar/Dockerfile +++ b/tracing/sidecar/Dockerfile @@ -1,42 +1,40 @@ -FROM alpine:3.21 AS build +FROM from_image AS build -USER root + USER root -RUN apk add alpine-sdk \ - bash \ - bpftool \ - clang \ - clang-dev \ - curl \ - git \ - libbpf-dev \ - linux-headers \ - llvm + RUN apk add alpine-sdk \ + bash \ + bpftool \ + clang \ + clang-dev \ + curl \ + git \ + libbpf-dev \ + linux-headers \ + llvm -ENV MISE_DATA_DIR="/mise" -ENV MISE_CONFIG_DIR="/mise" -ENV MISE_CACHE_DIR="/mise/cache" -ENV MISE_INSTALL_PATH="/usr/local/bin/mise" -ENV PATH="/mise/shims:$PATH" + ENV MISE_DATA_DIR="/mise" + ENV MISE_CONFIG_DIR="/mise" + ENV MISE_CACHE_DIR="/mise/cache" + ENV MISE_INSTALL_PATH="/usr/local/bin/mise" + ENV PATH="/mise/shims:$PATH" -RUN curl https://mise.run | sh + RUN curl https://mise.run | sh -# Make libclang easy to find for bindgen -ENV LIBCLANG_PATH=/usr/lib/llvm19/lib + # Make libclang easy to find for bindgen + ENV LIBCLANG_PATH=/usr/lib/llvm19/lib -ENV GOFLAGS=-buildvcs=false + ENV GOFLAGS=-buildvcs=false -WORKDIR /data -ADD --chown=skpr:skpr . /data + WORKDIR /data + ADD --chown=skpr:skpr . /data -# Check and build. -RUN mise trust . -RUN mise run lint -RUN mise run test -RUN mise run build:sidecar + # Check and build. + RUN mise trust . + RUN mise run build:sidecar FROM scratch -COPY --from=build /data/_output/compass-sidecar /usr/local/bin/compass-sidecar -ENV COMPASS_SIDECAR_PROCESS_NAME=php-fpm -CMD ["compass-sidecar"] + COPY --from=build /data/_output/compass-sidecar /usr/local/bin/compass-sidecar + ENV COMPASS_SIDECAR_PROCESS_NAME=php-fpm + CMD ["compass-sidecar"] diff --git a/tracing/trace/aggregated/trace.go b/tracing/trace/aggregated/trace.go new file mode 100644 index 0000000..e80e319 --- /dev/null +++ b/tracing/trace/aggregated/trace.go @@ -0,0 +1,80 @@ +// Package aggregated for storing aggregated tracing data. +package aggregated + +import ( + "fmt" + "sort" + "time" + + "github.com/skpr/compass/tracing/trace" +) + +// Unmarshal a full trace into a segmented trace. +func Unmarshal(fullTrace trace.Trace) Trace { + // We are using 5% buffer on the before/after function call for our rollup. + // We can make this configurable in a future release. + segmentDuration := time.Duration(float64(fullTrace.Metadata.ExecutionTime()) * 0.05) + + spans := make(map[string]Span) + + for _, call := range fullTrace.FunctionCalls { + var ( + start = fullTrace.Metadata.ExecutionTime() / segmentDuration + elapsed = call.Elapsed / segmentDuration + ) + + key := fmt.Sprintf("%s-%d-%d", call.Name, start.Nanoseconds(), elapsed.Nanoseconds()) + + span := Span{ + Name: call.Name, + Start: call.MonotonicStart - fullTrace.Metadata.MonotonicStart, + Elapsed: call.Elapsed, + Calls: 1, + } + + if val, ok := spans[key]; ok { + span.Calls++ + + if span.Start < val.Start { + span.Start = val.Start + } + + if span.End > val.End { + span.End = val.End + } + + if span.Elapsed > val.Elapsed { + span.Elapsed = val.Elapsed + } + + spans[key] = span + continue + } + + spans[key] = span + } + + aggregatedTrace := Trace{ + Metadata: fullTrace.Metadata, + TotalFunctionCalls: len(fullTrace.FunctionCalls), + } + + for _, span := range spans { + aggregatedTrace.Spans = append(aggregatedTrace.Spans, span) + } + + // We also need to sort these now that all the spans have gone through a map which does not have ordering. + sort.Slice(aggregatedTrace.Spans, func(i, j int) bool { + if aggregatedTrace.Spans[i].Start != aggregatedTrace.Spans[j].Start { + return aggregatedTrace.Spans[i].Start < aggregatedTrace.Spans[j].Start + } + + if aggregatedTrace.Spans[i].Name != aggregatedTrace.Spans[j].Name { + return aggregatedTrace.Spans[i].Name < aggregatedTrace.Spans[j].Name + } + + return aggregatedTrace.Spans[i].Elapsed < aggregatedTrace.Spans[j].Elapsed + }) + + return aggregatedTrace +} diff --git a/tracing/trace/aggregated/trace_test.go b/tracing/trace/aggregated/trace_test.go new file mode 100644 index 0000000..412b126 --- /dev/null +++ b/tracing/trace/aggregated/trace_test.go @@ -0,0 +1,43 @@ +package aggregated + +import ( + "testing" + "time" + + "github.com/skpr/compass/tracing/trace" +) + +func TestUnmarshal_AggregatesDuplicateCalls(t *testing.T) { + md := trace.Metadata{ + RequestID: "xxxxxxxxxxxxxx", + URI: "/test", + Method: "GET", + StartTime: time.Unix(0, 0), + MonotonicStart: 0 * time.Millisecond, + MonotonicEnd: 1000 * time.Millisecond, + } + + full := trace.Trace{ + Metadata: md, + FunctionCalls: []trace.FunctionCall{ + { + Name: "foo", + MonotonicStart: 100 * time.Millisecond, + MonotonicEnd: 300 * time.Millisecond, + Elapsed: 200 * time.Millisecond, + }, + { + Name: "foo", + MonotonicStart: 120 * time.Millisecond, + MonotonicEnd: 220 * time.Millisecond, + Elapsed: 100 * time.Millisecond, + }, + }, + } + + got := Unmarshal(full) + + if got.TotalFunctionCalls == 1 { + t.Fatalf("TotalFunctionCalls: got %d, want %d", got.TotalFunctionCalls, 2) + } +} diff --git a/tracing/trace/segmented/types.go b/tracing/trace/aggregated/types.go similarity index 54% rename from tracing/trace/segmented/types.go rename to tracing/trace/aggregated/types.go index 6bc1428..c179d38 100644 --- a/tracing/trace/segmented/types.go +++ b/tracing/trace/aggregated/types.go @@ -1,7 +1,8 @@ -package segmented +package aggregated import ( "fmt" + "time" "github.com/skpr/compass/tracing/trace" ) @@ -10,8 +11,6 @@ import ( type Trace struct { // Metadata associated with this trace. Metadata trace.Metadata `json:"metadata"` - // Total number of segments in this trace. - Segments int64 `json:"segments"` // TotalFunctionCalls that occurred during this trace. TotalFunctionCalls int `json:"totalFunctionCalls"` // Spans that are included in trace. @@ -22,20 +21,20 @@ type Trace struct { type Span struct { // Name of the function. Name string `json:"name"` - // The original start time of the function called in the span. - StartTime int64 `json:"startTime"` - // Which segment this function started. - Start int64 `json:"start"` - // How many segments this function call spans. - Length int64 `json:"length"` - // TotalFunctionCalls that were called during this span. - TotalFunctionCalls int `json:"calls"` + // When this function started in the trace. + Start time.Duration `json:"start"` + // When this function ended in the trace. + End time.Duration `json:"end"` + // How long this function was called for. + Elapsed time.Duration `json:"elapsed"` + // Number of times this function was called. + Calls int `json:"call"` } // GetName of the span and include the amount when more than one call. func (s Span) GetName() string { - if s.TotalFunctionCalls > 1 { - return fmt.Sprintf("%s (%d)", s.Name, s.TotalFunctionCalls) + if s.Calls > 1 { + return fmt.Sprintf("%s (%d)", s.Name, s.Calls) } return s.Name diff --git a/tracing/trace/count/trace.go b/tracing/trace/count/trace.go index af958d7..08b5c89 100644 --- a/tracing/trace/count/trace.go +++ b/tracing/trace/count/trace.go @@ -5,14 +5,14 @@ import ( "sort" "github.com/skpr/compass/tracing/trace" - "github.com/skpr/compass/tracing/trace/segmented" + "github.com/skpr/compass/tracing/trace/aggregated" ) // Unmarshal a full trace into a counted trace. func Unmarshal(fullTrace trace.Trace) Trace { // We first unmarshal it into segments so we can determine the percentage. // 100 allows us to compute a percentage. - segementedTrace := segmented.Unmarshal(fullTrace, 100) + segementedTrace := aggregated.Unmarshal(fullTrace) functions := make(map[string]Function) @@ -20,7 +20,7 @@ func Unmarshal(fullTrace trace.Trace) Trace { function := Function{ Name: span.Name, Calls: 1, - Percentage: span.Length, + Percentage: int64((float64(span.Elapsed) / float64(fullTrace.Metadata.ExecutionTime())) * 100), } if val, ok := functions[function.Name]; ok { diff --git a/tracing/trace/segmented/trace.go b/tracing/trace/segmented/trace.go deleted file mode 100644 index da1d5ef..0000000 --- a/tracing/trace/segmented/trace.go +++ /dev/null @@ -1,75 +0,0 @@ -// Package segmented for storing tracing data in segments. -package segmented - -import ( - "fmt" - "sort" - - "github.com/skpr/compass/tracing/trace" -) - -// Unmarshal a full trace into a segmented trace. -func Unmarshal(fullTrace trace.Trace, segments int64) Trace { - segmentLength := (fullTrace.Metadata.EndTime - fullTrace.Metadata.StartTime) / segments - - spans := make(map[string]Span) - - for _, call := range fullTrace.FunctionCalls { - span := Span{ - Name: call.Name, - StartTime: call.StartTime, - Start: call.StartTime - fullTrace.Metadata.StartTime, - Length: call.Elapsed, - TotalFunctionCalls: 1, - } - - var ( - keyStart = (call.StartTime - fullTrace.Metadata.StartTime) / segmentLength - keyLength = span.Length / segmentLength - ) - - if keyLength == 0 { - keyLength = 1 - } - - key := fmt.Sprintf("%s-%d-%d", span.Name, keyStart, keyLength) - - if val, ok := spans[key]; ok { - span.TotalFunctionCalls = val.TotalFunctionCalls + 1 - - if span.StartTime > val.StartTime { - span.StartTime = val.StartTime - } - - spans[key] = span - continue - } - - spans[key] = span - } - - segmentedTrace := Trace{ - Metadata: fullTrace.Metadata, - Segments: segments, - TotalFunctionCalls: len(fullTrace.FunctionCalls), - } - - for _, span := range spans { - segmentedTrace.Spans = append(segmentedTrace.Spans, span) - } - - // We also need to sort these now that all the spans have gone through a map which does not have ordering. - sort.Slice(segmentedTrace.Spans, func(i, j int) bool { - if segmentedTrace.Spans[i].StartTime != segmentedTrace.Spans[j].StartTime { - return segmentedTrace.Spans[i].StartTime < segmentedTrace.Spans[j].StartTime - } - - if segmentedTrace.Spans[i].Name != segmentedTrace.Spans[j].Name { - return segmentedTrace.Spans[i].Name < segmentedTrace.Spans[j].Name - } - - return segmentedTrace.Spans[i].Length < segmentedTrace.Spans[j].Length - }) - - return segmentedTrace -} diff --git a/tracing/trace/trace.go b/tracing/trace/trace.go index b1e0ed7..0754915 100644 --- a/tracing/trace/trace.go +++ b/tracing/trace/trace.go @@ -1,19 +1,7 @@ // Package trace implements complete tracing data. package trace -// Metadata associated with this trace. -type Metadata struct { - RequestID string `json:"requestID"` - URI string `json:"uri"` - Method string `json:"method"` - StartTime int64 `json:"startTime"` - EndTime int64 `json:"endTime"` -} - -// ExecutionTime of the trace. -func (m Metadata) ExecutionTime() int64 { - return m.EndTime - m.StartTime -} +import "time" // Trace data collected for a request. type Trace struct { @@ -21,9 +9,25 @@ type Trace struct { FunctionCalls []FunctionCall `json:"functionCalls"` } +// Metadata associated with this trace. +type Metadata struct { + RequestID string `json:"requestID"` + URI string `json:"uri"` + Method string `json:"method"` + StartTime time.Time `json:"startTime"` + MonotonicStart time.Duration `json:"monotonicStart"` + MonotonicEnd time.Duration `json:"monotonicEnd"` +} + +// ExecutionTime of the trace. +func (m Metadata) ExecutionTime() time.Duration { + return m.MonotonicEnd - m.MonotonicStart +} + // FunctionCall provides information about the function call. type FunctionCall struct { - Name string `json:"name"` - StartTime int64 `json:"startTime"` - Elapsed int64 `json:"elapsed"` + Name string `json:"name"` + MonotonicStart time.Duration `json:"monotonicStart"` + MonotonicEnd time.Duration `json:"monotonicEnd"` + Elapsed time.Duration `json:"elapsed"` }