Skip to content

feat(client): add usage reporting #2369

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

Open
wants to merge 62 commits into
base: master
Choose a base branch
from

Conversation

62w71st
Copy link
Contributor

@62w71st 62w71st commented Feb 7, 2025

No description provided.

@62w71st 62w71st requested a review from a team as a code owner February 7, 2025 20:23
@62w71st 62w71st requested a review from a team as a code owner February 22, 2025 21:35
Copy link
Contributor

@jyyi1 jyyi1 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 the contribution! As discussed in the meeting, please:

  • Refactor the newTunnel to accept a Client parameter
  • Parse the reporting config in NewClientWithBaseDialers, we can add new reporting field to type Client struct
  • Start the reporting here or here by reading the parsed reporting config field in Client. Maybe the latter one is better because we will add "Stop reporting" logic in the same file, but this means we need to pass additional reporting config parameter to Tunnel, this is a trade-off.
  • Stop the reporting here

Please also fix the go tests, the CI seems to be failing.

@jyyi1 jyyi1 changed the title (feat) Usage Reporting feat(client): add usage reporting Apr 24, 2025
Copy link
Contributor Author

@62w71st 62w71st left a comment

Choose a reason for hiding this comment

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

PTAL

Copy link
Contributor

@jyyi1 jyyi1 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 the update! The code looks a lot better.

@@ -7,7 +7,7 @@
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// distributed under the License is distributed on an "AS IS BASIS,
Copy link
Contributor

Choose a reason for hiding this comment

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

Plz revert

Suggested change
// distributed under the License is distributed on an "AS IS BASIS,
// distributed under the License is distributed on an "AS IS" BASIS,

if err != nil {
return nil, &platerrors.PlatformError{
Code: platerrors.InvalidConfig,
Message: "config is not valid YAML",
Copy link
Contributor

@jyyi1 jyyi1 May 16, 2025

Choose a reason for hiding this comment

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

Make the error message different from the previous one so the user knows what's going wrong.

Suggested change
Message: "config is not valid YAML",
Message: "client config is not valid YAML",

if errors.Is(err, errors.ErrUnsupported) {
return nil, &platerrors.PlatformError{
Code: platerrors.InvalidConfig,
Message: "unsupported config",
Copy link
Contributor

@jyyi1 jyyi1 May 16, 2025

Choose a reason for hiding this comment

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

Same as above.

Suggested change
Message: "unsupported config",
Message: "unsupported client config",

@@ -101,5 +126,31 @@ func NewClientWithBaseDialers(transportConfig string, tcpDialer transport.Stream
}
}

return &Client{sd: transportPair.StreamDialer, pl: transportPair.PacketListener}, nil
usageReportYAML, err := config.ParseConfigYAML(sessionConfig)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this sessionConfig specific to sessionReportingConfig? Or can it be used for other types of sessionConfig in the future? If it's the latter, we should probably implement type assertion to ensure the type is 'reporting'.

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 easiest is to move the parsing of the clientConfig to the config module, which already takes care of this. And it allows you to use first_supported

}
}
}
fmt.Println("usageReporter", usageReporter)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this for debugging purpose or logging purpose?

Name: cookieParts[0],
Value: cookieParts[1],
})
fmt.Println("Cookie found:", cookie)
Copy link
Contributor

Choose a reason for hiding this comment

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

logging/debugging?

func loadCookies(jar http.CookieJar, filename string) ([]*url.URL, error) {
// Read cookies from the file
file, err := os.Open(filename)
if os.IsNotExist(err) {
Copy link
Contributor

Choose a reason for hiding this comment

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

According to Go doc:

This function predates errors.Is. It only supports errors returned by the os package. New code should use errors.Is(err, fs.ErrNotExist).

Suggested change
if os.IsNotExist(err) {
if errors.Is(err, fs.ErrNotExist) {

}

// Save the cookies data to a file
file, err := os.Create(filename)
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 discussed for privacy, do we need to allow the user to delete this cookie file (though removing the key or any other methods)?

Copy link
Contributor

Choose a reason for hiding this comment

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

The cookie can be deleted when the key is deleted or when the Outline app is uninstalled. Do you want to add the option to be able to delete cookies manually?
@62w71st @fortuna

}
case <-ctx.Done():
// Stop reporting when the context is canceled
fmt.Println("Stopping reporting...")
Copy link
Contributor

Choose a reason for hiding this comment

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

debugging?

case <-ticker.C:
err := Report(tcp, ur.Url)
if err != nil {
fmt.Printf("Report failed: %v\n", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Plz use slog for logging?


// Stop reporting on the Client instance
if (this.outlineClient != null) {
this.outlineClient.stopReporting();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Move to tearDownActiveTunnel. But I wonder if we should just implement that in stopRemoteDevice instead and revert this file. Why would we not align them?

@@ -41,6 +45,10 @@ func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) {
return c.pl.ListenPacket(ctx)
}

func (c *Client) GetUsageReporter() *config.UsageReporter {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this needed? You already provide Start/StopReporting. Remove?

// NewClient creates a new Outline client from a configuration string.
func NewClient(transportConfig string) *NewClientResult {
func NewClient(transportConfig string, sessionConfig string) *NewClientResult {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not a session config. It's a report config. But we should merge them into a client config.

if err != nil {
return &NewClientResult{Error: platerrors.ToPlatformError(err)}
}
return &NewClientResult{Client: client}
}

func NewClientWithBaseDialers(transportConfig string, tcpDialer transport.StreamDialer, udpDialer transport.PacketDialer) (*Client, error) {
func NewClientWithBaseDialers(transportConfig string, sessionConfig string, tcpDialer transport.StreamDialer, udpDialer transport.PacketDialer) (*Client, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same

@@ -101,5 +126,31 @@ func NewClientWithBaseDialers(transportConfig string, tcpDialer transport.Stream
}
}

return &Client{sd: transportPair.StreamDialer, pl: transportPair.PacketListener}, nil
usageReportYAML, err := config.ParseConfigYAML(sessionConfig)
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 easiest is to move the parsing of the clientConfig to the config module, which already takes care of this. And it allows you to use first_supported

Comment on lines 55 to 56
transport: string;
session_report: string;
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
transport: string;
session_report: string;
client: string;

defer conn.Close()

// Create an HTTP POST request with cookies and a sample body
requestBody := "param1=value1&param2=value2"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please use the http.Client instead of rolling out your own.

}

// StartReporting calls the Report function at every internal.
func StartReporting(ctx context.Context, tcp transport.StreamDialer, ur *config.UsageReporter) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Cookies must be siloed across access keys. You must take into account the service id somehow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Current, cookies for all keys belonging to a provider are sent in the request. The provider should be able to dedupe them. Can you explain the requirement that motivates separating these cookies per key?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Each access key must have their own separate cookie jar to prevent possible cross-service leaks. It's a security requirement. Imagine two separate services talking to the same reporting service, for example.

Also, you need to delete the cookies when you delete the key, and this makes it easier, because you don't know the domains it needs to talk to.

@fortuna
Copy link
Collaborator

fortuna commented Jun 3, 2025

Please pull the latest master

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