Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit a74158f

Browse files
authored
Merge pull request #38 from cdr/ssh-activity
Usage metric pushing
2 parents c2eda4a + 9b2232d commit a74158f

File tree

7 files changed

+116
-19
lines changed

7 files changed

+116
-19
lines changed

cmd/coder/shell.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"go.coder.com/cli"
1717
"go.coder.com/flog"
1818

19+
"cdr.dev/coder-cli/internal/activity"
1920
"cdr.dev/wsep"
2021
)
2122

@@ -145,7 +146,10 @@ func runCommand(ctx context.Context, envName string, command string, args []stri
145146
go func() {
146147
stdin := process.Stdin()
147148
defer stdin.Close()
148-
_, err := io.Copy(stdin, os.Stdin)
149+
150+
ap := activity.NewPusher(entClient, env.ID, sshActivityName)
151+
wr := ap.Writer(stdin)
152+
_, err := io.Copy(wr, os.Stdin)
149153
if err != nil {
150154
cancel()
151155
}
@@ -168,3 +172,5 @@ func runCommand(ctx context.Context, envName string, command string, args []stri
168172
}
169173
return err
170174
}
175+
176+
const sshActivityName = "ssh"

cmd/coder/sync.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ func (cmd *syncCmd) Run(fl *pflag.FlagSet) {
6969
}
7070

7171
s := sync.Sync{
72-
Init: cmd.init,
73-
Environment: env,
74-
RemoteDir: remoteDir,
75-
LocalDir: absLocal,
76-
Client: entClient,
72+
Init: cmd.init,
73+
Env: env,
74+
RemoteDir: remoteDir,
75+
LocalDir: absLocal,
76+
Client: entClient,
7777
}
7878
for err == nil || err == sync.ErrRestartSync {
7979
err = s.Run()

cmd/coder/url.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import (
1111
"go.coder.com/flog"
1212
)
1313

14-
type urlCmd struct {
15-
}
14+
type urlCmd struct{}
1615

1716
type DevURL struct {
1817
Url string `json:"url"`

internal/activity/pusher.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package activity
2+
3+
import (
4+
"time"
5+
6+
"cdr.dev/coder-cli/internal/entclient"
7+
"go.coder.com/flog"
8+
"golang.org/x/time/rate"
9+
)
10+
11+
const pushInterval = time.Minute
12+
13+
// Pusher pushes activity metrics no more than once per pushInterval. Pushes
14+
// within the same interval are a no-op.
15+
type Pusher struct {
16+
envID string
17+
source string
18+
19+
client *entclient.Client
20+
rate *rate.Limiter
21+
}
22+
23+
func NewPusher(c *entclient.Client, envID, source string) *Pusher {
24+
return &Pusher{
25+
envID: envID,
26+
source: source,
27+
client: c,
28+
rate: rate.NewLimiter(rate.Every(pushInterval), 1),
29+
}
30+
}
31+
32+
func (p *Pusher) Push() {
33+
if !p.rate.Allow() {
34+
return
35+
}
36+
37+
err := p.client.PushActivity(p.source, p.envID)
38+
if err != nil {
39+
flog.Error("push activity: %s", err.Error())
40+
}
41+
}

internal/activity/writer.go

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package activity
2+
3+
import "io"
4+
5+
type activityWriter struct {
6+
p *Pusher
7+
wr io.Writer
8+
}
9+
10+
func (w *activityWriter) Write(p []byte) (n int, err error) {
11+
w.p.Push()
12+
return w.wr.Write(p)
13+
}
14+
15+
func (p *Pusher) Writer(wr io.Writer) io.Writer {
16+
return &activityWriter{p: p, wr: wr}
17+
}

internal/entclient/activity.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package entclient
2+
3+
import "net/http"
4+
5+
func (c Client) PushActivity(source string, envID string) error {
6+
res, err := c.request("POST", "/api/metrics/usage/push", map[string]string{
7+
"source": source,
8+
"environment_id": envID,
9+
})
10+
if err != nil {
11+
return err
12+
}
13+
14+
if res.StatusCode != http.StatusOK {
15+
return bodyError(res)
16+
}
17+
18+
return nil
19+
}

internal/sync/sync.go

+26-11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"go.coder.com/flog"
2323

24+
"cdr.dev/coder-cli/internal/activity"
2425
"cdr.dev/coder-cli/internal/entclient"
2526
"cdr.dev/wsep"
2627
)
@@ -33,8 +34,11 @@ type Sync struct {
3334
LocalDir string
3435
// RemoteDir is an absolute path.
3536
RemoteDir string
36-
entclient.Environment
37-
*entclient.Client
37+
// DisableMetrics disables activity metric pushing.
38+
DisableMetrics bool
39+
40+
Env entclient.Environment
41+
Client *entclient.Client
3842
}
3943

4044
func (s Sync) syncPaths(delete bool, local, remote string) error {
@@ -43,7 +47,7 @@ func (s Sync) syncPaths(delete bool, local, remote string) error {
4347
args := []string{"-zz",
4448
"-a",
4549
"--delete",
46-
"-e", self + " sh", local, s.Environment.Name + ":" + remote,
50+
"-e", self + " sh", local, s.Env.Name + ":" + remote,
4751
}
4852
if delete {
4953
args = append([]string{"--delete"}, args...)
@@ -68,7 +72,7 @@ func (s Sync) syncPaths(delete bool, local, remote string) error {
6872
}
6973

7074
func (s Sync) remoteRm(ctx context.Context, remote string) error {
71-
conn, err := s.Client.DialWsep(ctx, s.Environment)
75+
conn, err := s.Client.DialWsep(ctx, s.Env)
7276
if err != nil {
7377
return err
7478
}
@@ -229,13 +233,16 @@ func (s Sync) workEventGroup(evs []timedEvent) {
229233
}
230234

231235
const (
232-
// maxinflightInotify sets the maximum number of inotifies before the sync just restarts.
233-
// Syncing a large amount of small files (e.g .git or node_modules) is impossible to do performantly
234-
// with individual rsyncs.
236+
// maxinflightInotify sets the maximum number of inotifies before the
237+
// sync just restarts. Syncing a large amount of small files (e.g .git
238+
// or node_modules) is impossible to do performantly with individual
239+
// rsyncs.
235240
maxInflightInotify = 8
236241
maxEventDelay = time.Second * 7
237-
// maxAcceptableDispatch is the maximum amount of time before an event should begin its journey to the server.
238-
// This sets a lower bound for perceivable latency, but the higher it is, the better the optimization.
242+
// maxAcceptableDispatch is the maximum amount of time before an event
243+
// should begin its journey to the server. This sets a lower bound for
244+
// perceivable latency, but the higher it is, the better the
245+
// optimization.
239246
maxAcceptableDispatch = time.Millisecond * 50
240247
)
241248

@@ -245,13 +252,17 @@ const (
245252
func (s Sync) Run() error {
246253
events := make(chan notify.EventInfo, maxInflightInotify)
247254
// Set up a recursive watch.
248-
// We do this before the initial sync so we can capture any changes that may have happened during sync.
255+
// We do this before the initial sync so we can capture any changes
256+
// that may have happened during sync.
249257
err := notify.Watch(path.Join(s.LocalDir, "..."), events, notify.All)
250258
if err != nil {
251259
return xerrors.Errorf("create watch: %w", err)
252260
}
253261
defer notify.Stop(events)
254262

263+
ap := activity.NewPusher(s.Client, s.Env.ID, activityName)
264+
ap.Push()
265+
255266
setConsoleTitle("⏳ syncing project")
256267
err = s.initSync()
257268
if err != nil {
@@ -265,7 +276,8 @@ func (s Sync) Run() error {
265276
flog.Info("watching %s for changes", s.LocalDir)
266277

267278
var droppedEvents uint64
268-
// Timed events lets us track how long each individual file takes to update.
279+
// Timed events lets us track how long each individual file takes to
280+
// update.
269281
timedEvents := make(chan timedEvent, cap(events))
270282
go func() {
271283
defer close(timedEvents)
@@ -309,6 +321,9 @@ func (s Sync) Run() error {
309321
}
310322
s.workEventGroup(eventGroup)
311323
eventGroup = eventGroup[:0]
324+
ap.Push()
312325
}
313326
}
314327
}
328+
329+
const activityName = "sync"

0 commit comments

Comments
 (0)