-
Notifications
You must be signed in to change notification settings - Fork 4
Telemetry client #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Telemetry client #58
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
eaebd4b
telemetry client
carole-lavillonniere 4187957
address review comments
carole-lavillonniere 01e394b
no more os.Exit(1)
carole-lavillonniere 5b3c725
replace X-Client header with User-Agent
carole-lavillonniere ffb381c
rename Track to Emit
carole-lavillonniere 4f3e70b
mirror emulator logic for machine_id
carole-lavillonniere a32c541
pass context to Emit
carole-lavillonniere f7a5bab
replace Add/Wait with fire-and-forget channel
carole-lavillonniere ec05e69
check for CI var existence
carole-lavillonniere a0940f1
Close is idempotent
carole-lavillonniere a85cac5
Merge branch 'main' into carole/drg-534
carole-lavillonniere File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,34 +1,24 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
|
|
||
| "github.com/localstack/lstk/internal/env" | ||
| "github.com/localstack/lstk/internal/output" | ||
| "github.com/localstack/lstk/internal/runtime" | ||
| "github.com/localstack/lstk/internal/telemetry" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| func newStartCmd(cfg *env.Env) *cobra.Command { | ||
| func newStartCmd(cfg *env.Env, tel *telemetry.Client) *cobra.Command { | ||
| return &cobra.Command{ | ||
| Use: "start", | ||
| Short: "Start emulator", | ||
| Long: "Start emulator and services.", | ||
| PreRunE: initConfig, | ||
| Run: func(cmd *cobra.Command, args []string) { | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| rt, err := runtime.NewDockerRuntime() | ||
| if err != nil { | ||
| fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
|
|
||
| if err := runStart(cmd.Context(), rt, cfg); err != nil { | ||
| if !output.IsSilent(err) { | ||
| fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| } | ||
| os.Exit(1) | ||
| return err | ||
| } | ||
| return runStart(cmd.Context(), rt, cfg, tel) | ||
| }, | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| package telemetry | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "os" | ||
| "runtime" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "github.com/google/uuid" | ||
| "github.com/localstack/lstk/internal/version" | ||
| ) | ||
|
|
||
| func userAgent() string { | ||
| return fmt.Sprintf("localstack lstk/%s (%s; %s)", version.Version(), runtime.GOOS, runtime.GOARCH) | ||
| } | ||
|
|
||
| type Client struct { | ||
| enabled bool | ||
| sessionID string | ||
| machineID string | ||
|
|
||
| httpClient *http.Client | ||
| endpoint string | ||
|
|
||
| events chan eventBody | ||
| done chan struct{} | ||
| closeOnce sync.Once | ||
| } | ||
|
|
||
| func New(endpoint string, disabled bool) *Client { | ||
| if disabled { | ||
| return &Client{enabled: false} | ||
| } | ||
| c := &Client{ | ||
| enabled: true, | ||
| sessionID: uuid.NewString(), | ||
| machineID: LoadOrCreateMachineID(), | ||
| // http.Client has no default timeout (zero means none). Without one, a | ||
| // slow or unreachable endpoint would block the worker goroutine. | ||
| httpClient: &http.Client{ | ||
| Timeout: 3 * time.Second, | ||
| }, | ||
| endpoint: endpoint, | ||
| events: make(chan eventBody, 64), | ||
| done: make(chan struct{}), | ||
| } | ||
| go c.worker() | ||
| return c | ||
| } | ||
|
|
||
| type requestBody struct { | ||
| Events []eventBody `json:"events"` | ||
| } | ||
|
|
||
| type eventBody struct { | ||
| ctx context.Context // not serialized; carries context to the worker | ||
| Name string `json:"name"` | ||
| Metadata eventMetadata `json:"metadata"` | ||
| Payload any `json:"payload"` | ||
| } | ||
|
|
||
| type eventMetadata struct { | ||
| ClientTime string `json:"client_time"` | ||
| SessionID string `json:"session_id"` | ||
| } | ||
|
|
||
| func (c *Client) Emit(ctx context.Context, name string, payload map[string]any) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: I wonder if this naming won't be confusing with the other events we have. But I raised the idea of merging those into the same concept in the future either way so it makes sense. |
||
| if !c.enabled { | ||
| return | ||
| } | ||
|
|
||
| enriched := make(map[string]any, len(payload)+6) | ||
| for k, v := range payload { | ||
| enriched[k] = v | ||
| } | ||
| enriched["version"] = version.Version() | ||
| enriched["os"] = runtime.GOOS | ||
| enriched["arch"] = runtime.GOARCH | ||
| _, enriched["is_ci"] = os.LookupEnv("CI") | ||
| if c.machineID != "" { | ||
| enriched["machine_id"] = c.machineID | ||
| } | ||
|
|
||
| body := eventBody{ | ||
| ctx: context.WithoutCancel(ctx), | ||
| Name: name, | ||
| Metadata: eventMetadata{ | ||
| ClientTime: time.Now().UTC().Format("2006-01-02 15:04:05.000000"), | ||
| SessionID: c.sessionID, | ||
| }, | ||
| Payload: enriched, | ||
| } | ||
|
|
||
| select { | ||
| case c.events <- body: | ||
| default: | ||
| } | ||
| } | ||
|
|
||
| func (c *Client) worker() { | ||
| defer close(c.done) | ||
| for body := range c.events { | ||
| c.send(body) | ||
| } | ||
| } | ||
|
|
||
| func (c *Client) send(body eventBody) { | ||
| data, err := json.Marshal(requestBody{Events: []eventBody{body}}) | ||
| if err != nil { | ||
| return | ||
| } | ||
|
|
||
| req, err := http.NewRequestWithContext(body.ctx, http.MethodPost, c.endpoint, bytes.NewReader(data)) | ||
| if err != nil { | ||
| return | ||
| } | ||
| req.Header.Set("Content-Type", "application/json") | ||
| req.Header.Set("User-Agent", userAgent()) | ||
|
|
||
| resp, err := c.httpClient.Do(req) | ||
| if err != nil { | ||
| return | ||
| } | ||
| _ = resp.Body.Close() | ||
| } | ||
|
|
||
| // Close stops accepting new events, drains the event buffer, and blocks until | ||
| // all pending HTTP requests have completed. Call it before process exit to | ||
| // avoid dropping telemetry events. | ||
| func (c *Client) Close() { | ||
| if !c.enabled { | ||
| return | ||
| } | ||
| c.closeOnce.Do(func() { | ||
| close(c.events) | ||
| <-c.done | ||
| }) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
internal/outputtyped emitters instead of direct stderr writes.Line 66 prints directly via
fmt.Fprintf(os.Stderr, ...), which bypasses the typed output pathway used by command handlers.As per coding guidelines, "Emit typed events through internal/output (EmitInfo, EmitSuccess, EmitNote, EmitWarning, EmitStatus, EmitProgress, etc.) instead of printing from domain/command handlers".
🤖 Prompt for AI Agents