Skip to content

Commit 40f2ae9

Browse files
chore: Inject InstrumentationCollector in engine invocation (#370)
Co-authored-by: Jason Luong <[email protected]>
1 parent 67306d4 commit 40f2ae9

File tree

7 files changed

+361
-24
lines changed

7 files changed

+361
-24
lines changed

pkg/analytics/analytics.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,10 @@ func (a *AnalyticsImpl) GetInstrumentation() InstrumentationCollector {
315315
return a.instrumentor
316316
}
317317

318+
func (a *AnalyticsImpl) SetInstrumentation(ic InstrumentationCollector) {
319+
a.instrumentor = ic
320+
}
321+
318322
var DisabledInFedrampErr = errors.New("analytics are disabled in FedRAMP environments") //nolint:errname // breaking API change
319323

320324
// This method sanitizes the given content by searching for key-value mappings. It thereby replaces all keys defined in keysToFilter by the replacement string

pkg/mocks/workflow.go

Lines changed: 9 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/workflow/engine_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,69 @@ func Test_EngineBasics(t *testing.T) {
126126
assert.Equal(t, "callback2", extension["cmd2::hello"])
127127
}
128128

129+
func Test_EngineBasics_InvokeWithCollector(t *testing.T) {
130+
engine := NewWorkFlowEngine(configuration.NewWithOpts())
131+
collector := analytics.NewInstrumentationCollector()
132+
133+
expectedKey := "some"
134+
expectedValue := "value"
135+
136+
test1Id := NewWorkflowIdentifier("test1")
137+
test2Id := NewWorkflowIdentifier("test2")
138+
noOpWorkflowOptions := ConfigurationOptionsFromFlagset(pflag.NewFlagSet("", pflag.ContinueOnError))
139+
input := []Data{NewData(NewTypeIdentifier(test1Id, "input"), "random", nil)}
140+
141+
_, err := engine.Register(test1Id, noOpWorkflowOptions, func(invocation InvocationContext, input []Data) ([]Data, error) {
142+
invocation.GetAnalytics().AddExtensionStringValue(expectedKey, expectedValue)
143+
return invocation.GetEngine().Invoke(test2Id, WithInput(input)) // without specifying the collector, Invoke will use the default
144+
})
145+
assert.NoError(t, err)
146+
147+
_, err = engine.Register(test2Id, noOpWorkflowOptions, func(invocation InvocationContext, input []Data) ([]Data, error) {
148+
invocation.GetAnalytics().AddExtensionStringValue(expectedKey, expectedValue)
149+
return input, nil
150+
})
151+
assert.NoError(t, err)
152+
153+
err = engine.Init()
154+
assert.NoError(t, err)
155+
156+
testcases := []struct {
157+
name string
158+
invokeOptions []EngineInvokeOption
159+
assertCollector analytics.InstrumentationCollector
160+
}{
161+
{
162+
name: "Use specified Instrumentation Collector",
163+
assertCollector: collector,
164+
invokeOptions: []EngineInvokeOption{WithInput(input), WithInstrumentationCollector(collector)},
165+
},
166+
{
167+
name: "Use default Instrumentation Collector",
168+
assertCollector: engine.GetAnalytics().GetInstrumentation(),
169+
invokeOptions: []EngineInvokeOption{WithInput(input)},
170+
},
171+
}
172+
173+
for _, tc := range testcases {
174+
t.Run(tc.name, func(t *testing.T) {
175+
// invoke method under test, case collector is specified
176+
output, err := engine.Invoke(test1Id, tc.invokeOptions...)
177+
assert.NoError(t, err)
178+
assert.Equal(t, input, output)
179+
180+
// ensure the provided collector has the expected data
181+
instrumentationData, err := analytics.GetV2InstrumentationObject(tc.assertCollector)
182+
assert.NoError(t, err)
183+
assert.NotNil(t, instrumentationData)
184+
185+
extension := *instrumentationData.Data.Attributes.Interaction.Extension
186+
assert.Equal(t, expectedValue, extension["test1::some"])
187+
assert.Equal(t, expectedValue, extension["test2::some"])
188+
})
189+
}
190+
}
191+
129192
func Test_EngineRegisterErrorHandling(t *testing.T) {
130193
configuration := configuration.New()
131194
engine := NewWorkFlowEngine(configuration)

pkg/workflow/engineimpl.go

Lines changed: 73 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@ type EngineImpl struct {
3636

3737
var _ Engine = (*EngineImpl)(nil)
3838

39+
type engineRuntimeConfig struct {
40+
config configuration.Configuration
41+
input []Data
42+
ic analytics.InstrumentationCollector
43+
}
44+
45+
type EngineInvokeOption func(*engineRuntimeConfig)
46+
47+
func WithConfig(config configuration.Configuration) EngineInvokeOption {
48+
return func(e *engineRuntimeConfig) {
49+
e.config = config
50+
}
51+
}
52+
53+
func WithInput(input []Data) EngineInvokeOption {
54+
return func(e *engineRuntimeConfig) {
55+
e.input = input
56+
}
57+
}
58+
59+
func WithInstrumentationCollector(ic analytics.InstrumentationCollector) EngineInvokeOption {
60+
return func(e *engineRuntimeConfig) {
61+
e.ic = ic
62+
}
63+
}
64+
3965
func (e *EngineImpl) GetLogger() *zerolog.Logger {
4066
return e.logger
4167
}
@@ -214,26 +240,35 @@ func (e *EngineImpl) GetWorkflow(id Identifier) (Entry, bool) {
214240
return workflow, ok
215241
}
216242

217-
// Invoke invokes the workflow with the given identifier.
218-
func (e *EngineImpl) Invoke(id Identifier) ([]Data, error) {
219-
return e.InvokeWithInputAndConfig(id, []Data{}, nil)
220-
}
221-
243+
// Deprecated: Use Invoke() with WithInput() instead
244+
//
222245
// InvokeWithInput invokes the workflow with the given identifier and input data.
223246
func (e *EngineImpl) InvokeWithInput(id Identifier, input []Data) ([]Data, error) {
224-
return e.InvokeWithInputAndConfig(id, input, nil)
247+
return e.Invoke(id, WithInput(input))
225248
}
226249

250+
// Deprecated: Use Invoke() with WithConfig() instead
251+
//
227252
// InvokeWithConfig invokes the workflow with the given identifier and configuration.
228253
func (e *EngineImpl) InvokeWithConfig(id Identifier, config configuration.Configuration) ([]Data, error) {
229-
return e.InvokeWithInputAndConfig(id, []Data{}, config)
254+
return e.Invoke(id, WithConfig(config))
230255
}
231256

232-
// InvokeWithInputAndConfig invokes the workflow with the given identifier, input data and configuration.
257+
// Deprecated: Use Invoke() with WithInput() and WithConfig() instead
258+
//
259+
// InvokeWithInputAndConfig invokes the workflow with the given identifier, input data, and configuration.
233260
func (e *EngineImpl) InvokeWithInputAndConfig(
234261
id Identifier,
235262
input []Data,
236263
config configuration.Configuration,
264+
) ([]Data, error) {
265+
return e.Invoke(id, WithConfig(config), WithInput(input))
266+
}
267+
268+
// Invoke invokes the workflow with the given identifier.
269+
func (e *EngineImpl) Invoke(
270+
id Identifier,
271+
opts ...EngineInvokeOption,
237272
) ([]Data, error) {
238273
var output []Data
239274
var err error
@@ -249,30 +284,49 @@ func (e *EngineImpl) InvokeWithInputAndConfig(
249284
e.mu.Lock()
250285
e.invocationCounter++
251286

287+
// create default options
288+
options := engineRuntimeConfig{
289+
config: e.config.Clone(),
290+
input: []Data{},
291+
}
292+
293+
// override default options based on optional parameters
294+
for _, opt := range opts {
295+
opt(&options)
296+
}
297+
252298
// prepare logger
253299
prefix := fmt.Sprintf("%s:%d", id.Host, e.invocationCounter)
254-
zlogger := e.logger.With().Str("ext", prefix).Logger()
300+
localLogger := e.logger.With().Str("ext", prefix).Logger()
255301

256302
localUi := e.ui
257-
localAnalytics := NewAnalyticsWrapper(e.analytics, id.Host)
258303

259-
// prepare configuration
260-
if config == nil {
261-
config = e.config.Clone()
304+
var localAnalytics analytics.Analytics
305+
if options.ic != nil {
306+
tmpAnalytics := &analytics.AnalyticsImpl{}
307+
tmpAnalytics.SetInstrumentation(options.ic)
308+
localAnalytics = NewAnalyticsWrapper(tmpAnalytics, id.Host)
309+
} else {
310+
localAnalytics = NewAnalyticsWrapper(e.analytics, id.Host)
262311
}
263312

264313
// prepare networkAccess
265-
networkAccess := e.networkAccess.Clone()
266-
networkAccess.SetConfiguration(config)
314+
localNetworkAccess := e.networkAccess.Clone()
315+
localNetworkAccess.SetConfiguration(options.config)
316+
317+
localEngine := &engineWrapper{
318+
WrappedEngine: e,
319+
defaultInstrumentationCollector: options.ic,
320+
}
267321
e.mu.Unlock()
268322

269323
// create a context object for the invocation
270-
context := NewInvocationContext(id, config, e, networkAccess, zlogger, localAnalytics, localUi)
324+
context := NewInvocationContext(id, options.config, localEngine, localNetworkAccess, localLogger, localAnalytics, localUi)
271325

272326
// invoke workflow through its callback
273-
zlogger.Printf("Workflow Start")
274-
output, err = callback(context, input)
275-
zlogger.Printf("Workflow End")
327+
localLogger.Printf("Workflow Start")
328+
output, err = callback(context, options.input)
329+
localLogger.Printf("Workflow End")
276330
}
277331
} else {
278332
err = fmt.Errorf("workflow '%v' not found", id)

pkg/workflow/enginewrapper.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package workflow
2+
3+
import (
4+
"github.com/rs/zerolog"
5+
6+
"github.com/snyk/go-application-framework/pkg/analytics"
7+
"github.com/snyk/go-application-framework/pkg/configuration"
8+
"github.com/snyk/go-application-framework/pkg/networking"
9+
"github.com/snyk/go-application-framework/pkg/runtimeinfo"
10+
"github.com/snyk/go-application-framework/pkg/ui"
11+
)
12+
13+
type engineWrapper struct {
14+
WrappedEngine Engine
15+
defaultInstrumentationCollector analytics.InstrumentationCollector
16+
}
17+
18+
var _ Engine = (*engineWrapper)(nil)
19+
20+
func (e *engineWrapper) Init() error {
21+
return e.WrappedEngine.Init()
22+
}
23+
24+
func (e *engineWrapper) AddExtensionInitializer(initializer ExtensionInit) {
25+
e.WrappedEngine.AddExtensionInitializer(initializer)
26+
}
27+
28+
func (e *engineWrapper) Register(id Identifier, config ConfigurationOptions, callback Callback) (Entry, error) {
29+
return e.WrappedEngine.Register(id, config, callback)
30+
}
31+
32+
func (e *engineWrapper) GetWorkflows() []Identifier {
33+
return e.WrappedEngine.GetWorkflows()
34+
}
35+
36+
func (e *engineWrapper) GetWorkflow(id Identifier) (Entry, bool) {
37+
return e.WrappedEngine.GetWorkflow(id)
38+
}
39+
40+
// Invoke invokes the workflow with the given identifier.
41+
func (e *engineWrapper) Invoke(id Identifier, opts ...EngineInvokeOption) ([]Data, error) {
42+
options := &engineRuntimeConfig{}
43+
for _, opt := range opts {
44+
opt(options)
45+
}
46+
47+
// if no InstrumentationCollector is specified, and a default is available, the default be used
48+
if options.ic == nil && e.defaultInstrumentationCollector != nil {
49+
opts = append(opts, WithInstrumentationCollector(e.defaultInstrumentationCollector))
50+
}
51+
52+
return e.WrappedEngine.Invoke(id, opts...)
53+
}
54+
55+
// Deprecated: Use Invoke() with WithInput() instead
56+
//
57+
// InvokeWithInput invokes the workflow with the given identifier and input data.
58+
func (e *engineWrapper) InvokeWithInput(id Identifier, input []Data) ([]Data, error) {
59+
return e.Invoke(id, WithInput(input))
60+
}
61+
62+
// Deprecated: Use Invoke() with WithConfig() instead
63+
//
64+
// InvokeWithConfig invokes the workflow with the given identifier and configuration.
65+
func (e *engineWrapper) InvokeWithConfig(id Identifier, config configuration.Configuration) ([]Data, error) {
66+
return e.Invoke(id, WithConfig(config))
67+
}
68+
69+
// Deprecated: Use Invoke() with WithInput() and WithConfig() instead
70+
//
71+
// InvokeWithInputAndConfig invokes the workflow with the given identifier, input data, and configuration.
72+
func (e *engineWrapper) InvokeWithInputAndConfig(id Identifier, input []Data, config configuration.Configuration) ([]Data, error) {
73+
return e.Invoke(id, WithInput(input), WithConfig(config))
74+
}
75+
76+
func (e *engineWrapper) GetAnalytics() analytics.Analytics {
77+
return e.WrappedEngine.GetAnalytics()
78+
}
79+
80+
func (e *engineWrapper) GetNetworkAccess() networking.NetworkAccess {
81+
return e.WrappedEngine.GetNetworkAccess()
82+
}
83+
84+
func (e *engineWrapper) GetConfiguration() configuration.Configuration {
85+
return e.WrappedEngine.GetConfiguration()
86+
}
87+
88+
func (e *engineWrapper) SetLogger(logger *zerolog.Logger) {
89+
e.WrappedEngine.SetLogger(logger)
90+
}
91+
92+
func (e *engineWrapper) SetConfiguration(config configuration.Configuration) {
93+
e.WrappedEngine.SetConfiguration(config)
94+
}
95+
96+
func (e *engineWrapper) GetLogger() *zerolog.Logger {
97+
return e.WrappedEngine.GetLogger()
98+
}
99+
100+
func (e *engineWrapper) GetUserInterface() ui.UserInterface {
101+
return e.WrappedEngine.GetUserInterface()
102+
}
103+
104+
func (e *engineWrapper) SetUserInterface(ui ui.UserInterface) {
105+
e.WrappedEngine.SetUserInterface(ui)
106+
}
107+
108+
func (e *engineWrapper) GetRuntimeInfo() runtimeinfo.RuntimeInfo {
109+
return e.WrappedEngine.GetRuntimeInfo()
110+
}
111+
112+
func (e *engineWrapper) SetRuntimeInfo(ri runtimeinfo.RuntimeInfo) {
113+
e.WrappedEngine.SetRuntimeInfo(ri)
114+
}

0 commit comments

Comments
 (0)