Skip to content

Commit 402304a

Browse files
committed
chore: Send instrumentation data on kill
chore: refactor chore: refactor teardown logic
1 parent 8c9a419 commit 402304a

File tree

2 files changed

+142
-97
lines changed

2 files changed

+142
-97
lines changed

cliv2/cmd/cliv2/instrumentation.go

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

cliv2/cmd/cliv2/main.go

Lines changed: 73 additions & 97 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,52 @@ 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+
492+
errorList, err = processError(err, errorList)
493+
494+
for _, tempError := range errorList {
495+
if tempError != nil {
496+
cliAnalytics.AddError(tempError)
497+
}
498+
}
499+
}
500+
501+
exitCode := cliv2.DeriveExitCode(err)
502+
globalLogger.Printf("Deriving Exit Code %d (cause: %v)", exitCode, err)
503+
504+
displayError(err, globalEngine.GetUserInterface(), globalConfiguration, ctx)
505+
506+
updateInstrumentationDataBeforeSending(cliAnalytics, startTime, ua, exitCode)
507+
508+
if !globalConfiguration.GetBool(configuration.ANALYTICS_DISABLED) {
509+
sendAnalytics(cliAnalytics, globalLogger)
510+
}
511+
sendInstrumentation(globalEngine, cliAnalytics.GetInstrumentation(), globalLogger)
512+
513+
// cleanup resources in use
514+
// WARNING: deferred actions will execute AFTER cleanup; only defer if not impacted by this
515+
if _, cleanupErr := globalEngine.Invoke(basic_workflows.WORKFLOWID_GLOBAL_CLEANUP); cleanupErr != nil {
516+
globalLogger.Printf("Failed to cleanup %v", cleanupErr)
517+
}
518+
519+
if globalConfiguration.GetBool(configuration.DEBUG) {
520+
writeLogFooter(exitCode, errorList, globalConfiguration, networkAccess)
521+
}
522+
523+
return exitCode
524+
}
525+
551526
func MainWithErrorCode() int {
552527
initDebugBuild()
553528

554529
errorList := []error{}
555530
errorListMutex := sync.Mutex{}
531+
var tearDownOnce sync.Once
532+
var finalExitCode int
556533

557534
startTime := time.Now()
558535
var err error
@@ -656,6 +633,27 @@ func MainWithErrorCode() int {
656633
cliAnalytics.GetInstrumentation().SetStage(instrumentation.DetermineStage(cliAnalytics.IsCiEnvironment()))
657634
cliAnalytics.GetInstrumentation().SetStatus(analytics.Success)
658635

636+
// Set up signal handling to send instrumentation on premature termination
637+
signalChan := make(chan os.Signal, 1)
638+
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
639+
go func() {
640+
sig := <-signalChan
641+
globalLogger.Printf("Received signal %v, attempting to send instrumentation before exit", sig)
642+
643+
// TODO: Replace with proper error catalog error for signal termination
644+
signalError := cli.NewGeneralCLIFailureError(fmt.Sprintf("terminated by signal: %v", sig))
645+
signalError.ErrorCode = "SNYK-CLI-0025"
646+
647+
tearDownOnce.Do(func() {
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+
659657
setTimeout(globalConfiguration, func() {
660658
os.Exit(constants.SNYK_EXIT_CODE_EX_UNAVAILABLE)
661659
})
@@ -681,40 +679,18 @@ func MainWithErrorCode() int {
681679
// ignore
682680
}
683681

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-
}
692-
}
693-
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)
682+
// Stop signal handling before cleanup to prevent race conditions
683+
signal.Stop(signalChan)
700684

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-
}
685+
tearDownOnce.Do(func() {
686+
errorListMutex.Lock()
687+
errorListCopy := append([]error{}, errorList...)
688+
errorListMutex.Unlock()
712689

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

717-
return exitCode
693+
return finalExitCode
718694
}
719695

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

0 commit comments

Comments
 (0)