Slice per-step logs from the job's flat stream (log_index/log_length)

268bd5d · gmorell · 2026-06-25 19:57

4 files +73 -3
Message
{commit_body(@commit)}

Files changed

modified lib/git_gud/workflows.ex
+46 −2
@@ -543,16 +543,48 @@ defmodule GitGud.Workflows do
543 543 Persisted log rows for a step, ascending by sequence. Hydrates the log
544 544 view when a step is opened (live appends arrive separately via PubSub).
545 545 Returns `%{seq, body}` mapsthe shape the log stream renders.
546 +
547 + The runner streams one flat log per job (stored on the first step, see
548 + `ensure_log_step/1`); each step owns the slice `[log_index, log_index +
549 + log_length)` of it. When those offsets are known we return that slice;
550 + otherwise we fall back to rows stored directly under this step (the
551 + whole stream for the holder step, empty for the restthe old
552 + behavior, e.g. while a step is still running).
546 553 """
547 def list_step_logs(step_id) when is_integer(step_id) do
554 + def list_step_logs(%WorkflowStep{workflow_job_id: jid, log_index: idx, log_length: len})
555 + when is_integer(idx) and is_integer(len) and len > 0 do
556 + holder_id = log_holder_id(jid)
557 + upper = idx + len
558 +
559 + from(l in "workflow_step_logs",
560 + where: l.workflow_step_id == ^holder_id and l.seq >= ^idx and l.seq < ^upper,
561 + order_by: [asc: l.seq],
562 + select: %{seq: l.seq, body: l.body}
563 + )
564 + |> Repo.all()
565 + end
566 +
567 + def list_step_logs(%WorkflowStep{id: id}) do
548 568 from(l in "workflow_step_logs",
549 where: l.workflow_step_id == ^step_id,
569 + where: l.workflow_step_id == ^id,
550 570 order_by: [asc: l.seq],
551 571 select: %{seq: l.seq, body: l.body}
552 572 )
553 573 |> Repo.all()
554 574 end
555 575
576 + # Step that physically holds the job's flat log stream — the lowest
577 + # `number`, matching `ensure_log_step/1`.
578 + defp log_holder_id(job_id) do
579 + Repo.one(
580 + from s in WorkflowStep,
581 + where: s.workflow_job_id == ^job_id,
582 + order_by: [asc: s.number],
583 + limit: 1,
584 + select: s.id
585 + )
586 + end
587 +
556 588 defp next_log_seq(step_id) do
557 589 max_seq =
558 590 Repo.one(
@@ -729,6 +761,7 @@ defmodule GitGud.Workflows do
729 761 |> maybe_put(:conclusion, step_status_conclusion(status))
730 762 |> maybe_put(:started_at, timestamp_to_dt(st.started_at))
731 763 |> maybe_put(:completed_at, timestamp_to_dt(st.stopped_at))
764 + |> maybe_put_log_offsets(st)
732 765
733 766 {:ok, updated} =
734 767 step
@@ -744,6 +777,17 @@ defmodule GitGud.Workflows do
744 777
745 778 def apply_step_states(_job, _), do: :ok
746 779
780 + # The runner reports each step's slice of the job's flat log as
781 + # [log_index, log_index + log_length). Only persist when the step
782 + # actually has lines, so a 0-length intermediate update doesn't clobber
783 + # offsets set by an earlier one.
784 + defp maybe_put_log_offsets(changes, %{log_length: len, log_index: idx})
785 + when is_integer(len) and len > 0 and is_integer(idx) do
786 + changes |> Map.put(:log_index, idx) |> Map.put(:log_length, len)
787 + end
788 +
789 + defp maybe_put_log_offsets(changes, _), do: changes
790 +
747 791 defp step_result_to_status(:RESULT_SUCCESS), do: "success"
748 792 defp step_result_to_status(:RESULT_FAILURE), do: "failure"
749 793 defp step_result_to_status(:RESULT_CANCELLED), do: "cancelled"
modified lib/git_gud/workflows/workflow_step.ex
+4 −0
@@ -13,6 +13,10 @@ defmodule GitGud.Workflows.WorkflowStep do
13 13 field :conclusion, :string
14 14 field :started_at, :utc_datetime
15 15 field :completed_at, :utc_datetime
16 + # Offsets into the job's flat log stream (stored on the first step):
17 + # this step's lines are [log_index, log_index + log_length).
18 + field :log_index, :integer
19 + field :log_length, :integer
16 20
17 21 belongs_to :workflow_job, WorkflowJob
18 22
modified lib/git_gud_web/live/workflow_live/show.ex
+9 −1
@@ -101,7 +101,11 @@ defmodule GitGudWeb.WorkflowLive.Show do
101 101 # finished (or reloaded) run shows nothing until we load them here.
102 102 # `reset: true` replaces the stream with the authoritative set;
103 103 # dom_id keys on `seq` so any in-flight live appends dedupe.
104 logs = Workflows.list_step_logs(id)
104 + logs =
105 + case find_step(socket.assigns.run, id) do
106 + %{} = step -> Workflows.list_step_logs(step)
107 + _ -> []
108 + end
105 109
106 110 {:noreply,
107 111 socket
@@ -110,6 +114,10 @@ defmodule GitGudWeb.WorkflowLive.Show do
110 114 end
111 115 end
112 116
117 + defp find_step(run, id) do
118 + Enum.find_value(run.jobs, fn job -> Enum.find(job.steps, &(&1.id == id)) end)
119 + end
120 +
113 121 def handle_event("rerun", _params, socket) do
114 122 cond do
115 123 not socket.assigns.can_admin? ->
added priv/repo/migrations/20260625000000_add_step_log_offsets.exs
+14 −0
@@ -0,0 +1,14 @@
1 +defmodule GitGud.Repo.Migrations.AddStepLogOffsets do
2 + use Ecto.Migration
3 +
4 + # The runner streams one flat log per job (UpdateLog) and reports each
5 + # step's slice as [log_index, log_index + log_length) via StepState.
6 + # Persist those offsets so the run view can show per-step logs instead
7 + # of dumping the whole stream onto the first step.
8 + def change do
9 + alter table(:workflow_steps) do
10 + add :log_index, :integer
11 + add :log_length, :integer
12 + end
13 + end
14 +end

Parents: eaaaea6