Skip to content

Commit ef4ac9d

Browse files
authored
Add structured data in code evaluation logs (#3077)
1 parent 5ebced8 commit ef4ac9d

File tree

8 files changed

+161
-24
lines changed

8 files changed

+161
-24
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,13 @@ The following environment variables can be used to configure Livebook on boot:
265265
logging, either of: error, warning, notice, info, debug. Defaults to warning.
266266

267267
* `LIVEBOOK_LOG_METADATA` - a comma-separated list of metadata keys that should
268-
be included in the log messages. Currently the only Livebook-spcecific key is
269-
users (attached to evaluation and request logs). By default includes only
270-
request_id.
268+
be included in the log messages. Livebook-specific keys include:
269+
- `users` (attached to evaluation and request logs)
270+
- `session_mode` (attached to evaluation logs, either "default" or "app")
271+
- `code` (attached to evaluation logs, the code being evaluated)
272+
- `event` (attached to evaluation logs, currently always "code.evaluate")
273+
274+
By default includes only `request_id`.
271275

272276
* `LIVEBOOK_LOG_FORMAT` - sets the log output format, either "text" (default)
273277
for human-readable logs or "json" for structured JSON.

config/test.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ config :livebook, :logger, [
2222
{:handler, :json_log, :logger_std_h,
2323
%{
2424
config: %{file: ~c"#{path}"},
25-
formatter: {LoggerJSON.Formatters.Basic, %{metadata: [:users, :request_id]}}
25+
formatter:
26+
{LoggerJSON.Formatters.Basic,
27+
%{metadata: [:request_id, :users, :session_mode, :code, :event]}}
2628
}}
2729
]
2830

lib/livebook.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ defmodule Livebook do
104104
log_metadata = Livebook.Config.log_metadata!("LIVEBOOK_LOG_METADATA")
105105
log_format = Livebook.Config.log_format!("LIVEBOOK_LOG_FORMAT") || :text
106106

107+
config :livebook, :log_format, log_format
108+
107109
case {log_format, log_metadata} do
108110
{:json, log_metadata} ->
109111
config :logger, :default_handler,

lib/livebook/app.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ defmodule Livebook.App do
361361
files_source: {:dir, files_source},
362362
mode: :app,
363363
app_pid: self(),
364+
app_permanent: state.deployment_bundle.permanent,
364365
auto_shutdown_ms: state.deployment_bundle.notebook.app_settings.auto_shutdown_ms,
365366
started_by: user,
366367
deployed_by: state.deployment_bundle.deployed_by

lib/livebook/session.ex

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ defmodule Livebook.Session do
132132
deployment_ref: reference() | nil,
133133
deployed_app_monitor_ref: reference() | nil,
134134
app_pid: pid() | nil,
135+
app_permanent: boolean() | nil,
135136
auto_shutdown_ms: non_neg_integer() | nil,
136137
auto_shutdown_timer_ref: reference() | nil,
137138
started_by: Users.User.t() | nil,
@@ -191,6 +192,8 @@ defmodule Livebook.Session do
191192
192193
* `:app_pid` - the parent app process, when in running in `:app` mode
193194
195+
* `:app_permanent` - whether the app is permanent or a preview app
196+
194197
* `:auto_shutdown_ms` - the inactivity period (no clients) after which
195198
the session should close automatically
196199
@@ -962,6 +965,7 @@ defmodule Livebook.Session do
962965
deployment_ref: nil,
963966
deployed_app_monitor_ref: nil,
964967
app_pid: opts[:app_pid],
968+
app_permanent: opts[:app_permanent],
965969
auto_shutdown_ms: opts[:auto_shutdown_ms],
966970
auto_shutdown_timer_ref: nil,
967971
started_by: opts[:started_by],
@@ -2638,23 +2642,10 @@ defmodule Livebook.Session do
26382642
defp handle_action(state, _action), do: state
26392643

26402644
defp start_evaluation(state, cell, section, evaluation_opts) do
2641-
evaluation_users =
2642-
case state.data.mode do
2643-
:default -> Map.values(state.data.users_map)
2644-
:app -> if(state.deployed_by, do: [state.deployed_by], else: [])
2645-
end
2646-
2647-
Logger.info(
2648-
[
2649-
"""
2650-
Evaluating code
2651-
Session mode: #{state.data.mode}
2652-
Code: \
2653-
""",
2654-
inspect(cell.source, printable_limit: :infinity)
2655-
],
2656-
Livebook.Utils.logger_users_metadata(evaluation_users)
2657-
)
2645+
# Only log code evaluation if inside a regular notebook session or a preview app
2646+
if state.data.mode == :default or state.app_permanent == false do
2647+
log_code_evaluation(cell, state)
2648+
end
26582649

26592650
path =
26602651
case state.data.file || default_notebook_file(state) do
@@ -2693,6 +2684,42 @@ defmodule Livebook.Session do
26932684
state
26942685
end
26952686

2687+
defp log_code_evaluation(cell, state) do
2688+
session_mode = state.data.mode
2689+
2690+
evaluation_users =
2691+
case session_mode do
2692+
:default -> Map.values(state.data.users_map)
2693+
:app -> if(state.deployed_by, do: [state.deployed_by], else: [])
2694+
end
2695+
2696+
# We plan to deprecate this one in favor of the log call below.
2697+
# We're keeping this here because users may be depending on this already.
2698+
# Once the new log below, we ask users to migrate, and eventually delete
2699+
# this one.
2700+
Logger.info(
2701+
[
2702+
"""
2703+
Evaluating code
2704+
Session mode: #{session_mode}
2705+
Code: \
2706+
""",
2707+
inspect(cell.source, printable_limit: :infinity)
2708+
],
2709+
Livebook.Utils.logger_users_metadata(evaluation_users)
2710+
)
2711+
2712+
Logger.info(
2713+
"Evaluating code",
2714+
Keyword.merge(
2715+
Livebook.Utils.logger_users_metadata(evaluation_users),
2716+
session_mode: session_mode,
2717+
code: cell.source,
2718+
event: "code.evaluate"
2719+
)
2720+
)
2721+
end
2722+
26962723
defp hydrate_cell_source_digest(state, cell_id, tag) do
26972724
# Clients prune source, so they can't compute the digest, but it's
26982725
# necessary for evaluation to know which cells are changed, so we

lib/livebook/utils.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,9 @@ defmodule Livebook.Utils do
854854
into: %{}
855855
end
856856

857-
[users: inspect(list)]
857+
case Application.get_env(:livebook, :log_format) do
858+
:text -> [users: inspect(list)]
859+
:json -> [users: list]
860+
end
858861
end
859862
end

test/livebook/session_test.exs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule Livebook.SessionTest do
22
use ExUnit.Case, async: true
33

4+
import ExUnit.CaptureLog
45
import Livebook.HubHelpers
56
import Livebook.AppHelpers
67
import Livebook.SessionHelpers
@@ -2092,6 +2093,100 @@ defmodule Livebook.SessionTest do
20922093
end
20932094
end
20942095

2096+
describe "code evaluation logging" do
2097+
test "logs code evaluation for regular sessions" do
2098+
session = start_session()
2099+
Session.subscribe(session.id)
2100+
2101+
{_section_id, cell_id} = insert_section_and_cell(session.pid)
2102+
2103+
unique_id = Utils.random_short_id()
2104+
# Use random ID to uniquely identify the source code of this cell
2105+
Session.set_cell_attributes(session.pid, cell_id, %{source: "#{unique_id}"})
2106+
2107+
log =
2108+
capture_log([level: :info, metadata: [:session_mode, :code, :event]], fn ->
2109+
Session.queue_cell_evaluation(session.pid, cell_id)
2110+
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
2111+
end)
2112+
2113+
# Logs from other test might be captured, so we're using an unique_id
2114+
assert log =~ "code=#{unique_id}"
2115+
assert log =~ "Evaluating code"
2116+
assert log =~ "session_mode=default"
2117+
assert log =~ "event=code.evaluate"
2118+
end
2119+
2120+
test "logs code evaluation for preview apps" do
2121+
session = start_session()
2122+
Session.subscribe(session.id)
2123+
2124+
slug = Utils.random_short_id()
2125+
app_settings = %{Notebook.AppSettings.new() | slug: slug}
2126+
Session.set_app_settings(session.pid, app_settings)
2127+
2128+
{section_id, _cell_id} = insert_section_and_cell(session.pid)
2129+
2130+
Apps.subscribe()
2131+
Session.deploy_app(session.pid)
2132+
assert_receive {:app_created, %{slug: ^slug, pid: app_pid}}
2133+
2134+
on_exit(fn ->
2135+
App.close(app_pid)
2136+
end)
2137+
2138+
session_id = App.get_session_id(app_pid)
2139+
{:ok, app_session} = Livebook.Sessions.fetch_session(session_id)
2140+
2141+
Session.subscribe(app_session.id)
2142+
2143+
unique_id = Utils.random_short_id()
2144+
# Use random ID to uniquely identify the source code of this cell
2145+
Session.insert_cell(app_session.pid, section_id, 0, :code, %{source: "#{unique_id}"})
2146+
assert_receive {:operation, {:insert_cell, _, ^section_id, 0, :code, cell_id, _}}
2147+
2148+
log =
2149+
capture_log([level: :info, metadata: [:session_mode, :code, :event]], fn ->
2150+
Session.queue_cell_evaluation(app_session.pid, cell_id)
2151+
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
2152+
end)
2153+
2154+
# Logs from other test might be captured, so we're using an unique_id
2155+
assert log =~ "code=#{unique_id}"
2156+
assert log =~ "Evaluating code"
2157+
assert log =~ "session_mode=app"
2158+
assert log =~ "event=code.evaluate"
2159+
end
2160+
2161+
test "does not log code evaluation for permanent apps" do
2162+
slug = Utils.random_short_id()
2163+
app_settings = %{Notebook.AppSettings.new() | slug: slug}
2164+
2165+
unique_id = Utils.random_short_id()
2166+
# Use random ID to uniquely identify the source code of this cell
2167+
code_cell = %{Notebook.Cell.new(:code) | source: "#{unique_id}"}
2168+
section = %{Notebook.Section.new() | cells: [code_cell]}
2169+
notebook = %{Notebook.new() | sections: [section], app_settings: app_settings}
2170+
2171+
app_pid = deploy_notebook_sync(notebook, permanent: true)
2172+
session_id = App.get_session_id(app_pid)
2173+
2174+
{:ok, app_session} = Livebook.Sessions.fetch_session(session_id)
2175+
2176+
Session.subscribe(app_session.id)
2177+
cell_id = code_cell.id
2178+
2179+
log =
2180+
capture_log([level: :info, metadata: [:session_mode, :code, :event]], fn ->
2181+
Session.queue_cell_evaluation(app_session.pid, cell_id)
2182+
assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, _}}
2183+
end)
2184+
2185+
# Logs from other test might be captured, so we're using an unique_id
2186+
refute log =~ unique_id
2187+
end
2188+
end
2189+
20952190
defp start_session(opts \\ []) do
20962191
{:ok, session} = Livebook.Sessions.create_session(opts)
20972192

test/support/app_helpers.ex

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ defmodule Livebook.AppHelpers do
33

44
alias Livebook.TeamsRPC
55

6-
def deploy_notebook_sync(notebook) do
6+
def deploy_notebook_sync(notebook, opts \\ []) do
7+
opts = Keyword.validate!(opts, permanent: false)
8+
permanent_opts = Keyword.take(opts, [:permanent])
9+
710
app_spec = Livebook.Apps.NotebookAppSpec.new(notebook)
811

912
deployer_pid = Livebook.Apps.Deployer.local_deployer()
10-
ref = Livebook.Apps.Deployer.deploy_monitor(deployer_pid, app_spec)
13+
ref = Livebook.Apps.Deployer.deploy_monitor(deployer_pid, app_spec, permanent_opts)
1114

1215
receive do
1316
{:deploy_result, ^ref, {:ok, pid}} ->

0 commit comments

Comments
 (0)