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

Read module dependencies from debug.BuildInfo instead of go.mod #199

Merged
merged 11 commits into from
Apr 17, 2020

Conversation

wingyplus
Copy link
Contributor

This changes getting modules from runtime/debug instead of getting it from go.mod or vendor/modules.txt.

Screen Shot 2563-04-15 at 00 22 05

Fixes #184

@wingyplus
Copy link
Contributor Author

wingyplus commented Apr 14, 2020

From the image above, the sentry module that's mark as version 0.0.0 is replace module. I'm not sure how do we display this kind of module in sentry. The idea that I think will be add remark after version such as v0.0.0 (replaced by ).

@rhcarvalho
Copy link
Contributor

@wingyplus thanks for your contribution!

Could you please update go.mod to go 1.12 -- debug.ReadBuildInfo requires 1.12 and that's the minimum version we support today anyway.

Copy link
Contributor

@rhcarvalho rhcarvalho left a comment

Choose a reason for hiding this comment

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

Thanks for contributing ❤️ !
Good start, I have a feel suggestions below.

I noticed this integration went in and never got proper tests. If that's something you'd like to contribute too, would be very welcome. Specially now, testing it got much simpler since we don't depend on parsing external files.

The basic test cases would be around the kinds of output debug.ReadBuildInfo() can return: no replacements, replacements with mod+version, absolute path, relative path.

integrations.go Outdated
if fileExists("vendor/modules.txt") {
return getModulesFromVendorTxt()
}

if fileExists("vendor/vendor.json") {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is AFAICT used by the old govendor tool.
There is no reason for us to keep supporting that, govendor being only one among other once popular tools from the past (like dep, godep, glide, ...).

Go Modules are the present and the future, we can concentrate on that use case. Prior to Modules, there are so many ways to track dependencies and versions that we can't reasonably support everything. Users can still roll their own integration to capture this info if needed and not using Modules.

Would you like to have the pleasure to delete this portion of the code too?

In doing so, you'll likely realize there is no need for 3 functions: extractModules, getModules and getModulesFromBuildInfo -- we can move everything into one.

integrations.go Outdated
func getModulesFromBuildInfo() (map[string]string, error) {
info, ok := debug.ReadBuildInfo()
if !ok {
return nil, fmt.Errorf("cannot dependencies from debug.BuildInfo")
Copy link
Contributor

Choose a reason for hiding this comment

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

As we can see from #197, returning an error here will only confuse users.

The case when ok = False is when the main package is built with GO111MODULE=off, so there's nothing the integration can do anyway.

We can log a message to help with debugging "why package/module information is not appearing in Sentry?" but the message should not be about a "failure", but rather say this integration is disabled when build is not using Go Modules.

We can use a similar message as documented in https://golang.org/pkg/runtime/debug/#ReadBuildInfo:

"The Modules integration is not available in binaries built without module support."

integrations.go Outdated
Comment on lines 71 to 73
modules := make(map[string]string)
for _, dep := range info.Deps {
modules[dep.Path] = dep.Version
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs to include info.Main too 😉


As you well noted in a comment, we also need to handle replacements. The Sentry server only takes a map[string]string for package/module name => version, no concept of replacements. While we could look into adding replacements server-side and in the web interface, I think we can well start with just reporting replacements within the current server constraints.

There are replacements pointing to a module+version, and replacements pointing to absolute local path and relative local path, see https://github.com/golang/go/wiki/Modules#when-should-i-use-the-replace-directive.

We could use a syntax similar to how go list -m all prints replacements:

module/name v0.0.0 => replacement
map[string]string{"module/name": "v0.0.0 => replacement"}

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it sound good to me for doing this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@wingyplus
Copy link
Contributor Author

wingyplus commented Apr 15, 2020

@rhcarvalho Thanks for your review. Main issue was adressed.

The basic test cases would be around the kinds of output debug.ReadBuildInfo() can return: no replacements, replacements with mod+version, absolute path, relative path.

Integration tests would a bit hard because debug.ReadBuildInfo didn't inject modules in test mode (see golang/go#33976). Mocking debug.ReadBuildInfo might be a bit easier. The illustrate that I think is:

var readBuildInfo = debug.ReadBuildInfo // this way we can inject a function that returns a modules list. 

func extractModules(){
  info, err := readBuildInfo()
  // ....
}

Do you have any suggestion?

Copy link
Contributor

@rhcarvalho rhcarvalho left a comment

Choose a reason for hiding this comment

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

Mocking debug.ReadBuildInfo might be a bit easier. The illustrate that I think is:

var readBuildInfo = debug.ReadBuildInfo // this way we can inject a function that returns a modules list. 

func extractModules(){
  info, err := readBuildInfo()
  // ....
}

Do you have any suggestion?

What you wrote works. There is one drawback that is global state, and tests could not run in parallel if we ever wanted to.

I do have a suggestion for an alternative design. Here's a bit of a skeleton:

func extractModules(*debug.BuildInfo) map[string]string {
    // ...
}

// ...
info, ok := debug.ReadBuildInfo()
// ...
_ = extractModules(info)
func Test...(t *testing.T) {
    tests := []...
    // ...

    for _, tt := range tests {
        tt := tt
        // ...
        t.Run( //...
           got := extractModules(tt.info)
           if diff := cmp.Diff(tt.want, got); diff != "" {
                // ...
           }
        })
    }
}

With this we can have several test cases that each define the input as a *debug.BuildInfo and the corresponding expected map[string]string. This way we are back to simple input-output comparisons and no need for any mocks or other supporting code.

What do you think?

integrations.go Outdated
for _, dep := range info.Deps {
ver := dep.Version
if dep.Replace != nil {
ver += fmt.Sprintf(" => %s %s", dep.Replace.Path, dep.Version)
Copy link
Contributor

Choose a reason for hiding this comment

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

When the replacement is coming from a local path, there is no version (empty string).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is my mistake. It should be dep.Replace.Version not dep.Version.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rhcarvalho Fixed

@rhcarvalho rhcarvalho added this to Review in progress in Sentry SDK for Go Apr 15, 2020
@wingyplus
Copy link
Contributor Author

I do have a suggestion for an alternative design. Here's a bit of a skeleton:

func extractModules(*debug.BuildInfo) map[string]string {
// ...
}

// ...
info, ok := debug.ReadBuildInfo()
// ...
_ = extractModules(info)

func Test...(t *testing.T) {
tests := []...
// ...

for _, tt := range tests {
    tt := tt
    // ...
    t.Run( //...
       got := extractModules(tt.info)
       if diff := cmp.Diff(tt.want, got); diff != "" {
            // ...
       }
    })
}

}

With this we can have several test cases that each define the input as a *debug.BuildInfo and the corresponding expected map[string]string. This way we are back to simple input-output comparisons and no need for any mocks or other supporting code.

What do you think?

@rhcarvalho That's great idea! Let's try.

@rhcarvalho rhcarvalho added this to the v0.6.0 milestone Apr 17, 2020
Copy link
Contributor

@rhcarvalho rhcarvalho left a comment

Choose a reason for hiding this comment

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

@wingyplus awesome PR. Thank you!! 🥳

Going to release this into 0.6.0.

info: &debug.BuildInfo{
Main: debug.Module{
Path: "my/module",
Version: "(devel)",
Copy link
Contributor

Choose a reason for hiding this comment

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

I just realized that at the moment the Version of the main module (in fact, the module that contains package main) is always "(devel)". That is unfortunate, but at least the module name will be in Sentry. Proper version can be added using the Releases feature.

golang/go#29228

Comment on lines +339 to +341
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf("modules info mismatch (-want +got):\n%s", diff)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I took the liberty to change this to use cmp.Diff to make it easier to spot problems when tests fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@rhcarvalho I think we can turn assertEquals to uses cmp.Diff to display the diff when it's not equal. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see the point, as it just limits the usability. Personally I prefer calling cmp.Diff directly, it is more clear and flexible.

assertEquals always calls t.Fatal*, even when t.Error* would be more appropriate. Moving cmp.Diff into assertEquals would also limit control of comparison options.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, I see. Thanks for your point.

@rhcarvalho rhcarvalho merged commit 5747ddf into getsentry:master Apr 17, 2020
Sentry SDK for Go automation moved this from Review in progress to Done Apr 17, 2020
@wingyplus
Copy link
Contributor Author

@rhcarvalho thanks!! 😃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
2 participants