feat: introduce ledger and version (#87)

#### Features

- Introduce ledger for gathering analytics abt usage of foundryctl
- Adds version command

Related: https://github.com/SigNoz/platform-pod/issues/1910
This commit is contained in:
Nageshbansal
2026-03-30 16:07:37 +05:30
committed by GitHub
parent a5e02386ec
commit 0e4e752a19
19 changed files with 527 additions and 29 deletions
+36 -1
View File
@@ -8,15 +8,50 @@ permissions:
contents: write
id-token: write
env:
MAKE: make --no-print-directory --makefile=.primus/src/make/main.mk
jobs:
prepare:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.build-info.outputs.version }}
hash: ${{ steps.build-info.outputs.hash }}
steps:
- name: self-checkout
uses: actions/checkout@v4
- id: token
name: github-token-gen
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PRIMUS_APP_ID }}
private-key: ${{ secrets.PRIMUS_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
- name: primus-checkout
uses: actions/checkout@v4
with:
repository: signoz/primus
ref: main
path: .primus
token: ${{ steps.token.outputs.token }}
- name: build-info
id: build-info
run: |
echo "version=$($MAKE info-version)" >> $GITHUB_OUTPUT
echo "hash=$($MAKE info-commit-short)" >> $GITHUB_OUTPUT
docker:
uses: signoz/primus.workflows/.github/workflows/go-build.yaml@main
needs: [prepare]
secrets: inherit
with:
PRIMUS_REF: main
GO_BUILD_CONTEXT: ./cmd/foundryctl
GO_VERSION: 1.25
GO_BUILD_FLAGS: ""
GO_BUILD_FLAGS: >-
-ldflags='-s -w
-X github.com/signoz/foundry/internal/version.version=${{ needs.prepare.outputs.version }}
-X github.com/signoz/foundry/internal/version.hash=${{ needs.prepare.outputs.hash }}
-X github.com/signoz/foundry/internal/ledger.key=YPscRxkyHv2Fn0aOucmLrMYBBVAC1AFu"
GO_NAME: foundryctl
DOCKER_BASE_IMAGES: '{"distroless":"gcr.io/distroless/base:latest"}'
DOCKER_DOCKERFILE_PATH: Dockerfile.multi-arch
+5
View File
@@ -24,6 +24,11 @@ builds:
goarm64:
- v8.0
mod_timestamp: "{{ .CommitTimestamp }}"
ldflags:
- -s -w
- -X github.com/signoz/foundry/internal/version.version=v{{ .Version }}
- -X github.com/signoz/foundry/internal/version.hash={{ .ShortCommit }}
- -X github.com/signoz/foundry/internal/ledger.key=YPscRxkyHv2Fn0aOucmLrMYBBVAC1AFu
tags:
- timetzdata
+7 -6
View File
@@ -3,6 +3,7 @@
FOUNDRYCTL := go run ./cmd/foundryctl
GOTMPL := go run go.opentelemetry.io/build-tools/gotmpl@latest
CASTINGS_JSON = $$(cat docs/examples/castings.json)
NO_LEDGER := "--no-ledger"
clean:
cd pours/deployment && docker compose -p dev down --remove-orphans --volumes
@@ -10,26 +11,26 @@ clean:
rm -rf ./pours
gauge:
$(FOUNDRYCTL) gauge --debug -f ./tmp/casting.yaml
$(FOUNDRYCTL) gauge --debug $(NO_LEDGER) -f ./tmp/casting.yaml
forge:
$(FOUNDRYCTL) forge --debug -f ./tmp/casting.yaml
$(FOUNDRYCTL) forge --debug $(NO_LEDGER) -f ./tmp/casting.yaml
cast:
$(FOUNDRYCTL) cast --debug -f ./tmp/casting.yaml
$(FOUNDRYCTL) cast --debug $(NO_LEDGER) -f ./tmp/casting.yaml
test:
make forge
make docker
gen-examples:
$(FOUNDRYCTL) gen --debug examples
$(FOUNDRYCTL) gen --debug $(NO_LEDGER) examples
gen-schemas:
$(FOUNDRYCTL) gen --debug schemas
$(FOUNDRYCTL) gen --debug $(NO_LEDGER) schemas
gen-docs:
$(FOUNDRYCTL) catalog --debug --format json --output "docs/examples/castings.json"
$(FOUNDRYCTL) catalog --debug $(NO_LEDGER) --format json --output "docs/examples/castings.json"
$(GOTMPL) -b README.md.gotmpl -d "$(CASTINGS_JSON)" -o README.md
$(GOTMPL) -b docs/examples/README.md.gotmpl -d "$(CASTINGS_JSON)" -o docs/examples/README.md
+20 -5
View File
@@ -10,6 +10,7 @@ import (
foundryerrors "github.com/signoz/foundry/internal/errors"
"github.com/signoz/foundry/internal/foundry"
"github.com/signoz/foundry/internal/instrumentation"
"github.com/signoz/foundry/internal/ledger"
"github.com/spf13/cobra"
)
@@ -20,22 +21,26 @@ func registerCastCmd(rootCmd *cobra.Command) {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
logger := instrumentation.NewLogger(commonCfg.Debug)
tracker := newTracker()
defer func() {
_ = tracker.Close()
}()
if !castCfg.NoGauge {
err := runGauge(ctx, logger, commonCfg.File)
err := runGauge(ctx, logger, tracker, commonCfg.File)
if err != nil {
return err
}
}
if !castCfg.NoForge {
err := runForge(ctx, logger, commonCfg.File, poursCfg.Path)
err := runForge(ctx, logger, tracker, commonCfg.File, poursCfg.Path)
if err != nil {
return err
}
}
return runCast(ctx, logger, poursCfg.Path, commonCfg.File)
return runCast(ctx, logger, tracker, poursCfg.Path, commonCfg.File)
},
}
@@ -43,7 +48,7 @@ func registerCastCmd(rootCmd *cobra.Command) {
castCfg.RegisterFlags(castCmd)
}
func runCast(ctx context.Context, logger *slog.Logger, poursPath string, configPath string) error {
func runCast(ctx context.Context, logger *slog.Logger, tracker ledger.Ledger, poursPath string, configPath string) error {
foundry, err := foundry.New(logger)
if err != nil {
logger.ErrorContext(ctx, "failed to create foundry, please report this issues to developers at https://github.com/signoz/foundry/issues", foundryerrors.LogAttr(err))
@@ -59,8 +64,18 @@ func runCast(ctx context.Context, logger *slog.Logger, poursPath string, configP
lock, err := foundry.Config.GetV1Alpha1Lock(ctx, configPath)
if err != nil {
logger.ErrorContext(ctx, "failed to load generated casting.yaml.lock", foundryerrors.LogAttr(err))
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("cast"), err))
return err
}
return foundry.Cast(ctx, lock, poursPath)
props := ledger.CastingProperties("cast", lock)
err = foundry.Cast(ctx, lock, poursPath)
if err != nil {
tracker.Track(ctx, ledger.WithError(props, err))
return err
}
tracker.Track(ctx, ledger.WithSuccess(props))
return nil
}
+31 -5
View File
@@ -1,6 +1,7 @@
package main
import (
"context"
"encoding/json"
"log/slog"
"os"
@@ -10,6 +11,7 @@ import (
"github.com/signoz/foundry/api/v1alpha1"
"github.com/signoz/foundry/internal/foundry"
"github.com/signoz/foundry/internal/instrumentation"
"github.com/signoz/foundry/internal/ledger"
"github.com/spf13/cobra"
)
@@ -19,8 +21,12 @@ func registerCatalogCmd(rootCmd *cobra.Command) {
Short: "Show available castings",
RunE: func(cmd *cobra.Command, args []string) error {
logger := instrumentation.NewLogger(commonCfg.Debug)
tracker := newTracker()
defer func() {
_ = tracker.Close()
}()
return runCatalog(logger)
return runCatalog(cmd.Context(), logger, tracker)
},
}
@@ -61,9 +67,10 @@ func catalogGroup(e castingEntry) int {
}
}
func runCatalog(logger *slog.Logger) error {
func runCatalog(ctx context.Context, logger *slog.Logger, tracker ledger.Ledger) error {
f, err := foundry.New(logger)
if err != nil {
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("catalog"), err))
return err
}
@@ -88,13 +95,25 @@ func runCatalog(logger *slog.Logger) error {
if catalogCfg.Format == "json" {
data, err := json.MarshalIndent(map[string]any{"Castings": entries}, "", " ")
if err != nil {
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("catalog"), err))
return err
}
if catalogCfg.OutPath != "" {
return os.WriteFile(catalogCfg.OutPath, data, 0644)
err = os.WriteFile(catalogCfg.OutPath, data, 0644)
if err != nil {
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("catalog"), err))
return err
}
tracker.Track(ctx, ledger.WithSuccess(ledger.CommandProperties("catalog")))
return nil
}
_, err = os.Stdout.Write(data)
return err
if err != nil {
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("catalog"), err))
return err
}
tracker.Track(ctx, ledger.WithSuccess(ledger.CommandProperties("catalog")))
return nil
}
table := tablewriter.NewWriter(os.Stdout)
@@ -103,5 +122,12 @@ func runCatalog(logger *slog.Logger) error {
_ = table.Append(e.Mode, e.Flavor, e.Platform, e.Example)
}
return table.Render()
err = table.Render()
if err != nil {
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("catalog"), err))
return err
}
tracker.Track(ctx, ledger.WithSuccess(ledger.CommandProperties("catalog")))
return nil
}
+4 -2
View File
@@ -17,13 +17,15 @@ var (
)
type commonConfig struct {
File string
Debug bool
File string
Debug bool
NoLedger bool
}
func (c *commonConfig) RegisterFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(&c.File, "file", "f", "casting.yaml", "Path to the casting configuration file.")
cmd.PersistentFlags().BoolVarP(&c.Debug, "debug", "d", false, "Enable debug mode.")
cmd.PersistentFlags().BoolVar(&c.NoLedger, "no-ledger", false, "Disable anonymous usage ledger.")
}
type poursConfig struct {
+13 -2
View File
@@ -9,6 +9,7 @@ import (
foundryerrors "github.com/signoz/foundry/internal/errors"
"github.com/signoz/foundry/internal/foundry"
"github.com/signoz/foundry/internal/instrumentation"
"github.com/signoz/foundry/internal/ledger"
"github.com/signoz/foundry/internal/writer"
"github.com/spf13/cobra"
)
@@ -21,15 +22,19 @@ func registerForgeCmd(rootCmd *cobra.Command) {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
logger := instrumentation.NewLogger(commonCfg.Debug)
tracker := newTracker()
defer func() {
_ = tracker.Close()
}()
return runForge(ctx, logger, commonCfg.File, poursCfg.Path)
return runForge(ctx, logger, tracker, commonCfg.File, poursCfg.Path)
},
}
rootCmd.AddCommand(forgeCmd)
}
func runForge(ctx context.Context, logger *slog.Logger, path string, poursPath string) error {
func runForge(ctx context.Context, logger *slog.Logger, tracker ledger.Ledger, path string, poursPath string) error {
foundry, err := foundry.New(logger)
if err != nil {
logger.ErrorContext(ctx, "failed to create foundry, please report this issues to developers at https://github.com/signoz/foundry/issues", foundryerrors.LogAttr(err))
@@ -38,18 +43,24 @@ func runForge(ctx context.Context, logger *slog.Logger, path string, poursPath s
config, err := foundry.Config.GetV1Alpha1(ctx, path)
if err != nil {
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("forge"), err))
return err
}
props := ledger.CastingProperties("forge", config)
poursAbsPath, err := filepath.Abs(poursPath)
if err != nil {
tracker.Track(ctx, ledger.WithError(props, err))
return err
}
err = foundry.Forge(ctx, config, path, &writer.Options{Output: &os.File{}, TargetDirectory: poursAbsPath})
if err != nil {
tracker.Track(ctx, ledger.WithError(props, err))
return err
}
tracker.Track(ctx, ledger.WithSuccess(props))
return nil
}
+12 -2
View File
@@ -7,6 +7,7 @@ import (
foundryerrors "github.com/signoz/foundry/internal/errors"
"github.com/signoz/foundry/internal/foundry"
"github.com/signoz/foundry/internal/instrumentation"
"github.com/signoz/foundry/internal/ledger"
"github.com/spf13/cobra"
)
@@ -17,15 +18,19 @@ func registerGaugeCmd(rootCmd *cobra.Command) {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
logger := instrumentation.NewLogger(commonCfg.Debug)
tracker := newTracker()
defer func() {
_ = tracker.Close()
}()
return runGauge(ctx, logger, commonCfg.File)
return runGauge(ctx, logger, tracker, commonCfg.File)
},
}
rootCmd.AddCommand(gaugeCmd)
}
func runGauge(ctx context.Context, logger *slog.Logger, path string) error {
func runGauge(ctx context.Context, logger *slog.Logger, tracker ledger.Ledger, path string) error {
foundry, err := foundry.New(logger)
if err != nil {
logger.ErrorContext(ctx, "failed to create foundry, please report this issues to developers at https://github.com/signoz/foundry/issues", foundryerrors.LogAttr(err))
@@ -35,14 +40,19 @@ func runGauge(ctx context.Context, logger *slog.Logger, path string) error {
casting, err := foundry.Config.GetV1Alpha1(ctx, path)
if err != nil {
logger.ErrorContext(ctx, err.Error())
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("gauge"), err))
return err
}
props := ledger.CastingProperties("gauge", casting)
err = foundry.Gauge(ctx, casting)
if err != nil {
logger.ErrorContext(ctx, err.Error())
tracker.Track(ctx, ledger.WithError(props, err))
return err
}
tracker.Track(ctx, ledger.WithSuccess(props))
return nil
}
+20 -6
View File
@@ -11,6 +11,7 @@ import (
foundryerrors "github.com/signoz/foundry/internal/errors"
"github.com/signoz/foundry/internal/foundry"
"github.com/signoz/foundry/internal/instrumentation"
"github.com/signoz/foundry/internal/ledger"
"github.com/signoz/foundry/internal/types"
"github.com/spf13/cobra"
"github.com/swaggest/jsonschema-go"
@@ -35,8 +36,12 @@ func registerGenExamples(rootCmd *cobra.Command) {
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
logger := instrumentation.NewLogger(commonCfg.Debug)
tracker := newTracker()
defer func() {
_ = tracker.Close()
}()
return runGenExamples(ctx, logger)
return runGenExamples(ctx, logger, tracker)
},
}
@@ -49,19 +54,23 @@ func registerGenSchemas(rootCmd *cobra.Command) {
Short: "Generate schema files.",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
logger := instrumentation.NewLogger(commonCfg.Debug)
tracker := newTracker()
defer func() {
_ = tracker.Close()
}()
return runGenSchemas(ctx, logger)
return runGenSchemas(ctx, tracker)
},
}
rootCmd.AddCommand(genSchemasCmd)
}
func runGenExamples(ctx context.Context, logger *slog.Logger) error {
func runGenExamples(ctx context.Context, logger *slog.Logger, tracker ledger.Ledger) error {
foundry, err := foundry.New(logger)
if err != nil {
logger.ErrorContext(ctx, "failed to create foundry, please report this issues to developers at https://github.com/signoz/foundry/issues", foundryerrors.LogAttr(err))
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("gen.examples"), err))
return err
}
@@ -82,27 +91,32 @@ func runGenExamples(ctx context.Context, logger *slog.Logger) error {
return err
}
err = runForge(ctx, logger, filepath.Join(rootPath, "casting.yaml"), filepath.Join(rootPath, "pours"))
err = runForge(ctx, logger, tracker, filepath.Join(rootPath, "casting.yaml"), filepath.Join(rootPath, "pours"))
if err != nil {
logger.ErrorContext(ctx, "failed to forge casting", slog.Any("deployment", deployment), foundryerrors.LogAttr(err))
continue
}
}
tracker.Track(ctx, ledger.WithSuccess(ledger.CommandProperties("gen.examples")))
return nil
}
func runGenSchemas(context.Context, *slog.Logger) error {
func runGenSchemas(ctx context.Context, tracker ledger.Ledger) error {
reflector := jsonschema.Reflector{}
schema, err := reflector.Reflect(v1alpha1.Casting{})
if err != nil {
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("gen.schemas"), err))
log.Fatal(err)
}
err = os.WriteFile(filepath.Join("docs", "schemas", "v1alpha1.yaml"), types.MustMarshalYAML(schema), 0644)
if err != nil {
tracker.Track(ctx, ledger.WithError(ledger.CommandProperties("gen.schemas"), err))
return err
}
tracker.Track(ctx, ledger.WithSuccess(ledger.CommandProperties("gen.schemas")))
return nil
}
+20
View File
@@ -6,6 +6,9 @@ import (
foundryerrors "github.com/signoz/foundry/internal/errors"
"github.com/signoz/foundry/internal/instrumentation"
"github.com/signoz/foundry/internal/ledger"
"github.com/signoz/foundry/internal/ledger/noopledger"
"github.com/signoz/foundry/internal/ledger/segmentledger"
"github.com/spf13/cobra"
)
@@ -29,6 +32,7 @@ func main() {
registerCastCmd(rootCmd)
registerGenCmd(rootCmd)
registerCatalogCmd(rootCmd)
registerVersionCmd(rootCmd)
logger := instrumentation.NewLogger(false)
@@ -37,3 +41,19 @@ func main() {
os.Exit(1)
}
}
// newTracker creates a Ledger based on config.
// Returns a no-op ledger when --no-ledger is passed.
func newTracker() ledger.Ledger {
config := ledger.NewConfig()
if commonCfg.NoLedger {
config.Enabled = false
}
switch config.Provider() {
case "segment":
return segmentledger.New(config)
default:
return noopledger.New()
}
}
+36
View File
@@ -0,0 +1,36 @@
package main
import (
"fmt"
"os"
"github.com/signoz/foundry/internal/version"
"github.com/spf13/cobra"
)
var versionCfg versionConfig
type versionConfig struct {
Short bool
}
func (c *versionConfig) RegisterFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&c.Short, "short", false, "Print version in a single line")
}
func registerVersionCmd(rootCmd *cobra.Command) {
versionCmd := &cobra.Command{
Use: "version",
Short: "Print the version information",
Run: func(cmd *cobra.Command, args []string) {
if versionCfg.Short {
_, _ = fmt.Fprintln(os.Stdout, version.Info.Short())
return
}
version.Info.PrettyPrint()
},
}
versionCfg.RegisterFlags(versionCmd)
rootCmd.AddCommand(versionCmd)
}
+51
View File
@@ -0,0 +1,51 @@
# Ledger
Foundryctl maintains an anonymous usage ledger to help the SigNoz team understand how the tool is used, identify common errors, and prioritize improvements. **No personally identifiable information (PII) is collected.**
## What is collected
Each command execution sends a single `foundryctl` event with the following properties:
| Property | Description | Example |
|---|---|---|
| `command` | The command that was run | `forge`, `cast` |
| `platform` | Deployment platform from casting.yaml | `aws`, `docker`, `linux` |
| `mode` | Deployment mode | `docker`, `systemd`, `kubernetes` |
| `flavor` | Deployment flavor | `compose`, `binary`, `helm` |
| `patches_configured` | Whether patches are defined | `true` / `false` |
| `patch_count` | Number of patch entries | `0`, `2` |
| `infrastructure_enabled` | Whether IaC generation is enabled | `true` / `false` |
| `metastore_kind` | MetaStore backend type | `postgres`, `sqlite` |
| `telemetry_store_kind` | TelemetryStore backend type | `clickhouse` |
| `telemetry_keeper_kind` | TelemetryKeeper backend type | `clickhousekeeper` |
| `success` | Whether the command succeeded | `true` / `false` |
| `error` | Error message (on failure only) | `missing tool: docker` |
| `os` | Operating system | `linux`, `darwin` |
| `arch` | CPU architecture | `amd64`, `arm64` |
| `foundry_version` | foundryctl version | `0.1.0` |
### Identity
Events are attributed to the machine hostname as an anonymous identifier. No usernames, emails, IP addresses, or file contents are sent.
## Tracked commands
All commands send the same `foundryctl` event, differentiated by the `command` property:
- `gauge`
- `forge`
- `cast`
- `gen.examples`
- `gen.schemas`
- `catalog`
## How to disable the ledger
### Per-command
Use the `--no-ledger` flag on any command:
```bash
foundryctl forge --no-ledger
foundryctl --no-ledger cast
```
+3
View File
@@ -6,6 +6,7 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/evanphx/json-patch/v5 v5.9.11
github.com/olekukonko/tablewriter v1.1.4
github.com/segmentio/analytics-go/v3 v3.3.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/swaggest/jsonschema-go v0.3.79
@@ -28,6 +29,7 @@ require (
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/clipperhouse/displaywidth v0.10.0 // indirect
@@ -91,6 +93,7 @@ require (
github.com/rubenv/sql-migrate v1.8.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/segmentio/backo-go v1.0.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.7.0 // indirect
+6
View File
@@ -26,6 +26,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bool64/dev v0.2.43 h1:yQ7qiZVef6WtCl2vDYU0Y+qSq+0aBrQzY8KXkklk9cQ=
github.com/bool64/dev v0.2.43/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
@@ -256,6 +258,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/segmentio/analytics-go/v3 v3.3.0 h1:8VOMaVGBW03pdBrj1CMFfY9o/rnjJC+1wyQHlVxjw5o=
github.com/segmentio/analytics-go/v3 v3.3.0/go.mod h1:p8owAF8X+5o27jmvUognuXxdtqvSGtD0ZrfY2kcS9bE=
github.com/segmentio/backo-go v1.0.0 h1:kbOAtGJY2DqOR0jfRkYEorx/b18RgtepGtY3+Cpe6qA=
github.com/segmentio/backo-go v1.0.0/go.mod h1:kJ9mm9YmoWSkk+oQ+5Cj8DEoRCX2JT6As4kEtIIOp1M=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
+89
View File
@@ -0,0 +1,89 @@
package ledger
import "github.com/signoz/foundry/api/v1alpha1"
// key is the Segment write key, set via ldflags at build time.
// Example: -ldflags "-X github.com/signoz/foundry/internal/ledger.key=<key>".
var key string = "<unset>"
// Config holds ledger configuration.
type Config struct {
Enabled bool
Segment Segment
}
// Segment holds Segment-specific configuration.
type Segment struct {
Key string
}
// NewConfig returns the default ledger configuration.
// The Segment write key is populated from the ldflags-injected value.
func NewConfig() Config {
return Config{
Enabled: true,
Segment: Segment{
Key: key,
},
}
}
// Provider returns the provider name based on the configuration.
func (c Config) Provider() string {
if c.Enabled {
return "segment"
}
return "noop"
}
// Property keys for casting details.
const (
PropCommand = "command"
PropPlatform = "platform"
PropMode = "mode"
PropFlavor = "flavor"
PropPatchesConfigured = "patches_configured"
PropPatchCount = "patch_count"
PropInfrastructureEnabled = "infrastructure_enabled"
PropMetaStoreKind = "metastore_kind"
PropTelemetryStoreKind = "telemetry_store_kind"
PropTelemetryKeeperKind = "telemetry_keeper_kind"
PropSuccess = "success"
PropError = "error"
)
// CommandProperties returns a minimal property map with just the command name.
func CommandProperties(command string) map[string]any {
return map[string]any{
PropCommand: command,
}
}
// CastingProperties extracts trackable properties from a Casting config.
func CastingProperties(command string, casting v1alpha1.Casting) map[string]any {
return map[string]any{
PropCommand: command,
PropPlatform: casting.Spec.Deployment.Platform,
PropMode: casting.Spec.Deployment.Mode,
PropFlavor: casting.Spec.Deployment.Flavor,
PropPatchesConfigured: len(casting.Spec.Patches) > 0,
PropPatchCount: len(casting.Spec.Patches),
PropInfrastructureEnabled: casting.Spec.Infrastructure.Enabled,
PropMetaStoreKind: casting.Spec.MetaStore.Kind.String(),
PropTelemetryStoreKind: casting.Spec.TelemetryStore.Kind.String(),
PropTelemetryKeeperKind: casting.Spec.TelemetryKeeper.Kind.String(),
}
}
// WithSuccess adds success=true to the properties.
func WithSuccess(props map[string]any) map[string]any {
props[PropSuccess] = true
return props
}
// WithError adds success=false and the error message to the properties.
func WithError(props map[string]any, err error) map[string]any {
props[PropSuccess] = false
props[PropError] = err.Error()
return props
}
+13
View File
@@ -0,0 +1,13 @@
// Package ledger provides anonymous usage tracking for foundryctl commands.
package ledger
import "context"
// Ledger is the interface for tracking CLI usage events.
type Ledger interface {
// Track records a single foundryctl event with the given properties.
Track(ctx context.Context, properties map[string]any)
// Close flushes any pending events and releases resources.
Close() error
}
+18
View File
@@ -0,0 +1,18 @@
package noopledger
import (
"context"
"github.com/signoz/foundry/internal/ledger"
)
// provider is a no-op implementation of ledger.Ledger.
type provider struct{}
// New creates a ledger that does nothing.
func New() ledger.Ledger {
return &provider{}
}
func (p *provider) Track(_ context.Context, _ map[string]any) {}
func (p *provider) Close() error { return nil }
+69
View File
@@ -0,0 +1,69 @@
package segmentledger
import (
"context"
"os"
"runtime"
segment "github.com/segmentio/analytics-go/v3"
"github.com/signoz/foundry/internal/ledger"
"github.com/signoz/foundry/internal/ledger/noopledger"
"github.com/signoz/foundry/internal/version"
)
// provider implements ledger.Ledger using Segment.
type provider struct {
client segment.Client
}
// New creates a new Segment ledger provider.
// Returns a noop provider if the write key is not set.
func New(config ledger.Config) ledger.Ledger {
if config.Segment.Key == "" || config.Segment.Key == "<unset>" {
return noopledger.New()
}
client, err := segment.NewWithConfig(config.Segment.Key, segment.Config{})
if err != nil {
return noopledger.New()
}
return &provider{
client: client,
}
}
func (p *provider) Track(_ context.Context, properties map[string]any) {
if properties == nil {
properties = make(map[string]any)
}
properties["os"] = runtime.GOOS
properties["arch"] = runtime.GOARCH
properties["foundry_version"] = version.Info.Version()
props := segment.NewProperties()
for k, v := range properties {
props.Set(k, v)
}
_ = p.client.Enqueue(segment.Track{
AnonymousId: getDistinctID(),
Event: "foundryctl",
Properties: props,
})
}
func (p *provider) Close() error {
return p.client.Close()
}
// getDistinctID returns a stable anonymous identifier for the machine.
// It uses the hostname so there is no PII stored.
func getDistinctID() string {
hostname, err := os.Hostname()
if err != nil {
return "unknown"
}
return hostname
}
+74
View File
@@ -0,0 +1,74 @@
package version
import (
"fmt"
"os"
"runtime"
"time"
)
// These will be set via ldflags at build time.
var (
version string = "<unset>"
hash string = "<unset>"
)
var (
Info Build = Build{
version: version,
hash: hash,
goVersion: runtime.Version(),
}
)
// Build contains information about the build environment.
type Build struct {
// The version of the current build.
version string
// The git hash of the current build.
hash string
// The version of go.
goVersion string
}
func (b Build) Version() string {
return b.version
}
func (b Build) Hash() string {
return b.hash
}
func (b Build) GoVersion() string {
return b.goVersion
}
func (b Build) Short() string {
return fmt.Sprintf("foundryctl %s (%s) %s", b.version, b.hash, b.goVersion)
}
func (b Build) PrettyPrint() {
year := time.Now().Year()
ascii := []string{
"███████╗ ██████╗ ██╗ ██╗███╗ ██╗██████╗ ██████╗ ██╗ ██╗",
"██╔════╝██╔═══██╗██║ ██║████╗ ██║██╔══██╗██╔══██╗╚██╗ ██╔╝",
"█████╗ ██║ ██║██║ ██║██╔██╗ ██║██║ ██║██████╔╝ ╚████╔╝ ",
"██╔══╝ ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══██╗ ╚██╔╝ ",
"██║ ╚██████╔╝╚██████╔╝██║ ╚████║██████╔╝██║ ██║ ██║ ",
"╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚═╝ ╚═╝ ╚═╝ ",
}
_, _ = fmt.Fprintln(os.Stdout)
for _, line := range ascii {
_, _ = fmt.Fprintln(os.Stdout, line)
}
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintf(os.Stdout, " Version: %s\n", b.version)
_, _ = fmt.Fprintf(os.Stdout, " Commit: %s\n", b.hash)
_, _ = fmt.Fprintf(os.Stdout, " Go: %s\n", b.goVersion)
_, _ = fmt.Fprintln(os.Stdout)
_, _ = fmt.Fprintf(os.Stdout, " Copyright %d SigNoz, All rights reserved.\n", year)
_, _ = fmt.Fprintln(os.Stdout)
}