Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: add more detailed init timings in build timeline#20503

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Emyrk merged 13 commits intomainfromstevenmasley/build_timeline_init
Oct 28, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletionsprovisioner/terraform/executor.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,7 +28,10 @@ import (
"github.com/coder/coder/v2/provisionersdk/proto"
)

var version170 = version.Must(version.NewVersion("1.7.0"))
var (
version170 = version.Must(version.NewVersion("1.7.0"))
version190 = version.Must(version.NewVersion("1.9.0"))
)
Comment on lines +31 to +34
Copy link
Member

@matifalimatifaliOct 29, 2025
edited
Loading

Choose a reason for hiding this comment

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

Terraform versions below 1.12 are already EOL. We also bundle Terraform in the official image. We may not need to worry about supporting older versions.
https://endoflife.date/terraform

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

We should update our minimum supported then.

Copy link
MemberAuthor

Choose a reason for hiding this comment

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

I will make a PR


type executor struct {
logger slog.Logger
Expand DownExpand Up@@ -225,7 +228,7 @@ func (e *executor) init(ctx, killCtx context.Context, logr logSink) error {
lockFilePath := filepath.Join(e.workdir, ".terraform.lock.hcl")
preInitChecksum := checksumFileCRC32(ctx, e.logger, lockFilePath)

outWriter, doneOut :=logWriter(logr, proto.LogLevel_DEBUG)
outWriter, doneOut :=e.provisionLogWriter(logr)
errWriter, doneErr := logWriter(logr, proto.LogLevel_ERROR)
defer func() {
_ = outWriter.Close()
Expand All@@ -244,7 +247,16 @@ func (e *executor) init(ctx, killCtx context.Context, logr logSink) error {
"-input=false",
}

err := e.execWriteOutput(ctx, killCtx, args, e.basicEnv(), outWriter, errBuf)
ver, err := e.version(ctx)
if err != nil {
return xerrors.Errorf("extract version: %w", err)
}
if ver.GreaterThanOrEqual(version190) {
// Added in v1.9.0:
args = append(args, "-json")
}

err = e.execWriteOutput(ctx, killCtx, args, e.basicEnv(), outWriter, errBuf)
var exitErr *exec.ExitError
if xerrors.As(err, &exitErr) {
if bytes.Contains(errBuf.b.Bytes(), []byte("text file busy")) {
Expand DownExpand Up@@ -809,17 +821,21 @@ func extractTimingSpan(log *terraformProvisionLog) (time.Time, *timingSpan, erro
return time.Time{}, nil, xerrors.Errorf("unexpected timing kind: %q", log.Type)
}

// Init logs omit millisecond precision, so using `time.Now` as a fallback
// for these logs is more precise than parsing the second precision alone.
// https://github.com/hashicorp/terraform/pull/37818
ts, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", log.Timestamp)
if err != nil {
// TODO: log
ts = time.Now()
}

return ts, &timingSpan{
kind: typ,
action: log.Hook.Action,
provider: log.Hook.Resource.Provider,
resource: log.Hook.Resource.Addr,
kind: typ,
messageCode: log.MessageCode,
action: log.Hook.Action,
provider: log.Hook.Resource.Provider,
resource: log.Hook.Resource.Addr,
}, nil
}

Expand All@@ -842,11 +858,14 @@ func convertTerraformLogLevel(logLevel string, sink logSink) proto.LogLevel {
}

type terraformProvisionLog struct {
Level string `json:"@level"`
Message string `json:"@message"`
Timestamp string `json:"@timestamp"`
Type string `json:"type"`
Hook terraformProvisionLogHook `json:"hook"`
Level string `json:"@level"`
Message string `json:"@message"`
Timestamp string `json:"@timestamp"`
Type string `json:"type"`
// MessageCode is only set for init phase messages after Terraform 1.9.0
// This field is not used by plan/apply.
MessageCode initMessageCode `json:"message_code,omitempty"`
Hook terraformProvisionLogHook `json:"hook"`

Diagnostic *tfjson.Diagnostic `json:"diagnostic,omitempty"`
}
Expand Down
166 changes: 166 additions & 0 deletionsprovisioner/terraform/inittimings.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
package terraform

import (
"slices"
"time"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/provisionersdk/proto"
)

const (
// defaultInitAction is a human-readable action for init timing spans. The coder
// frontend displays the action, which would be an empty string if not set to
// this constant. Setting it to "load" gives more context to users about what is
// happening during init. The init steps either "load" from disk or http.
defaultInitAction = "load"
)

var (
// resourceName maps init message codes to human-readable resource names.
// This is purely for better readability in the timing spans.
resourceName = map[initMessageCode]string{
initInitializingBackendMessage: "backend",
initInitializingStateStoreMessage: "backend",

initInitializingModulesMessage: "modules",
initUpgradingModulesMessage: "modules",

initInitializingProviderPluginMessage: "provider plugins",
}

// executionOrder is the expected sequential steps during `terraform init`.
// Some steps of the init have more than 1 possible "initMessageCode".
//
// In practice, since Coder has a defined way of running Terraform, only
// one code per step is expected. However, this allows for future-proofing
// in case Coder adds more Terraform init configurations.
executionOrder = [][]initMessageCode{
{
initInitializingBackendMessage,
initInitializingStateStoreMessage, // If using a state store backend
},
{
initInitializingModulesMessage,
initUpgradingModulesMessage, // if "-upgrade" flag provided
},
{initInitializingProviderPluginMessage},
{
initOutputInitSuccessMessage,
initOutputInitSuccessCloudMessage, // If using terraform cloud
},
}
)

// ingestInitTiming handles ingesting timing spans from `terraform init` logs.
// These logs are formatted differently from plan/apply logs, so they need their
// own ingestion logic.
//
// The logs are also less granular, only indicating the start of major init
// steps, rather than per-resource actions. Since initialization is done
// serially, we can infer the end time of each stage from the start time of the
// next stage.
func (t *timingAggregator) ingestInitTiming(ts time.Time, s *timingSpan) {
switch s.messageCode {
case initInitializingBackendMessage, initInitializingStateStoreMessage:
// Backend loads the tfstate from the backend data source. For coder, this is
// always a state file on disk, making it nearly an instantaneous operation.
s.start = ts
s.state = proto.TimingState_STARTED
case initInitializingModulesMessage, initUpgradingModulesMessage:
s.start = ts
s.state = proto.TimingState_STARTED
case initInitializingProviderPluginMessage:
s.start = ts
s.state = proto.TimingState_STARTED
case initOutputInitSuccessMessage, initOutputInitSuccessCloudMessage:
// The final message indicates successful completion of init. There is no start
// message for this, but we want to continue the pattern such that this completes
// the previous stage.
s.end = ts
s.state = proto.TimingState_COMPLETED
default:
return
}

// Init logs should be assigned to the init stage.
// Ideally the executor could use an `init` stage aggregator directly, but
// that would require a larger refactor.
s.stage = database.ProvisionerJobTimingStageInit
// The default action is an empty string. Set it to "load" for some human readability.
s.action = defaultInitAction
// Resource name is an empty string. Name it something more useful.
s.resource = resourceName[s.messageCode]

// finishPrevious completes the previous step in the init sequence, if applicable.
t.finishPrevious(ts, s)

t.lookupMu.Lock()
// Memoize this span by its unique attributes and the determined state.
// This will be used in aggregate() to determine the duration of the resource action.
t.stateLookup[s.hashByState(s.state)] = s
t.lookupMu.Unlock()
}

func (t *timingAggregator) finishPrevious(ts time.Time, s *timingSpan) {
index := slices.IndexFunc(executionOrder, func(codes []initMessageCode) bool {
return slices.Contains(codes, s.messageCode)
})
if index <= 0 {
// If the index is not found or is the first item, nothing to complete.
return
}

// Complete the previous message.
previousSteps := executionOrder[index-1]

t.lookupMu.Lock()
// Complete the previous step. We are not tracking the state of these steps, so
// we cannot tell for sure what the previous step `MessageCode` was. The
// aggregator only reports timings that have a start & end. So if we end all
// possible previous step `MessageCodes`, the aggregator will only report the one
// that was actually started.
//
// This is a bit of a hack, but it works given the constraints of the init logs.
// Ideally we would store more state about the init steps. Or loop over the
// stored timings to find the one that was started. This is just simpler and
// accomplishes the same goal.
for _, step := range previousSteps {
cpy := *s
cpy.start = time.Time{}
cpy.end = ts
cpy.messageCode = step
cpy.resource = resourceName[step]
cpy.state = proto.TimingState_COMPLETED
t.stateLookup[cpy.hashByState(cpy.state)] = &cpy
}

t.lookupMu.Unlock()
}

// mergeInitTimings merges manual init timings with existing timings that are
// sourced by the logs. This is done because prior to Terraform v1.9, init logs
// did not have a `-json` formatting option.
// So before v1.9, the init stage is manually timed outside the `terraform init`.
// After v1.9, the init stage is timed via logs.
func mergeInitTimings(manualInit []*proto.Timing, existing []*proto.Timing) []*proto.Timing {
initFailed := slices.ContainsFunc(existing, func(timing *proto.Timing) bool {
return timing.State == proto.TimingState_FAILED
})

if initFailed {
// The init logs do not provide enough information for failed init timings.
// So use the manual timings in this case.
return append(manualInit, existing...)
}

hasInitStage := slices.ContainsFunc(existing, func(timing *proto.Timing) bool {
return timing.Stage == string(database.ProvisionerJobTimingStageInit)
})

if hasInitStage {
return existing
}

return append(manualInit, existing...)
}
2 changes: 1 addition & 1 deletionprovisioner/terraform/provision.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -170,7 +170,7 @@ func (s *server) Plan(

// Prepend init timings since they occur prior to plan timings.
// Order is irrelevant; this is merely indicative.
resp.Timings =append(initTimings.aggregate(), resp.Timings...)
resp.Timings =mergeInitTimings(initTimings.aggregate(), resp.Timings)
resp.Modules = modules
return resp
}
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
A successful build which results in successful plan and apply timings.

-- init --
{"@level":"info","@message":"Terraform 1.13.3","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:29.576675-05:00","terraform":"1.13.3","type":"version","ui":"1.2"}
{"@level":"info","@message":"Initializing the backend...","@module":"terraform.ui","@timestamp":"2025-10-22T17:48:29.000000Z","message_code":"initializing_backend_message","type":"init_output"}
{"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","@timestamp":"2025-10-22T17:48:29.000000Z","message_code":"initializing_modules_message","type":"init_output"}
{"@level":"info","@message":"Downloading registry.coder.com/coder/cursor/coder 1.3.2 for cursor...","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:29.780639-05:00","type":"log"}
{"@level":"info","@message":"- cursor in .terraform/modules/cursor","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:29.982904-05:00","type":"log"}
{"@level":"info","@message":"Downloading registry.coder.com/coder/jetbrains/coder 1.1.0 for jetbrains...","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:30.039894-05:00","type":"log"}
{"@level":"info","@message":"- jetbrains in .terraform/modules/jetbrains","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:30.202355-05:00","type":"log"}
{"@level":"info","@message":"Downloading git::https://github.com/coder/large-module.git for large-5mb-module...","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:30.202394-05:00","type":"log"}
{"@level":"info","@message":"- large-5mb-module in .terraform/modules/large-5mb-module","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:31.799988-05:00","type":"log"}
{"@level":"info","@message":"Initializing provider plugins...","@module":"terraform.ui","@timestamp":"2025-10-22T17:48:31.000000Z","message_code":"initializing_provider_plugin_message","type":"init_output"}
{"@level":"info","@message":"kreuzwerker/docker: Reusing previous version from the dependency lock file","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:31.801342-05:00","type":"log"}
{"@level":"info","@message":"hashicorp/http: Reusing previous version from the dependency lock file","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:31.868885-05:00","type":"log"}
{"@level":"info","@message":"coder/coder: Reusing previous version from the dependency lock file","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:31.894724-05:00","type":"log"}
{"@level":"info","@message":"Installing provider version: hashicorp/http v3.5.0...","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:32.081468-05:00","type":"log"}
{"@level":"info","@message":"Installed provider version: hashicorp/http v3.5.0 (signed by HashiCorp)","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:32.375580-05:00","type":"log"}
{"@level":"info","@message":"Installing provider version: coder/coder v2.11.0...","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:32.869110-05:00","type":"log"}
{"@level":"info","@message":"Installed provider version: coder/coder v2.11.0 (signed by a HashiCorp partnerkey_id: 93C75807601AA0EC)","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:33.350069-05:00","type":"log"}
{"@level":"info","@message":"Installing provider version: kreuzwerker/docker v3.6.2...","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:33.572112-05:00","type":"log"}
{"@level":"info","@message":"Installed provider version: kreuzwerker/docker v3.6.2 (self-signedkey_id: BD080C4571C6104C)","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:34.458153-05:00","type":"log"}
{"@level":"info","@message":"Partner and community providers are signed by their developers.\nIf you'd like to know more about provider signing, you can read about it here:\nhttps://developer.hashicorp.com/terraform/cli/plugins/signing","@module":"terraform.ui","@timestamp":"2025-10-22T12:48:34.458177-05:00","type":"log"}
{"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","@timestamp":"2025-10-22T17:48:34.000000Z","message_code":"output_init_success_message","type":"init_output"}
{"@level":"info","@message":"You may now begin working with Terraform. Try running \"terraform plan\" to see\nany changes that are required for your infrastructure. All Terraform commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for Terraform,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.","@module":"terraform.ui","@timestamp":"2025-10-22T17:48:34Z","message_code":"output_init_success_cli_message","type":"init_output"}
-- plan --
{"@level":"info","@message":"Terraform 1.9.2","@module":"terraform.ui","@timestamp":"2024-08-15T10:26:38.097648+02:00","terraform":"1.9.2","type":"version","ui":"1.2"}
{"@level":"info","@message":"data.coder_workspace.me: Refreshing...","@module":"terraform.ui","@timestamp":"2024-08-15T10:26:39.194726+02:00","hook":{"resource":{"addr":"data.coder_workspace.me","module":"","resource":"data.coder_workspace.me","implied_provider":"coder","resource_type":"coder_workspace","resource_name":"me","resource_key":null},"action":"read"},"type":"apply_start"}
Expand DownExpand Up@@ -30,10 +52,13 @@ A successful build which results in successful plan and apply timings.
{"@level":"info","@message":"Apply complete! Resources: 4 added, 0 changed, 0 destroyed.","@module":"terraform.ui","@timestamp":"2024-08-15T10:26:40.204593+02:00","changes":{"add":4,"change":0,"import":0,"remove":0,"operation":"apply"},"type":"change_summary"}
{"@level":"info","@message":"Outputs: 0","@module":"terraform.ui","@timestamp":"2024-08-15T10:26:40.205051+02:00","outputs":{},"type":"outputs"}
-- timings --
{"start":"2025-10-22T17:48:29Z","end":"2025-10-22T17:48:31Z","action":"load","resource":"modules","stage":"init","state":"COMPLETED"}
{"start":"2025-10-22T17:48:29Z","end":"2025-10-22T17:48:29Z","action":"load","resource":"backend","stage":"init","state":"COMPLETED"}
{"start":"2025-10-22T17:48:31Z","end":"2025-10-22T17:48:34Z","action":"load","resource":"provider plugins","stage":"init","state":"COMPLETED"}
{"start":"2024-08-15T08:26:39.194726Z","end":"2024-08-15T08:26:39.195820Z","action":"read","source":"coder","resource":"data.coder_workspace.me","stage":"plan","state":"COMPLETED"}
{"start":"2024-08-15T08:26:39.194726Z","end":"2024-08-15T08:26:39.195712Z","action":"read","source":"coder","resource":"data.coder_provisioner.me","stage":"plan","state":"COMPLETED"}
{"start":"2024-08-15T08:26:39.194726Z","end":"2024-08-15T08:26:39.195836Z","action":"read","source":"coder","resource":"data.coder_parameter.memory_size","stage":"plan","state":"COMPLETED"}
{"start":"2024-08-15T08:26:39.616546Z","end":"2024-08-15T08:26:39.618045Z","action":"create","source":"coder","resource":"coder_agent.main","stage":"apply","state":"COMPLETED"}
{"start":"2024-08-15T08:26:39.626722Z","end":"2024-08-15T08:26:39.669954Z","action":"create","source":"docker","resource":"docker_image.main","stage":"apply","state":"COMPLETED"}
{"start":"2024-08-15T08:26:39.627335Z","end":"2024-08-15T08:26:39.660616Z","action":"create","source":"docker","resource":"docker_volume.home_volume","stage":"apply","state":"COMPLETED"}
{"start":"2024-08-15T08:26:39.682223Z","end":"2024-08-15T08:26:40.186482Z","action":"create","source":"docker","resource":"docker_container.workspace[0]","stage":"apply","state":"COMPLETED"}
{"start":"2024-08-15T08:26:39.682223Z","end":"2024-08-15T08:26:40.186482Z","action":"create","source":"docker","resource":"docker_container.workspace[0]","stage":"apply","state":"COMPLETED"}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp