Skip to content

Commit 9cd3059

Browse files
authored
Log cog build/push functions (#2289)
* Add build logging * Add R8_COGLOG_DISABLE to disable logging * Add push logging * Fix tests * Fix lint
1 parent c505da2 commit 9cd3059

File tree

10 files changed

+332
-21
lines changed

10 files changed

+332
-21
lines changed

pkg/cli/build.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import (
88
"github.com/spf13/cobra"
99
"github.com/spf13/pflag"
1010

11+
"github.com/replicate/cog/pkg/coglog"
1112
"github.com/replicate/cog/pkg/config"
13+
"github.com/replicate/cog/pkg/docker"
14+
"github.com/replicate/cog/pkg/http"
1215
"github.com/replicate/cog/pkg/image"
1316
"github.com/replicate/cog/pkg/util/console"
1417
)
@@ -57,8 +60,17 @@ func newBuildCommand() *cobra.Command {
5760
func buildCommand(cmd *cobra.Command, args []string) error {
5861
ctx := cmd.Context()
5962

63+
command := docker.NewDockerCommand()
64+
client, err := http.ProvideHTTPClient(ctx, command)
65+
if err != nil {
66+
return err
67+
}
68+
logClient := coglog.NewClient(client)
69+
logCtx := logClient.StartBuild(buildFast, buildLocalImage)
70+
6071
cfg, projectDir, err := config.GetConfig(projectDirFlag)
6172
if err != nil {
73+
logClient.EndBuild(ctx, err, logCtx)
6274
return err
6375
}
6476
if cfg.Build.Fast {
@@ -75,14 +87,17 @@ func buildCommand(cmd *cobra.Command, args []string) error {
7587

7688
err = config.ValidateModelPythonVersion(cfg)
7789
if err != nil {
90+
logClient.EndBuild(ctx, err, logCtx)
7891
return err
7992
}
8093

81-
if err := image.Build(ctx, cfg, projectDir, imageName, buildSecrets, buildNoCache, buildSeparateWeights, buildUseCudaBaseImage, buildProgressOutput, buildSchemaFile, buildDockerfileFile, DetermineUseCogBaseImage(cmd), buildStrip, buildPrecompile, buildFast, nil, buildLocalImage); err != nil {
94+
if err := image.Build(ctx, cfg, projectDir, imageName, buildSecrets, buildNoCache, buildSeparateWeights, buildUseCudaBaseImage, buildProgressOutput, buildSchemaFile, buildDockerfileFile, DetermineUseCogBaseImage(cmd), buildStrip, buildPrecompile, buildFast, nil, buildLocalImage, command); err != nil {
95+
logClient.EndBuild(ctx, err, logCtx)
8296
return err
8397
}
8498

8599
console.Infof("\nImage built as %s", imageName)
100+
logClient.EndBuild(ctx, nil, logCtx)
86101

87102
return nil
88103
}

pkg/cli/push.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99

1010
"github.com/replicate/go/uuid"
1111

12+
"github.com/replicate/cog/pkg/coglog"
1213
"github.com/replicate/cog/pkg/config"
1314
"github.com/replicate/cog/pkg/docker"
1415
"github.com/replicate/cog/pkg/global"
16+
"github.com/replicate/cog/pkg/http"
1517
"github.com/replicate/cog/pkg/image"
1618
"github.com/replicate/cog/pkg/util/console"
1719
)
@@ -44,8 +46,17 @@ func newPushCommand() *cobra.Command {
4446
func push(cmd *cobra.Command, args []string) error {
4547
ctx := cmd.Context()
4648

49+
command := docker.NewDockerCommand()
50+
client, err := http.ProvideHTTPClient(ctx, command)
51+
if err != nil {
52+
return err
53+
}
54+
logClient := coglog.NewClient(client)
55+
logCtx := logClient.StartPush(buildFast, buildLocalImage)
56+
4757
cfg, projectDir, err := config.GetConfig(projectDirFlag)
4858
if err != nil {
59+
logClient.EndPush(ctx, err, logCtx)
4960
return err
5061
}
5162
if cfg.Build.Fast {
@@ -58,17 +69,23 @@ func push(cmd *cobra.Command, args []string) error {
5869
}
5970

6071
if imageName == "" {
61-
return fmt.Errorf("To push images, you must either set the 'image' option in cog.yaml or pass an image name as an argument. For example, 'cog push r8.im/your-username/hotdog-detector'")
72+
err = fmt.Errorf("To push images, you must either set the 'image' option in cog.yaml or pass an image name as an argument. For example, 'cog push r8.im/your-username/hotdog-detector'")
73+
logClient.EndPush(ctx, err, logCtx)
74+
return err
6275
}
6376

6477
replicatePrefix := fmt.Sprintf("%s/", global.ReplicateRegistryHost)
6578
if strings.HasPrefix(imageName, replicatePrefix) {
6679
if err := docker.ManifestInspect(ctx, imageName); err != nil && strings.Contains(err.Error(), `"code":"NAME_UNKNOWN"`) {
67-
return fmt.Errorf("Unable to find Replicate existing model for %s. Go to replicate.com and create a new model before pushing.", imageName)
80+
err = fmt.Errorf("Unable to find Replicate existing model for %s. Go to replicate.com and create a new model before pushing.", imageName)
81+
logClient.EndPush(ctx, err, logCtx)
82+
return err
6883
}
6984
} else {
7085
if buildLocalImage {
71-
return fmt.Errorf("Unable to push a local image model to a non replicate host, please disable the local image flag before pushing to this host.")
86+
err = fmt.Errorf("Unable to push a local image model to a non replicate host, please disable the local image flag before pushing to this host.")
87+
logClient.EndPush(ctx, err, logCtx)
88+
return err
7289
}
7390
}
7491

@@ -83,7 +100,7 @@ func push(cmd *cobra.Command, args []string) error {
83100

84101
startBuildTime := time.Now()
85102

86-
if err := image.Build(ctx, cfg, projectDir, imageName, buildSecrets, buildNoCache, buildSeparateWeights, buildUseCudaBaseImage, buildProgressOutput, buildSchemaFile, buildDockerfileFile, DetermineUseCogBaseImage(cmd), buildStrip, buildPrecompile, buildFast, annotations, buildLocalImage); err != nil {
103+
if err := image.Build(ctx, cfg, projectDir, imageName, buildSecrets, buildNoCache, buildSeparateWeights, buildUseCudaBaseImage, buildProgressOutput, buildSchemaFile, buildDockerfileFile, DetermineUseCogBaseImage(cmd), buildStrip, buildPrecompile, buildFast, annotations, buildLocalImage, command); err != nil {
87104
return err
88105
}
89106

@@ -94,14 +111,13 @@ func push(cmd *cobra.Command, args []string) error {
94111
console.Info("Fast push enabled.")
95112
}
96113

97-
command := docker.NewDockerCommand()
98114
err = docker.Push(ctx, imageName, buildFast, projectDir, command, docker.BuildInfo{
99115
BuildTime: buildDuration,
100116
BuildID: buildID.String(),
101-
})
117+
}, client)
102118
if err != nil {
103119
if strings.Contains(err.Error(), "404") {
104-
return fmt.Errorf("Unable to find existing Replicate model for %s. "+
120+
err = fmt.Errorf("Unable to find existing Replicate model for %s. "+
105121
"Go to replicate.com and create a new model before pushing."+
106122
"\n\n"+
107123
"If the model already exists, you may be getting this error "+
@@ -110,15 +126,20 @@ func push(cmd *cobra.Command, args []string) error {
110126
"or `sudo cog push` instead of `cog push`, "+
111127
"which causes Docker to use the wrong Docker credentials.",
112128
imageName)
129+
logClient.EndPush(ctx, err, logCtx)
130+
return err
113131
}
114-
return fmt.Errorf("Failed to push image: %w", err)
132+
err = fmt.Errorf("Failed to push image: %w", err)
133+
logClient.EndPush(ctx, err, logCtx)
134+
return err
115135
}
116136

117137
console.Infof("Image '%s' pushed", imageName)
118138
if strings.HasPrefix(imageName, replicatePrefix) {
119139
replicatePage := fmt.Sprintf("https://%s", strings.Replace(imageName, global.ReplicateRegistryHost, global.ReplicateWebsiteHost, 1))
120140
console.Infof("\nRun your model on Replicate:\n %s", replicatePage)
121141
}
142+
logClient.EndPush(ctx, nil, logCtx)
122143

123144
return nil
124145
}

pkg/coglog/client.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package coglog
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"errors"
8+
"net/http"
9+
"net/url"
10+
"strconv"
11+
"strings"
12+
"time"
13+
14+
"github.com/replicate/cog/pkg/env"
15+
"github.com/replicate/cog/pkg/util/console"
16+
)
17+
18+
type Client struct {
19+
client *http.Client
20+
}
21+
22+
type BuildLogContext struct {
23+
started time.Time
24+
fast bool
25+
localImage bool
26+
}
27+
28+
type PushLogContext struct {
29+
started time.Time
30+
fast bool
31+
localImage bool
32+
}
33+
34+
type buildLog struct {
35+
DurationMs float32 `json:"length_ms"`
36+
BuildError *string `json:"error"`
37+
Fast bool `json:"fast"`
38+
LocalImage bool `json:"local_image"`
39+
}
40+
41+
type pushLog struct {
42+
DurationMs float32 `json:"length_ms"`
43+
BuildError *string `json:"error"`
44+
Fast bool `json:"fast"`
45+
LocalImage bool `json:"local_image"`
46+
}
47+
48+
func NewClient(client *http.Client) *Client {
49+
return &Client{
50+
client: client,
51+
}
52+
}
53+
54+
func (c *Client) StartBuild(fast bool, localImage bool) BuildLogContext {
55+
logContext := BuildLogContext{
56+
started: time.Now(),
57+
fast: fast,
58+
localImage: localImage,
59+
}
60+
return logContext
61+
}
62+
63+
func (c *Client) EndBuild(ctx context.Context, err error, logContext BuildLogContext) bool {
64+
var errorStr *string = nil
65+
if err != nil {
66+
errStr := err.Error()
67+
errorStr = &errStr
68+
}
69+
buildLog := buildLog{
70+
DurationMs: float32(time.Now().Sub(logContext.started).Milliseconds()),
71+
BuildError: errorStr,
72+
Fast: logContext.fast,
73+
LocalImage: logContext.localImage,
74+
}
75+
76+
jsonData, err := json.Marshal(buildLog)
77+
if err != nil {
78+
console.Warn("Failed to marshal JSON for build log: " + err.Error())
79+
return false
80+
}
81+
82+
err = c.postLog(ctx, jsonData)
83+
if err != nil {
84+
console.Warn(err.Error())
85+
return false
86+
}
87+
88+
return true
89+
}
90+
91+
func (c *Client) StartPush(fast bool, localImage bool) PushLogContext {
92+
logContext := PushLogContext{
93+
started: time.Now(),
94+
fast: fast,
95+
localImage: localImage,
96+
}
97+
return logContext
98+
}
99+
100+
func (c *Client) EndPush(ctx context.Context, err error, logContext PushLogContext) bool {
101+
var errorStr *string = nil
102+
if err != nil {
103+
errStr := err.Error()
104+
errorStr = &errStr
105+
}
106+
pushLog := pushLog{
107+
DurationMs: float32(time.Now().Sub(logContext.started).Milliseconds()),
108+
BuildError: errorStr,
109+
Fast: logContext.fast,
110+
LocalImage: logContext.localImage,
111+
}
112+
113+
jsonData, err := json.Marshal(pushLog)
114+
if err != nil {
115+
console.Warn("Failed to marshal JSON for build log: " + err.Error())
116+
return false
117+
}
118+
119+
err = c.postLog(ctx, jsonData)
120+
if err != nil {
121+
console.Warn(err.Error())
122+
return false
123+
}
124+
125+
return true
126+
}
127+
128+
func (c *Client) postLog(ctx context.Context, jsonData []byte) error {
129+
disabled, err := DisableFromEnvironment()
130+
if err != nil {
131+
return err
132+
}
133+
if disabled {
134+
return errors.New("Cog logging disabled")
135+
}
136+
137+
url := buildURL()
138+
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url.String(), bytes.NewReader(jsonData))
139+
if err != nil {
140+
return err
141+
}
142+
resp, err := c.client.Do(req)
143+
if err != nil {
144+
return err
145+
}
146+
if resp.StatusCode != http.StatusOK {
147+
return errors.New("Bad response from build log: " + strconv.Itoa(resp.StatusCode))
148+
}
149+
return nil
150+
}
151+
152+
func baseURL() url.URL {
153+
return url.URL{
154+
Scheme: env.SchemeFromEnvironment(),
155+
Host: HostFromEnvironment(),
156+
}
157+
}
158+
159+
func buildURL() url.URL {
160+
url := baseURL()
161+
url.Path = strings.Join([]string{"", "v1", "build"}, "/")
162+
return url
163+
}

pkg/coglog/client_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package coglog
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"net/url"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/replicate/cog/pkg/env"
12+
)
13+
14+
func TestLogBuild(t *testing.T) {
15+
// Setup mock http server
16+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
w.WriteHeader(http.StatusOK)
18+
}))
19+
defer server.Close()
20+
url, err := url.Parse(server.URL)
21+
require.NoError(t, err)
22+
t.Setenv(env.SchemeEnvVarName, url.Scheme)
23+
t.Setenv(CoglogHostEnvVarName, url.Host)
24+
25+
client := NewClient(http.DefaultClient)
26+
logContext := client.StartBuild(false, false)
27+
success := client.EndBuild(t.Context(), nil, logContext)
28+
require.True(t, success)
29+
}
30+
31+
func TestLogBuildDisabled(t *testing.T) {
32+
t.Setenv(CoglogDisableEnvVarName, "true")
33+
client := NewClient(http.DefaultClient)
34+
logContext := client.StartBuild(false, false)
35+
success := client.EndBuild(t.Context(), nil, logContext)
36+
require.False(t, success)
37+
}
38+
39+
func TestLogPush(t *testing.T) {
40+
// Setup mock http server
41+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
42+
w.WriteHeader(http.StatusOK)
43+
}))
44+
defer server.Close()
45+
url, err := url.Parse(server.URL)
46+
require.NoError(t, err)
47+
t.Setenv(env.SchemeEnvVarName, url.Scheme)
48+
t.Setenv(CoglogHostEnvVarName, url.Host)
49+
50+
client := NewClient(http.DefaultClient)
51+
logContext := client.StartPush(false, false)
52+
success := client.EndPush(t.Context(), nil, logContext)
53+
require.True(t, success)
54+
}

pkg/coglog/env.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package coglog
2+
3+
import (
4+
"os"
5+
"strconv"
6+
)
7+
8+
const CoglogHostEnvVarName = "R8_COGLOG_HOST"
9+
const CoglogDisableEnvVarName = "R8_COGLOG_DISABLE"
10+
11+
func HostFromEnvironment() string {
12+
host := os.Getenv(CoglogHostEnvVarName)
13+
if host == "" {
14+
host = "coglog.replicate.delivery"
15+
}
16+
return host
17+
}
18+
19+
func DisableFromEnvironment() (bool, error) {
20+
disable := os.Getenv(CoglogDisableEnvVarName)
21+
if disable == "" {
22+
disable = "false"
23+
}
24+
return strconv.ParseBool(disable)
25+
}

0 commit comments

Comments
 (0)