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

Capability to manually create spans #956

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

xBis7
Copy link

@xBis7 xBis7 commented Sep 25, 2024

opentelemetry-plugin issue: #90

This PR is based on the work done on PR #84.

The patch is adding a new step withNewSpan which allows users to create their own spans.

The option accepts 3 parameters

  • label
    • name of the span, string value
  • attributes
    • list of attributes, same as withSpanAttributes
  • setAttributesOnlyOnParent
    • whether to inherit the defined attributes to children spans or not, boolean value
    • all defined attributes are inherited by default

From the above 3 parameters, only the label is required and everything else is optional.

Example usage

withNewSpan(label: 'custom-build-span', attributes: ([
  spanAttribute(key: 'modules', value: '2'),
  spanAttribute(key: 'command', value: 'mvn clean install')
]), setAttributesOnlyOnParent: true) {
  sh './build-module1.sh'
  sh './build-module2.sh'
}

or

withNewSpan(label: 'custom-build-span', attributes: ([
  spanAttribute(key: 'modules', value: '2'),
  spanAttribute(key: 'command', value: 'mvn clean install')
])) {
  sh './build-module1.sh'
  sh './build-module2.sh'
}

or

withNewSpan(label: 'custom-build-span') {
  sh './build-module1.sh'
  sh './build-module2.sh'
}
  • The new step was named withNewSpan to be consistent with the naming of other steps like withSpanAttributes
  • For the step execution, SpanAttributeStepExecution was reused
  • Added a parameter so that the user can control whether the defined attributes should be inherited on children spans of not

Testing done

  • An integration test was added for testing the overall usage with different pipeline scripts
  • A unit test was added for testing the changes in MonitorPipelineListener
    • It's also testing the code logic for not inheriting the new span attributes to children spans
  • The patch was also tested manually with jenkins and jaeger

Screenshot from manual testing

withNewSpan_jaeger_testing

Submitter checklist

  • Make sure you are opening from a topic/feature/bugfix branch (right side) and not your main branch!
  • Ensure that the pull request title represents the desired changelog entry
  • Please describe what you did
  • Link to relevant issues in GitHub or Jira
  • Link to relevant pull requests, esp. upstream and downstream changes
  • Ensure you have provided tests - that demonstrates feature works or fixes the issue

@xBis7
Copy link
Author

xBis7 commented Sep 25, 2024

@kuisathaverat @cyrille-leclerc Can you take a look at this PR?

It's related to this issue #90

// install() returns a Future for every plugin. Call get() on the Future so that this line blocks
// until the operation is finished and the future is available.
jenkinsRule.jenkins.getPluginManager().install(
Collections.singletonList("durable-task"), true).get(0).get();
Copy link
Author

@xBis7 xBis7 Sep 25, 2024

Choose a reason for hiding this comment

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

Without this change, most of the integration tests are failing to start.

@xBis7
Copy link
Author

xBis7 commented Sep 30, 2024

@kuisathaverat Did you get a chance to take a look?

Copy link
Member

@v1v v1v left a comment

Choose a reason for hiding this comment

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

I've just added a comment without reviewing the implementation but the syntax and usability


BTW, thanks so much for your contribution 💯 🙏

Comment on lines +85 to +87
withNewSpan(label: 'custom-build-span', attributes: ([
spanAttribute(key: 'modules', value: '2'),
spanAttribute(key: 'command', value: 'mvn clean install')
Copy link
Member

Choose a reason for hiding this comment

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

Out of curiosity, could this be less verbose as a key and value instead of a spanAttribute object?

Something like the below pseudo-code:

 withNewSpan(label: 'custom-build-span', attributes: ([
            'modules': '2',
            'command': 'mvn clean install'])

The declarative syntax might benefit from a shorter syntax. If users decide to use a bunch of attributes, with the current proposal, there will be 26 chars extra per entry.

What do you think?

Copy link
Author

Choose a reason for hiding this comment

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

Hi @v1v, thanks for the review!

could this be less verbose as a key and value instead of a spanAttribute object?

Not at the moment, but it's doable.

In my current approach, I'm reusing the code for the SpanAttributeStepExecution and it works the same way as in WithSpanAttributesStep which has this constructor

    @DataBoundConstructor
    public WithSpanAttributesStep(List<SpanAttribute> spanAttributes) {
        this.spanAttributes = spanAttributes;
    }

for that reason, the WithNewSpanStep also accepts a List<SpanAttribute>.

Because it's annotated with @DataBoundConstructor, you can't overload it but if the parameter type changes to Object attributes, it can also accept a map and that would achieve the behavior that you want. We can convert the map to a List<SpanAttribute> and keep reusing the existing code.

The question is, do we want to make the change only in WithNewSpanStep or should we make it in WithSpanAttributesStep to make the change more global and keep the behavior consistent?

Copy link
Author

Choose a reason for hiding this comment

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

I did some testing and it turns out that it's not that straight forward. Jenkins doesn't like the type-cast from Object to a SpanAttribute element of a List. No issues with using a Map, we can get the values and then create a List<SpanAttribute> but that will break consistency with the other steps.

Copy link
Author

Choose a reason for hiding this comment

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

Hi @v1v, any thoughts on this?

Copy link
Member

Choose a reason for hiding this comment

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

Thanks Christos for your analysis. It's totally fine

@kuisathaverat
Copy link
Contributor

kuisathaverat commented Oct 16, 2024

Sorry for not getting back to you sooner, but I did not find time to perform the manual test needed to verify that there are no regressions. Also, I want to make a huge pipeline test to verify there are no leaks or performance issues.

@xBis7
Copy link
Author

xBis7 commented Oct 16, 2024

@kuisathaverat Is there something I can do to help? Run some test? I've run manual tests but my use-cases might not match yours.

@kuisathaverat
Copy link
Contributor

kuisathaverat commented Oct 16, 2024

I need more time. I have compared two "similar" pipelines to see if the memory usage grows, and it does.

A pipeline with 500 steps uses about 450MB of RAM and does not increase memory usage after it is finished. I executed it 10 times.

pipeline {
  agent any
  stages {
    stage('prepare') {
      steps {
        bunchOfSteps(0)
      }
    }
  }
}

def bunchOfSteps(level) {
    if (level < 100) {
        sh "echo level ${level}"
        bunchOfSteps(level + 1)
    }
}

A pipeline with 500 steps and the new step to create spans uses about 850MB of RAM and increases the memory usage after it finishes about 350MB. I executed it one time.

pipeline {
  agent any
  stages {
    stage('prepare') {
      steps {
        bunchOfSteps(0)
      }
    }
  }
}

def bunchOfSteps(level) {
    if (level < 500) {
        withNewSpan(label: 'custom-build-span-' + level, attributes: ([
            spanAttribute(key: 'level', value: level),
            ]), setAttributesOnlyOnParent: true) {
                sh "echo level ${level}"
                bunchOfSteps(level + 1)
            }
    }
}

@xBis7
Copy link
Author

xBis7 commented Oct 16, 2024

It would be interesting to compare the RAM usage between

pipeline {
  agent any
  stages {
    stage('prepare') {
      steps {
        bunchOfSteps(0)
      }
    }
  }
}

def bunchOfSteps(level) {
    if (level < 500) {
        withSpanAttributes([
            spanAttribute(key: 'level', value: level),
            ]) {
                sh "echo level ${level}"
                bunchOfSteps(level + 1)
            }
    }
}
pipeline {
  agent any
  stages {
    stage('prepare') {
      steps {
        bunchOfSteps(0)
      }
    }
  }
}

def bunchOfSteps(level) {
    if (level < 500) {
        withNewSpan(label: 'custom-build-span-' + level, attributes: ([
            spanAttribute(key: 'level', value: level),
            ]), setAttributesOnlyOnParent: true) {
                sh "echo level ${level}"
                bunchOfSteps(level + 1)
            }
    }
}

withSpanAttributes might also increase the RAM usage afterwards.

@kuisathaverat
Copy link
Contributor

kuisathaverat commented Oct 16, 2024

this one directly crash after 250 steps, but I not see the memory change after several executions

def bunchOfSteps(level) {
    if (level < 500) {
        withSpanAttributes([
            spanAttribute(key: 'level', value: level),
            ]) {
                sh "echo level ${level}"
                bunchOfSteps(level + 1)
            }
    }
}

@kuisathaverat
Copy link
Contributor

I am going to test this one too

def bunchOfSteps(level) {
    if (level < 500) {
        withNewSpan(label: 'custom-build-span-' + level, setAttributesOnlyOnParent: true) {
                sh "echo level ${level}"
                bunchOfSteps(level + 1)
            }
    }
}

@kuisathaverat
Copy link
Contributor

I am going to change the approach recursivity 500 it is a lite too much for a pipeline

@kuisathaverat
Copy link
Contributor

pipeline {
  agent any
  stages {
    stage('prepare') {
      steps {
        bunchOfSteps(0)
      }
    }
  }
}

def bunchOfSteps(level) {
    if (level < 10) {
        recursiveSteps(0)
        bunchOfSteps(level + 1)
    }
}

def recursiveSteps(level){
    if (level < 50) {
        withNewSpan(label: 'custom-build-span-' + level, attributes: ([
            spanAttribute(key: 'level', value: level),
            ]), setAttributesOnlyOnParent: true) {
                sh "echo level ${level}"
                recursiveSteps(level + 1)
            }
    }
}

@kuisathaverat
Copy link
Contributor

I am also testing

pipeline {
  agent any
  stages {
    stage('prepare') {
      steps {
      withNewSpan(label: 'custom-build-span', attributes: ([
        spanAttribute(key: 'level', value: 'foo'),
        ]), setAttributesOnlyOnParent: false) {
            bunchOfSteps(0)
        }
      }
    }
  }
}

def bunchOfSteps(level) {
    if (level < 500) {
        sh "echo level ${level}"
        bunchOfSteps(level + 1)
    }
}

@kuisathaverat
Copy link
Contributor

I need to grab some head dumps and verify there is nothing weird.

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.

3 participants