Skip to content

Commit 34f431c

Browse files
committed
chore: Send instrumentation data on kill
1 parent c65d8ac commit 34f431c

4 files changed

Lines changed: 149 additions & 99 deletions

File tree

cliv2/cmd/cliv2/instrumentation.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ package main
44
import _ "github.com/snyk/go-application-framework/pkg/networking/fips_enable"
55

66
import (
7+
"encoding/json"
78
"os/exec"
9+
"strconv"
810
"strings"
911
"time"
1012

13+
"github.com/rs/zerolog"
1114
"github.com/snyk/go-application-framework/pkg/analytics"
1215
"github.com/snyk/go-application-framework/pkg/configuration"
1316
"github.com/snyk/go-application-framework/pkg/instrumentation"
1417

18+
"github.com/snyk/cli/cliv2/internal/constants"
1519
cli_utils "github.com/snyk/cli/cliv2/internal/utils"
1620

1721
localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows"
@@ -74,3 +78,70 @@ func updateInstrumentationDataBeforeSending(cliAnalytics analytics.Analytics, st
7478
cliAnalytics.GetInstrumentation().SetStatus(analytics.Failure)
7579
}
7680
}
81+
82+
func sendAnalytics(a analytics.Analytics, debugLogger *zerolog.Logger) {
83+
debugLogger.Print("Sending Analytics")
84+
85+
a.SetApiUrl(globalConfiguration.GetString(configuration.API_URL))
86+
87+
res, err := a.Send()
88+
if err != nil || res == nil {
89+
debugLogger.Err(err).Msg("Failed to send Analytics")
90+
return
91+
}
92+
defer func() {
93+
_ = res.Body.Close()
94+
}()
95+
96+
successfullySend := 200 <= res.StatusCode && res.StatusCode < 300
97+
if successfullySend {
98+
debugLogger.Print("Analytics successfully send")
99+
} else {
100+
var details string
101+
if res != nil {
102+
details = res.Status
103+
}
104+
105+
debugLogger.Print("Failed to send Analytics:", details)
106+
}
107+
}
108+
109+
func sendInstrumentation(eng workflow.Engine, instrumentor analytics.InstrumentationCollector, logger *zerolog.Logger) {
110+
// Avoid duplicate data to be sent for IDE integrations that use the CLI
111+
if !shallSendInstrumentation(eng.GetConfiguration(), instrumentor) {
112+
logger.Print("This CLI call is not instrumented!")
113+
return
114+
}
115+
116+
// add temporary static nodejs binary flag, remove once linuxstatic is official
117+
staticNodeJsBinaryBool, parseErr := strconv.ParseBool(constants.StaticNodeJsBinary)
118+
if parseErr != nil {
119+
logger.Print("Failed to parse staticNodeJsBinary:", parseErr)
120+
} else {
121+
// the legacycli:: prefix is added to maintain compatibility with our monitoring dashboard
122+
instrumentor.AddExtension("legacycli::static-nodejs-binary", staticNodeJsBinaryBool)
123+
}
124+
125+
logger.Print("Sending Instrumentation")
126+
data, err := analytics.GetV2InstrumentationObject(instrumentor, analytics.WithLogger(logger))
127+
if err != nil {
128+
logger.Err(err).Msg("Failed to derive data object")
129+
}
130+
131+
v2InstrumentationData := utils.ValueOf(json.Marshal(data))
132+
localConfiguration := globalConfiguration.Clone()
133+
// the report analytics workflow needs --experimental to run
134+
// we pass the flag here so that we report at every interaction
135+
localConfiguration.Set(configuration.FLAG_EXPERIMENTAL, true)
136+
localConfiguration.Set("inputData", string(v2InstrumentationData))
137+
_, err = eng.InvokeWithConfig(
138+
localworkflows.WORKFLOWID_REPORT_ANALYTICS,
139+
localConfiguration,
140+
)
141+
142+
if err != nil {
143+
logger.Err(err).Msg("Failed to send Instrumentation")
144+
} else {
145+
logger.Print("Instrumentation successfully sent")
146+
}
147+
}

cliv2/cmd/cliv2/main.go

Lines changed: 75 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import (
1111
"io"
1212
"os"
1313
"os/exec"
14-
"strconv"
14+
"os/signal"
1515
"strings"
1616
"sync"
17+
"syscall"
1718
"time"
1819

1920
"github.com/google/uuid"
@@ -221,71 +222,6 @@ func runWorkflowAndProcessData(engine workflow.Engine, logger *zerolog.Logger, n
221222
return err
222223
}
223224

224-
func sendAnalytics(analytics analytics.Analytics, debugLogger *zerolog.Logger) {
225-
debugLogger.Print("Sending Analytics")
226-
227-
analytics.SetApiUrl(globalConfiguration.GetString(configuration.API_URL))
228-
229-
res, err := analytics.Send()
230-
if err != nil {
231-
debugLogger.Err(err).Msg("Failed to send Analytics")
232-
return
233-
}
234-
defer func() { _ = res.Body.Close() }()
235-
236-
successfullySend := 200 <= res.StatusCode && res.StatusCode < 300
237-
if successfullySend {
238-
debugLogger.Print("Analytics successfully send")
239-
} else {
240-
var details string
241-
if res != nil {
242-
details = res.Status
243-
}
244-
245-
debugLogger.Print("Failed to send Analytics:", details)
246-
}
247-
}
248-
249-
func sendInstrumentation(eng workflow.Engine, instrumentor analytics.InstrumentationCollector, logger *zerolog.Logger) {
250-
// Avoid duplicate data to be sent for IDE integrations that use the CLI
251-
if !shallSendInstrumentation(eng.GetConfiguration(), instrumentor) {
252-
logger.Print("This CLI call is not instrumented!")
253-
return
254-
}
255-
256-
// add temporary static nodejs binary flag, remove once linuxstatic is official
257-
staticNodeJsBinaryBool, parseErr := strconv.ParseBool(constants.StaticNodeJsBinary)
258-
if parseErr != nil {
259-
logger.Print("Failed to parse staticNodeJsBinary:", parseErr)
260-
} else {
261-
// the legacycli:: prefix is added to maintain compatibility with our monitoring dashboard
262-
instrumentor.AddExtension("legacycli::static-nodejs-binary", staticNodeJsBinaryBool)
263-
}
264-
265-
logger.Print("Sending Instrumentation")
266-
data, err := analytics.GetV2InstrumentationObject(instrumentor, analytics.WithLogger(logger))
267-
if err != nil {
268-
logger.Err(err).Msg("Failed to derive data object")
269-
}
270-
271-
v2InstrumentationData := utils.ValueOf(json.Marshal(data))
272-
localConfiguration := globalConfiguration.Clone()
273-
// the report analytics workflow needs --experimental to run
274-
// we pass the flag here so that we report at every interaction
275-
localConfiguration.Set(configuration.FLAG_EXPERIMENTAL, true)
276-
localConfiguration.Set("inputData", string(v2InstrumentationData))
277-
_, err = eng.InvokeWithConfig(
278-
localworkflows.WORKFLOWID_REPORT_ANALYTICS,
279-
localConfiguration,
280-
)
281-
282-
if err != nil {
283-
logger.Err(err).Msg("Failed to send Instrumentation")
284-
} else {
285-
logger.Print("Instrumentation successfully sent")
286-
}
287-
}
288-
289225
func help(_ *cobra.Command, _ []string) error {
290226
helpProvided = true
291227
args := utils.RemoveSimilar(os.Args[1:], "--") // remove all double dash arguments to avoid issues with the help command
@@ -548,11 +484,51 @@ func initExtensions(engine workflow.Engine, config configuration.Configuration)
548484
}
549485
}
550486

487+
// tearDown handles sending analytics and instrumentation
488+
// It is used both for normal exit and signal-triggered exit
489+
func tearDown(ctx context.Context, err error, errorList []error, startTime time.Time, ua networking.UserAgentInfo, cliAnalytics analytics.Analytics, networkAccess networking.NetworkAccess) int {
490+
if err != nil {
491+
errorList, err = processError(err, errorList)
492+
493+
for _, tempError := range errorList {
494+
if tempError != nil {
495+
cliAnalytics.AddError(tempError)
496+
}
497+
}
498+
}
499+
500+
exitCode := cliv2.DeriveExitCode(err)
501+
globalLogger.Printf("Deriving Exit Code %d (cause: %v)", exitCode, err)
502+
503+
displayError(err, globalEngine.GetUserInterface(), globalConfiguration, ctx)
504+
505+
updateInstrumentationDataBeforeSending(cliAnalytics, startTime, ua, exitCode)
506+
507+
if !globalConfiguration.GetBool(configuration.ANALYTICS_DISABLED) {
508+
sendAnalytics(cliAnalytics, globalLogger)
509+
}
510+
sendInstrumentation(globalEngine, cliAnalytics.GetInstrumentation(), globalLogger)
511+
512+
// cleanup resources in use
513+
// WARNING: deferred actions will execute AFTER cleanup; only defer if not impacted by this
514+
if _, cleanupErr := globalEngine.Invoke(basic_workflows.WORKFLOWID_GLOBAL_CLEANUP); cleanupErr != nil {
515+
globalLogger.Printf("Failed to cleanup %v", cleanupErr)
516+
}
517+
518+
if globalConfiguration.GetBool(configuration.DEBUG) {
519+
writeLogFooter(exitCode, errorList, globalConfiguration, networkAccess)
520+
}
521+
522+
return exitCode
523+
}
524+
551525
func MainWithErrorCode() int {
552526
initDebugBuild()
553527

554528
errorList := []error{}
555529
errorListMutex := sync.Mutex{}
530+
var tearDownOnce sync.Once
531+
var finalExitCode int
556532

557533
startTime := time.Now()
558534
var err error
@@ -656,6 +632,29 @@ func MainWithErrorCode() int {
656632
cliAnalytics.GetInstrumentation().SetStage(instrumentation.DetermineStage(cliAnalytics.IsCiEnvironment()))
657633
cliAnalytics.GetInstrumentation().SetStatus(analytics.Success)
658634

635+
// prepare for signal handling
636+
signalChan := make(chan os.Signal, 1)
637+
638+
if globalConfiguration.GetBool(configuration.PREVIEW_FEATURES_ENABLED) {
639+
// Set up signal handling to send instrumentation on premature termination
640+
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
641+
go func() {
642+
sig := <-signalChan
643+
globalLogger.Printf("Received signal %v, attempting to send instrumentation before exit", sig)
644+
645+
tearDownOnce.Do(func() {
646+
signalError := cli.NewTerminatedBySignalError(fmt.Sprintf("Signal: %v", sig))
647+
648+
errorListMutex.Lock()
649+
errorListCopy := append([]error{}, errorList...)
650+
errorListMutex.Unlock()
651+
652+
finalExitCode = tearDown(ctx, signalError, errorListCopy, startTime, ua, cliAnalytics, networkAccess)
653+
})
654+
os.Exit(finalExitCode)
655+
}()
656+
}
657+
659658
setTimeout(globalConfiguration, func() {
660659
os.Exit(constants.SNYK_EXIT_CODE_EX_UNAVAILABLE)
661660
})
@@ -681,40 +680,20 @@ func MainWithErrorCode() int {
681680
// ignore
682681
}
683682

684-
if err != nil {
685-
errorList, err = processError(err, errorList)
686-
687-
for _, tempError := range errorList {
688-
if tempError != nil {
689-
cliAnalytics.AddError(tempError)
690-
}
691-
}
683+
if globalConfiguration.GetBool(configuration.PREVIEW_FEATURES_ENABLED) {
684+
// Stop signal handling before cleanup to prevent race conditions
685+
signal.Stop(signalChan)
692686
}
693687

694-
displayError(err, globalEngine.GetUserInterface(), globalConfiguration, ctx)
695-
696-
exitCode := cliv2.DeriveExitCode(err)
697-
globalLogger.Printf("Deriving Exit Code %d (cause: %v)", exitCode, err)
698-
699-
updateInstrumentationDataBeforeSending(cliAnalytics, startTime, ua, exitCode)
700-
701-
if !globalConfiguration.GetBool(configuration.ANALYTICS_DISABLED) {
702-
sendAnalytics(cliAnalytics, globalLogger)
703-
}
704-
sendInstrumentation(globalEngine, cliAnalytics.GetInstrumentation(), globalLogger)
705-
706-
// cleanup resources in use
707-
// WARNING: deferred actions will execute AFTER cleanup; only defer if not impacted by this
708-
_, err = globalEngine.Invoke(basic_workflows.WORKFLOWID_GLOBAL_CLEANUP)
709-
if err != nil {
710-
globalLogger.Printf("Failed to cleanup %v", err)
711-
}
688+
tearDownOnce.Do(func() {
689+
errorListMutex.Lock()
690+
errorListCopy := append([]error{}, errorList...)
691+
errorListMutex.Unlock()
712692

713-
if debugEnabled {
714-
writeLogFooter(exitCode, errorList, globalConfiguration, networkAccess)
715-
}
693+
finalExitCode = tearDown(ctx, err, errorListCopy, startTime, ua, cliAnalytics, networkAccess)
694+
})
716695

717-
return exitCode
696+
return finalExitCode
718697
}
719698

720699
func processError(err error, errorList []error) ([]error, error) {

cliv2/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ require (
2121
github.com/snyk/cli-extension-secrets v0.0.0-20260323093911-a7c31752d309
2222
github.com/snyk/code-client-go v1.26.2
2323
github.com/snyk/container-cli v0.0.0-20260213211631-cd2b2cf8f3ea
24-
github.com/snyk/error-catalog-golang-public v0.0.0-20260316131845-f02d7f42046b
24+
github.com/snyk/error-catalog-golang-public v0.0.0-20260324172308-ed924e985635
2525
github.com/snyk/go-application-framework v0.0.0-20260317115718-b9ea94f06443
2626
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65
2727
github.com/snyk/snyk-iac-capture v0.6.5

cliv2/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,8 +559,8 @@ github.com/snyk/container-cli v0.0.0-20260213211631-cd2b2cf8f3ea h1:/v48hCMPiZVj
559559
github.com/snyk/container-cli v0.0.0-20260213211631-cd2b2cf8f3ea/go.mod h1:P5yW8+jkwhYBsj5l2jtHeWujyX+SAtvkC8+LELKdlWI=
560560
github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 h1:kgZNQ5ztI4+n3YKLR5LJbqL8WJmUYgDSbFKREIY79g0=
561561
github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62/go.mod h1:hTr91da/4ze2nk9q6ZW1BmfM2Z8rLUZSEZ3kK+6WGpc=
562-
github.com/snyk/error-catalog-golang-public v0.0.0-20260316131845-f02d7f42046b h1:DM2SPu7rhsD/TNS7zhv4ZoqLLi2cFOqg1VTBCP6RfSg=
563-
github.com/snyk/error-catalog-golang-public v0.0.0-20260316131845-f02d7f42046b/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4=
562+
github.com/snyk/error-catalog-golang-public v0.0.0-20260324172308-ed924e985635 h1:UA5a0wCahEp22LPcha9a5AbqP/p8ognF0seGdGnUjfU=
563+
github.com/snyk/error-catalog-golang-public v0.0.0-20260324172308-ed924e985635/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4=
564564
github.com/snyk/go-application-framework v0.0.0-20260317115718-b9ea94f06443 h1:gZuN0M6m2pQK2RGckTAe7ke7CNSPtMMnVpTEWTuUYTc=
565565
github.com/snyk/go-application-framework v0.0.0-20260317115718-b9ea94f06443/go.mod h1:6MuCxSVGYNY3gfNKPZc5oMuy5/Q+yxbLxKnVtOMSB8Y=
566566
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 h1:CEQuYv0Go6MEyRCD3YjLYM2u3Oxkx8GpCpFBd4rUTUk=

0 commit comments

Comments
 (0)