Skip to content
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

Add listeners to trace file metadata #571

Merged
merged 1 commit into from
Feb 14, 2024

Conversation

jhrotko
Copy link
Collaborator

@jhrotko jhrotko commented Feb 12, 2024

Description

This PR intends to retrieve metadata from the parsing file. This was an issue when tracking the occurrences of extends in a project for telemetry since at the time the metrics are sent the Project is fully resolved thus no extends instances.

This will be one out of three PRs.
docker/compose#11492

Interaction

sequenceDiagram
    participant pinata
    participant compose
    participant compose-go
    compose->>compose-go: register listeners 
    compose->>+compose-go: load project
    compose-go->>+compose-go/extends: ApplyExtends
   Note right of compose-go/extends: options.ProcessEvent("extends", metadata)
    compose-go/extends-->>-compose-go: finish extends
    compose-go-->>-compose: finish load
    compose-->>pinata: send OTEL metrics
Loading

Fix

by adding the listener to increment the number of processed extends, found a bug where when going down the search tree, we were not updating the leafs after the merge. In a case where we had chained extends, we would over look the tree, since we would only update the services at the head of the recursion in ApplyExtends

@jhrotko jhrotko force-pushed the process-event-loader branch 4 times, most recently from 9bafbd8 to 05698d5 Compare February 13, 2024 15:24
@jhrotko jhrotko marked this pull request as ready for review February 13, 2024 15:24
@jhrotko jhrotko requested a review from ndeloof as a code owner February 13, 2024 15:24
Copy link
Collaborator

@ndeloof ndeloof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some design suggestion for more flexibility by consumer

Request changes on Project: it should NOT own FileMetadata, leave use of parsing events to the consumer - here you want to count extends, but there's other scenario we want to list files being processed, or ... mode details TBD

types/project.go Outdated
@@ -58,6 +58,18 @@ type Project struct {
// DisabledServices track services which have been disable as profile is not active
DisabledServices Services `yaml:"-" json:"-"`
Profiles []string `yaml:"-" json:"-"`

// Metadata for telemetry
FileMetada *FileMetada `yaml:"-" json:"-"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as Listener allows to track events during parsing, Project should not need to host such a metadata attribute. Just let the consumer manage collecting metrics with it's own data structure

cli/options.go Outdated

// Callbacks to retrieve metadata information during parse defined before
// creating the project
Listeners map[string][]loader.Listener
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes it a bit complicated to write a listener for more than one event type. Could declare Listeners []Listener as Listener is a func(event string, details any, it already receives the event type, and writing a listener then is just a question of a switch and can select any interesting event and ignore all others.

Copy link
Collaborator Author

@jhrotko jhrotko Feb 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ndeloof by writing the way you are suggesting, you are passing the map functionality inside every registered listener by writing in all further listeners with a switch. When in reality with the current solution we take advantage of the map data structure to handle this. Also, when processing 1 listener, with your recomendation, you will need to iterate over all listeners, when in reality you just care for a subset of them. I can see one advantage in having a []Listener instead of map[string][]Listener which is, in case you want to have a particular Listener that is triggered for all events

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the other way: if I want to collect multiple events, with your design I'll have to register mutliple listeners for all the events I'm interested in, while with a generic one I have a native filtering capability using switch:

func listener(event string ...) {
  switch event {
  case "extends": 
     countExtends++
  case "include": 
     countInclude++
  case "env_file": 
     countEnvFile++
}

Also, if you're interested by a single event type, we can offer a Filter:

cli.WithListener( cli.FilterEvent("extends", func(...)) )

by the way, Listener doesn't need to be a slice, could be a single reference, and we can offer a composition helper

cli.WithListener( cli.CombineListeners(func(...), func(...), func(...)) )

tl;dr keep the original design simple/naive and offer helpers to give more flexibility to the consumer

@@ -60,6 +60,8 @@ func applyServiceExtends(ctx context.Context, name string, services map[string]a
if !ok {
return s, nil
}
opts.ProcessEvent("extends", name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in addition to service name, would be useful to track the file so we can detect all files involved in a compose model
Listener's metadata parameter could be declared a map[string]any so it is easy to pass more than just one attribute here, and add more as this evolves without impacting consumers

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was a leftover from a test :) I intended to pass the extends (the whole extends metadata from the service) I will change this. I left a any in case for some reason in the future someone does not want to necessarily pass a map[string]any. What do you think @ndeloof ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the benefit of a map[string]any is that this allows to "add more" without impact on previously reported keys. Using any the consumer would need to adjust to any change that's made to the original struct

options.Listeners = map[string][]Listener{
"extends": {func(event string, metadata any) {
extendsCount++
}},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}},
options.Listeners = []Listeners{
func(event string, metadata map[string]any {
if event == "extends" {
count ++
}
}},

Copy link
Member

@milas milas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with Nicolas' suggestions - I think a generic listener that gets the event type + metadata is fine, consumers can ignore what they don't care about. Other than that and last cleanup though, lgtm!

@jhrotko jhrotko force-pushed the process-event-loader branch from 05698d5 to a6e592d Compare February 14, 2024 12:14
Copy link
Collaborator

@ndeloof ndeloof left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

cli/options.go Outdated
Comment on lines 374 to 375
func (o *ProjectOptions) WithListener(listeners []loader.Listener) {
o.Listeners = listeners
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Better make this a appending function so it can be used multiple times

Suggested change
func (o *ProjectOptions) WithListener(listeners []loader.Listener) {
o.Listeners = listeners
func (o *ProjectOptions) WithListener(listener loader.Listener) {
o.Listeners = append(o.Listeners, listener)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jhrotko jhrotko force-pushed the process-event-loader branch from a6e592d to 0411918 Compare February 14, 2024 13:18
@jhrotko jhrotko force-pushed the process-event-loader branch from 0411918 to 6e4fb2b Compare February 14, 2024 13:24
@@ -480,6 +491,13 @@ func withConvertWindowsPaths(options *ProjectOptions) func(*loader.Options) {
}
}

// save listeners from ProjectOptions (compose) to loader.Options
func withListener(options *ProjectOptions) func(*loader.Options) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there could be more than one listener to save

Suggested change
func withListener(options *ProjectOptions) func(*loader.Options) {
func withListeners(options *ProjectOptions) func(*loader.Options) {

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed. I plan to add include metric support using this mechanism so will fix this at same time in a follow-up PR

@ndeloof ndeloof merged commit 26a5dd3 into compose-spec:main Feb 14, 2024
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants