mux_tf 0.16.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f2bcb9ac31603cfab773175a4d81a7ab85e8cd44124dbdf7287234dd70537db
4
- data.tar.gz: e50b7671fabc4ca94703452daef5fa3a0322f2f4fce103d2a1f02b05012e80a1
3
+ metadata.gz: 4daae54b1c3dec534cbea1d885966275435c29d6a528afac0a406c574fe5d559
4
+ data.tar.gz: 5c92881a86eac6d57e955a4194fa78119f5e86934d0b52cbacff473a3d8ddb1a
5
5
  SHA512:
6
- metadata.gz: 5bf42a828264de913553c309f8acf9274175a7a2a2e9bc71f04534e91649ca6e91727ccf21677406f52d71331771935ead9f2011c4bb6b4780988174b7860767
7
- data.tar.gz: abaaff54837201173a5c2ccaa796e2c501d48f4f8d72c46687b6275b6541037284be57dd8090c88d4d88c3eeee8983f68b03ce85e6a6bea4d98df914d1e62dbd
6
+ metadata.gz: e1ff3f7eb9936e8eeb0c1f56463405cba9f2b996ac3ff05aee914698ee447befec00ae845ec14328ab7b2e6e80201a63ae7d99dcd5ac424584843ec2091c724e
7
+ data.tar.gz: bb01ac4e5fbfe60f3ea9aadab25c42543c53c55757c86da14e2ab1eef1d707b35a5b76f9bacb4d075eca00fa58c16eb278cffe1497612ac6cca91b5bb0934caa
@@ -8,6 +8,8 @@ module MuxTf
8
8
  include PiotrbCliUtils::CriCommandSupport
9
9
  extend PiotrbCliUtils::Util
10
10
 
11
+ attr_reader :last_lock_info
12
+
11
13
  def plan_cmd
12
14
  define_cmd("plan", summary: "Re-run plan") do |_opts, _args, _cmd|
13
15
  run_validate && run_plan
@@ -52,12 +54,25 @@ module MuxTf
52
54
  when :error
53
55
  log "something went wrong", depth: 1
54
56
  when :changes
55
- unless ENV["JSON_PLAN"]
57
+ if ENV["JSON_PLAN"]
58
+ plan_filename = PlanFilenameGenerator.for_path
59
+ plan = PlanSummaryHandler.from_file(plan_filename)
60
+
61
+ log plan.plan_text_output
62
+
63
+ log ""
64
+
65
+ plan.simple_summary do |line|
66
+ log line, depth: 2
67
+ end
68
+ else
56
69
  log "Printing Plan Summary ...", depth: 1
57
70
  plan_filename = PlanFilenameGenerator.for_path
58
- pretty_plan_summary(plan_filename)
71
+ plan = PlanSummaryHandler.from_file(plan_filename)
72
+ plan.simple_summary do |line|
73
+ log line, depth: 2
74
+ end
59
75
  end
60
- puts plan_summary_text if ENV["JSON_PLAN"]
61
76
  when :unknown
62
77
  # nothing
63
78
  end
@@ -110,13 +125,6 @@ module MuxTf
110
125
  created: meta["Created"]
111
126
  }
112
127
  end
113
-
114
- def pretty_plan_summary(filename)
115
- plan = PlanSummaryHandler.from_file(filename)
116
- plan.simple_summary do |line|
117
- log line, depth: 2
118
- end
119
- end
120
128
  end
121
129
  end
122
130
  end
@@ -14,15 +14,60 @@ module MuxTf
14
14
  class << self
15
15
  attr_accessor :plan_command
16
16
 
17
+ def detect_tool_type(tool_cmd: nil)
18
+ tool_cmd ||= ENV.fetch("MUX_TF_BASE_CMD", "terraform")
19
+ output = `#{tool_cmd} --version`
20
+ case output
21
+ when /^Terraform v(.+)$/
22
+ {
23
+ tool_name: "terraform",
24
+ tool_version: ::Regexp.last_match(1)
25
+ }
26
+ when /^OpenTofu v(.+)$/
27
+ {
28
+ tool_name: "opentofu",
29
+ tool_version: ::Regexp.last_match(1)
30
+ }
31
+ when /^terragrunt version v(.+)$/
32
+ result = {
33
+ tool_wrapper: "terragrunt",
34
+ tool_wrapper_version: ::Regexp.last_match(1)
35
+ }
36
+ tg_tool = ENV.fetch("TG_TF_PATH", "tofu")
37
+ result.merge! detect_tool_type(tool_cmd: tg_tool)
38
+ result
39
+ else
40
+ raise "can't parse tool version from: #{output.inspect}"
41
+ end
42
+ end
43
+
44
+ def init_tool_env
45
+ info = detect_tool_type
46
+
47
+ if %w[terraform opentofu].include?(info[:tool_name])
48
+ ENV["TF_IN_AUTOMATION"] = "1"
49
+ ENV["TF_INPUT"] = "0"
50
+ end
51
+
52
+ return unless info[:tool_wrapper] == "terragrunt"
53
+
54
+ if Gem::Version.new(info[:tool_wrapper_version]) >= Gem::Version.new("0.70.0")
55
+ # new syntax
56
+ ENV["TG_LOG_FORMAT"] = "json"
57
+ ENV["TG_TF_FORWARD_STDOUT"] = "true"
58
+ else
59
+ # old syntax
60
+ ENV["TERRAGRUNT_JSON_LOG"] = "1"
61
+ ENV["TERRAGRUNT_FORWARD_TF_STDOUT"] = "1"
62
+ end
63
+ end
64
+
17
65
  def run(args)
18
66
  version_check
19
67
 
20
68
  self.plan_command = PlanCommand.new
21
69
 
22
- ENV["TF_IN_AUTOMATION"] = "1"
23
- ENV["TF_INPUT"] = "0"
24
- ENV["TERRAGRUNT_JSON_LOG"] = "1"
25
- ENV["TERRAGRUNT_FORWARD_TF_STDOUT"] = "1"
70
+ init_tool_env
26
71
 
27
72
  if args[0] == "cli"
28
73
  cmd_loop
@@ -36,6 +81,7 @@ module MuxTf
36
81
  if args[0] && valid_commands.include?(args[0])
37
82
  stop_reason = catch(:stop) {
38
83
  root_cmd.run(args, {}, hard_exit: true)
84
+ nil
39
85
  }
40
86
  log pastel.red("Stopped: #{stop_reason}") if stop_reason
41
87
  return
@@ -94,6 +140,8 @@ module MuxTf
94
140
  end
95
141
 
96
142
  meta[:warnings]&.each do |warning|
143
+ next if warning[:printed]
144
+
97
145
  log "-" * 20
98
146
  log pastel.yellow("Warning: #{warning[:message]}")
99
147
  warning[:body]&.each do |line|
@@ -103,6 +151,8 @@ module MuxTf
103
151
  end
104
152
 
105
153
  meta[:errors]&.each do |error|
154
+ next if error[:printed]
155
+
106
156
  log "-" * 20
107
157
  log pastel.red("Error: #{error[:message]}")
108
158
  error[:body]&.each do |line|
@@ -149,12 +199,21 @@ module MuxTf
149
199
  log wrap_log["unprocessed remedies: #{remedies.to_a}", color: :red], depth: 1
150
200
  return [false, results]
151
201
  end
202
+ if remedies.delete? :user_error
203
+ remedy = :user_error
204
+ log wrap_log["user error encountered!", color: :red]
205
+ log wrap_log["-" * 40, color: :red]
206
+ log wrap_log["!! User Error, Please fix the issue and try again", color: :red]
207
+ log wrap_log["-" * 40, color: :red]
208
+ results[:user_error] = true
209
+ return [false, results]
210
+ end
152
211
  if remedies.delete? :init
153
212
  remedy = :init
154
213
  log wrap_log["Running terraform init ..."], depth: 2
155
- exit_code, meta = PlanFormatter.run_tf_init
214
+ exit_code, meta = InitFormatter.run_tf_init
156
215
  print_errors_and_warnings(meta)
157
- remedies = PlanFormatter.init_status_to_remedies(exit_code, meta)
216
+ remedies = InitFormatter.init_status_to_remedies(exit_code, meta)
158
217
  status, r_results = process_remedies(remedies, from: from, level: level + 1)
159
218
  results.merge!(r_results)
160
219
  return [true, r_results] if status
@@ -170,9 +229,9 @@ module MuxTf
170
229
  remedy = :reconfigure
171
230
  log wrap_log["Running terraform init ..."], depth: 2
172
231
  result = remedy_retry_helper(from: :reconfigure, level: level + 1, attempt: retry_count) {
173
- exit_code, meta = PlanFormatter.run_tf_init(reconfigure: true)
232
+ exit_code, meta = InitFormatter.run_tf_init(reconfigure: true)
174
233
  print_errors_and_warnings(meta)
175
- remedies = PlanFormatter.init_status_to_remedies(exit_code, meta)
234
+ remedies = InitFormatter.init_status_to_remedies(exit_code, meta)
176
235
  [remedies, exit_code, meta]
177
236
  }
178
237
  unless result
@@ -180,15 +239,6 @@ module MuxTf
180
239
  return [false, result]
181
240
  end
182
241
  end
183
- if remedies.delete? :user_error
184
- remedy = :user_error
185
- log wrap_log["user error encountered!", color: :red]
186
- log wrap_log["-" * 40, color: :red]
187
- log wrap_log["!! User Error, Please fix the issue and try again", color: :red]
188
- log wrap_log["-" * 40, color: :red]
189
- results[:user_error] = true
190
- return [false, results]
191
- end
192
242
  if remedies.delete? :auth
193
243
  remedy = :auth
194
244
  log wrap_log["auth error encountered!", color: :red]
@@ -221,13 +271,26 @@ module MuxTf
221
271
  cmd_loop(status)
222
272
  end
223
273
 
224
- def cmd_loop(status = nil)
274
+ def cmd_loop(initial_status = nil)
225
275
  root_cmd = build_root_cmd
226
276
 
227
277
  folder_name = File.basename(Dir.getwd)
228
278
 
229
279
  puts root_cmd.help
230
280
 
281
+ status = initial_status
282
+
283
+ prompt = proc { format_prompt(folder_name, status) }
284
+
285
+ run_cmd_loop(prompt) do |cmd|
286
+ status = nil
287
+ throw(:stop, :no_input) if cmd == ""
288
+ args = Shellwords.split(cmd)
289
+ root_cmd.run(args, {}, hard_exit: false)
290
+ end
291
+ end
292
+
293
+ def format_prompt(folder_name, status)
231
294
  prompt = "#{folder_name} => "
232
295
  case status
233
296
  when :error, :unknown
@@ -235,12 +298,7 @@ module MuxTf
235
298
  when :changes
236
299
  prompt = "[#{pastel.yellow(status.to_s)}] #{prompt}"
237
300
  end
238
-
239
- run_cmd_loop(prompt) do |cmd|
240
- throw(:stop, :no_input) if cmd == ""
241
- args = Shellwords.split(cmd)
242
- root_cmd.run(args, {}, hard_exit: false)
243
- end
301
+ prompt
244
302
  end
245
303
 
246
304
  def build_root_cmd
@@ -261,32 +319,19 @@ module MuxTf
261
319
  root_cmd
262
320
  end
263
321
 
264
- def plan_summary_text
265
- plan_filename = PlanFilenameGenerator.for_path
266
- if File.exist?("#{plan_filename}.txt") && File.mtime("#{plan_filename}.txt").to_f >= File.mtime(plan_filename).to_f
267
- File.read("#{plan_filename}.txt")
268
- else
269
- puts "Inspecting Changes ... #{plan_filename}"
270
- data = PlanUtils.text_version_of_plan_show(plan_filename)
271
- File.write("#{plan_filename}.txt", data)
272
- data
273
- end
274
- end
275
-
276
322
  def plan_details_cmd
277
323
  define_cmd("details", summary: "Show Plan Details") do |_opts, _args, _cmd|
278
- puts plan_summary_text
324
+ plan_filename = PlanFilenameGenerator.for_path
325
+ plan = PlanSummaryHandler.from_file(plan_filename)
279
326
 
280
- unless ENV["JSON_PLAN"]
281
- log "Printing Plan Summary ...", depth: 1
327
+ log plan.plan_text_output
282
328
 
283
- plan_filename = PlanFilenameGenerator.for_path
284
- plan = PlanSummaryHandler.from_file(plan_filename)
285
- plan.simple_summary do |line|
286
- log line
287
- end
329
+ log ""
330
+
331
+ log "Resource Summary:"
332
+ plan.simple_summary do |line|
333
+ log line
288
334
  end
289
- # puts plan_summary_text if ENV["JSON_PLAN"]
290
335
  end
291
336
  end
292
337
 
@@ -315,7 +360,7 @@ module MuxTf
315
360
  define_cmd("force-unlock", summary: "Force unlock state after encountering a lock error!") do # rubocop:disable Metrics/BlockLength
316
361
  prompt = TTY::Prompt.new(interrupt: :noop)
317
362
 
318
- lock_info = @last_lock_info
363
+ lock_info = @plan_command.last_lock_info
319
364
 
320
365
  if lock_info
321
366
  table = TTY::Table.new(header: %w[Field Value])
@@ -358,7 +403,7 @@ module MuxTf
358
403
 
359
404
  def init_cmd
360
405
  define_cmd("init", summary: "Re-run init") do |_opts, _args, _cmd|
361
- exit_code, meta = PlanFormatter.run_tf_init
406
+ exit_code, meta = InitFormatter.run_tf_init
362
407
  print_errors_and_warnings(meta)
363
408
  if exit_code != 0
364
409
  log meta.inspect unless meta.empty?
@@ -379,7 +424,7 @@ module MuxTf
379
424
 
380
425
  def reconfigure_cmd
381
426
  define_cmd("reconfigure", summary: "Reconfigure modules/plguins") do |_opts, _args, _cmd|
382
- exit_code, meta = PlanFormatter.run_tf_init(reconfigure: true)
427
+ exit_code, meta = InitFormatter.run_tf_init(reconfigure: true)
383
428
  print_errors_and_warnings(meta)
384
429
  if exit_code != 0
385
430
  log meta.inspect unless meta.empty?
@@ -411,7 +456,7 @@ module MuxTf
411
456
  end
412
457
 
413
458
  def run_upgrade
414
- exit_code, meta = PlanFormatter.run_tf_init(upgrade: true)
459
+ exit_code, meta = InitFormatter.run_tf_init(upgrade: true)
415
460
  print_errors_and_warnings(meta)
416
461
  case exit_code
417
462
  when 0
@@ -0,0 +1,257 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MuxTf
4
+ module FormatterCommon
5
+ def tf_cmd_json(cmd_call_proc, &block)
6
+ last_stderr_line = nil
7
+ handler = proc { |(stream, raw_line)| # rubocop:disable Metrics/BlockLength
8
+ case stream
9
+ when :command
10
+ log "Running command: #{raw_line.strip} ...", depth: 2
11
+ when :stdout
12
+ begin
13
+ parsed_line = JSON.parse(raw_line)
14
+ parsed_line.keys.each do |key|
15
+ if key[0] == "@"
16
+ parsed_line[key[1..]] = parsed_line[key]
17
+ parsed_line.delete(key)
18
+ end
19
+ end
20
+ parsed_line.symbolize_keys!
21
+ parsed_line[:stream] = stream
22
+ if last_stderr_line
23
+ emit_line_helper(last_stderr_line, &block)
24
+ last_stderr_line = nil
25
+ end
26
+ emit_line_helper(parsed_line, &block)
27
+ rescue JSON::ParserError
28
+ # eg: "[WARN] Provider spacelift-io/spacelift (registry.opentofu.org) gpg key expired, this will fail in future versions of OpenTofu\n"
29
+ # treat the line as a normal log line
30
+ parsed_line = {
31
+ message: raw_line.strip,
32
+ module: "non-json-log"
33
+ }
34
+ parsed_line[:level] = raw_line[0..-2].strip if raw_line.match?(/^[A-Z]+/)
35
+ parsed_line[:stream] = stream
36
+ emit_line_helper(parsed_line, &block)
37
+ end
38
+ when :stderr
39
+ parsed_line = parse_non_json_plan_line(raw_line)
40
+ parsed_line[:stream] = stream
41
+
42
+ if parsed_line[:blank]
43
+ if last_stderr_line
44
+ emit_line_helper(last_stderr_line, &block)
45
+ last_stderr_line = nil
46
+ end
47
+ elsif parsed_line[:merge_up]
48
+ if last_stderr_line
49
+ last_stderr_line[:message] += "\n#{parsed_line[:message]}"
50
+ else
51
+ # this is just a standalone message then
52
+ parsed_line.delete(:merge_up)
53
+ last_stderr_line = parsed_line
54
+ end
55
+ elsif last_stderr_line
56
+ emit_line_helper(last_stderr_line, &block)
57
+ last_stderr_line = parsed_line
58
+ else
59
+ last_stderr_line = parsed_line
60
+ end
61
+ end
62
+ }
63
+
64
+ status = cmd_call_proc.call(handler)
65
+
66
+ emit_line_helper(last_stderr_line, &block) if last_stderr_line
67
+ status
68
+ end
69
+
70
+ def parse_non_json_plan_line(raw_line)
71
+ result = {}
72
+
73
+ if raw_line.match(/^time=(?<timestamp>[^ ]+) level=(?<level>[^ ]+) msg=(?<message>.+?)(?: prefix=\[(?<prefix>.+?)\])?\s*$/)
74
+ result.merge!($LAST_MATCH_INFO.named_captures.symbolize_keys)
75
+ result[:module] = "terragrunt"
76
+ result.delete(:prefix) unless result[:prefix]
77
+ result[:prefix] = Pathname.new(result[:prefix]).relative_path_from(Dir.getwd).to_s if result[:prefix]
78
+
79
+ result[:merge_up] = true if result[:message].match(/^\d+ errors? occurred:$/)
80
+ elsif raw_line.strip == ""
81
+ result[:blank] = true
82
+ else
83
+ result[:message] = raw_line
84
+ result[:merge_up] = true
85
+ end
86
+
87
+ # time=2023-08-25T11:44:41-07:00 level=error msg=Terraform invocation failed in /Users/piotr/Work/janepods/.terragrunt-cache/BM86IAj5tW4bZga2lXeYT8tdOKI/V0IEypKSfyl-kHfCnRNAqyX02V8/modules/event-bus prefix=[/Users/piotr/Work/janepods/accounts/eks-dev/admin/apps/kube-system-event-bus]
88
+ # time=2023-08-25T11:44:41-07:00 level=error msg=1 error occurred:
89
+ # * [/Users/piotr/Work/janepods/.terragrunt-cache/BM86IAj5tW4bZga2lXeYT8tdOKI/V0IEypKSfyl-kHfCnRNAqyX02V8/modules/event-bus] exit status 2
90
+ #
91
+ #
92
+ result
93
+ end
94
+
95
+ def emit_line_helper(result, &block)
96
+ result[:level] ||= result[:stream] == :stderr ? "error" : "info"
97
+ result[:module] ||= result[:stream]
98
+ result[:type] ||= "unknown"
99
+
100
+ result[:message].lstrip! if result[:message] =~ /^\n/
101
+
102
+ if result[:message].match(/^Terraform invocation failed in (.+)/)
103
+ result[:type] = "tf_failed"
104
+
105
+ lines = result[:message].split("\n")
106
+ result[:diagnostic] = {
107
+ "summary" => "Terraform invocation failed",
108
+ "detail" => result[:message],
109
+ roots: [],
110
+ extra: []
111
+ }
112
+
113
+ lines.each do |line|
114
+ if line.match(/^\s+\* \[(.+)\] exit status (\d+)$/)
115
+ result[:diagnostic][:roots] << {
116
+ path: $LAST_MATCH_INFO[1],
117
+ status: $LAST_MATCH_INFO[2].to_i
118
+ }
119
+ elsif line.match(/^\d+ errors? occurred$/)
120
+ # noop
121
+ else
122
+ result[:diagnostic][:extra] << line
123
+ end
124
+ end
125
+
126
+ result[:message] = "Terraform invocation failed"
127
+ end
128
+
129
+ block.call(result)
130
+ end
131
+
132
+ def print_unhandled_error_line(parsed_line)
133
+ if parsed_line[:diagnostic]
134
+ color = :red
135
+ dinfo = parsed_line[:diagnostic]
136
+
137
+ log "#{pastel.decorate(dinfo['severity'].capitalize, color)}: #{dinfo['summary']}", depth: 3
138
+ log dinfo["detail"].split("\n"), depth: 4 if dinfo["detail"]
139
+ log format_validation_range(dinfo, color), depth: 4 if dinfo["range"]
140
+ elsif parsed_line[:message] =~ /^\[reset\]/
141
+ log pastel.red(parsed_line[:message].gsub(/^\[reset\]/, "")), depth: 3
142
+ else
143
+ p parsed_line
144
+ end
145
+ end
146
+
147
+ def format_validation_range(dinfo, color)
148
+ range = dinfo["range"]
149
+
150
+ # filename: "../../../modules/pods/jane_pod/main.tf"
151
+ # start:
152
+ # line: 151
153
+ # column: 27
154
+ # byte: 6632
155
+ # end:
156
+ # line: 151
157
+ # column: 53
158
+ # byte: 6658
159
+
160
+ context_lines = 3
161
+
162
+ lines = range["start"]["line"]..range["end"]["line"]
163
+ columns = range["start"]["column"]..range["end"]["column"]
164
+
165
+ # on ../../../modules/pods/jane_pod/main.tf line 151, in module "jane":
166
+ # 151: jane_resources_preset = var.jane_resources_presetx
167
+ output = []
168
+ lines_info = if lines.size == 1
169
+ "#{lines.first}:#{columns.first}"
170
+ else
171
+ "#{lines.first}:#{columns.first} to #{lines.last}:#{columns.last}"
172
+ end
173
+ output << "on: #{range['filename']} line#{lines.size > 1 ? 's' : ''}: #{lines_info}"
174
+
175
+ # TODO: in terragrunt mode, we need to somehow figure out the path to the cache root, all the paths will end up being relative to that
176
+ if File.exist?(range["filename"])
177
+ file_lines = File.read(range["filename"]).split("\n")
178
+ extract_range = (([lines.first - context_lines,
179
+ 0].max)..([lines.last + context_lines, file_lines.length - 1].min))
180
+ file_lines.each_with_index do |line, index|
181
+ if extract_range.cover?(index + 1)
182
+ if lines.cover?(index + 1)
183
+ start_col = 1
184
+ end_col = :max
185
+ if index + 1 == lines.first
186
+ start_col = columns.first
187
+ elsif index + 1 == lines.last
188
+ start_col = columns.last
189
+ end
190
+ painted_line = paint_line(line, color, start_col: start_col, end_col: end_col)
191
+ output << "#{pastel.decorate('>', color)} #{index + 1}: #{painted_line}"
192
+ else
193
+ output << " #{index + 1}: #{line}"
194
+ end
195
+ end
196
+ end
197
+ elsif dinfo["snippet"]
198
+ snippet = dinfo["snippet"]
199
+ # {
200
+ # "context"=>"locals",
201
+ # "code"=>" aws_iam_policy.crossplane_aws_ecr.arn",
202
+ # "start_line"=>72,
203
+ # "highlight_start_offset"=>8,
204
+ # "highlight_end_offset"=>41,
205
+ # "values"=>[]
206
+ # }
207
+ output << "Code:"
208
+ output << "in #{snippet['context']}" if snippet["context"]
209
+ line = snippet["start_line"]
210
+ snippet["code"].split("\n").each do |l|
211
+ output << "#{line}: #{l}"
212
+ line += 1
213
+ end
214
+ end
215
+
216
+ if dinfo["snippet"] && dinfo["snippet"]["values"]&.any?
217
+ snippet = dinfo["snippet"]
218
+ output << ""
219
+ output << "Values:"
220
+ snippet["values"].each do |value|
221
+ output << " #{pastel.bold(value['traversal'])} #{value['statement']}"
222
+ end
223
+ end
224
+
225
+ output
226
+ end
227
+
228
+ def paint_line(line, *paint_options, start_col: 1, end_col: :max)
229
+ end_col = line.length if end_col == :max
230
+ prefix = line[0, start_col - 1]
231
+ suffix = line[end_col..]
232
+ middle = line[start_col - 1..end_col - 1]
233
+ "#{prefix}#{pastel.decorate(middle, *paint_options)}#{suffix}"
234
+ end
235
+
236
+ def print_tg_error_line(parsed_line)
237
+ parsed_line[:message].split("\n").each do |line|
238
+ next if line.strip == ""
239
+
240
+ parsed_message = JSON.parse(line)
241
+ raw_prefix = "[TG ERROR] "
242
+ prefix = pastel.red(raw_prefix)
243
+ index = 0
244
+ lines = parsed_message["msg"].split("\n").map { |l|
245
+ l = if index.zero?
246
+ prefix + l
247
+ else
248
+ (" " * raw_prefix.length) + l
249
+ end
250
+ index += 1
251
+ l
252
+ }
253
+ log lines, depth: 1
254
+ end
255
+ end
256
+ end
257
+ end