Module: LanguageOperator::CLI::Commands::Agent::Logs

Included in:
Base
Defined in:
lib/language_operator/cli/commands/agent/logs.rb

Overview

Log streaming for agents

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/language_operator/cli/commands/agent/logs.rb', line 12

def self.included(base)
  base.class_eval do
    desc 'logs NAME', 'Show agent execution logs'
    long_desc <<-DESC
      Stream agent execution logs in real-time.

      Use -f to follow logs continuously (like tail -f).

      Examples:
        langop agent logs my-agent
        langop agent logs my-agent -f
    DESC
    option :cluster, type: :string, desc: 'Override current cluster context'
    option :follow, type: :boolean, aliases: '-f', default: false, desc: 'Follow logs'
    option :tail, type: :numeric, default: 100, desc: 'Number of lines to show from the end'
    def logs(name)
      handle_command_error('get logs') do
        ctx = CLI::Helpers::ClusterContext.from_options(options)

        # Get agent to determine the pod name
        agent = get_resource_or_exit(LanguageOperator::Constants::RESOURCE_AGENT, name)

        mode = agent.dig('spec', 'mode') || 'autonomous'

        # Build kubectl command for log streaming
        tail_arg = "--tail=#{options[:tail]}"
        follow_arg = options[:follow] ? '-f' : ''

        # For scheduled agents, logs come from CronJob pods
        # For autonomous agents, logs come from Deployment pods
        if mode == 'scheduled'
        # Get most recent job from cronjob
        else
          # Get pod from deployment
        end
        # Use normalized label selector for pod discovery
        label_selector = CLI::Helpers::LabelUtils.agent_pod_selector(name)

        # Use kubectl logs with label selector
        cmd = "#{ctx.kubectl_prefix} logs -l #{label_selector} #{tail_arg} #{follow_arg} --all-containers"

        Formatters::ProgressFormatter.info("Streaming logs for agent '#{name}'...")
        puts

        # Track threads and interruption state for cleanup
        stdout_thread = nil
        stderr_thread = nil
        interrupted = false

        # Install signal handler for graceful interruption
        original_int_handler = Signal.trap('INT') do
          interrupted = true
          stdout_thread&.terminate
          stderr_thread&.terminate
          puts "\n[Interrupted]"
          exit(0)
        end

        begin
          # Stream raw logs in real-time without formatting
          Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
            # Close unused stdin immediately to prevent resource leak
            stdin.close

            # Handle stdout (logs)
            stdout_thread = Thread.new do
              stdout.each_line do |line|
                break if interrupted

                puts line
                $stdout.flush
              end
            rescue IOError
              # Expected when stream is closed during interruption
            end

            # Handle stderr (errors)
            stderr_thread = Thread.new do
              stderr.each_line do |line|
                break if interrupted

                warn line
              end
            rescue IOError
              # Expected when stream is closed during interruption
            end

            # Wait for both streams to complete or interruption
            stdout_thread.join unless interrupted
            stderr_thread.join unless interrupted

            # Check exit status if not interrupted
            unless interrupted
              exit_status = wait_thr.value
              exit exit_status.exitstatus unless exit_status.success?
            end
          end
        ensure
          # Restore original signal handler
          Signal.trap('INT', original_int_handler)

          # Cleanup threads if they're still running
          stdout_thread&.terminate
          stderr_thread&.terminate
        end
      end
    end
  end
end