ez_logs_agent 0.1.3

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.
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EzLogsAgent
4
+ # Canonical sanitizer for "user-supplied data we're about to ship over
5
+ # the wire": HTTP params, GraphQL variables, and ActiveJob/Sidekiq
6
+ # arguments.
7
+ #
8
+ # Rules:
9
+ # - Sensitive keys (password / token / secret / api_key / credit_card,
10
+ # plus user-configured patterns) are replaced with "[FILTERED]".
11
+ # - Nested objects recurse up to MAX_NESTING_DEPTH (3); anything deeper
12
+ # collapses to "[Object]" so we never serialize unbounded graphs.
13
+ # - Arrays: small primitive arrays pass through; large ones (>5)
14
+ # collapse to a preview-plus-count string; arrays of objects are
15
+ # sanitized element-by-element with the same depth budget.
16
+ # - Non-primitive Ruby objects (anything not String/Numeric/Bool/nil)
17
+ # become "[Object]". This protects us from accidentally serializing
18
+ # ActiveRecord instances or other big graphs that found their way
19
+ # into a job argument.
20
+ #
21
+ # The module is pure (no I/O, no state), so it's safe to call from
22
+ # any thread.
23
+ module Sanitizer
24
+ # Default sensitive-key patterns. Matched case-insensitively as
25
+ # SUBSTRINGS of the key, so `customer_password` matches `password`.
26
+ SENSITIVE_PATTERNS = %w[
27
+ password passwd pwd
28
+ token access_token refresh_token api_token auth_token
29
+ secret api_secret client_secret
30
+ api_key apikey private_key privatekey secret_key secretkey
31
+ credential auth authorization
32
+ encrypted encrypted_data
33
+ ssn social_security
34
+ credit_card card_number cvv cvc
35
+ ].freeze
36
+
37
+ # Hard ceiling for nested object recursion. Deeper structures
38
+ # collapse to the literal string "[Object]".
39
+ MAX_NESTING_DEPTH = 3
40
+
41
+ # Threshold above which an array is summarized instead of inlined.
42
+ # Below this size, primitive arrays are shipped verbatim; arrays
43
+ # of objects are mapped element-by-element.
44
+ MAX_ARRAY_DISPLAY_SIZE = 5
45
+
46
+ class << self
47
+ # Sanitize a single key/value pair. Public entry point used by
48
+ # HTTP-param and job-arg sanitization.
49
+ #
50
+ # @param key [String, Symbol] Variable / parameter name.
51
+ # @param value [Object] Variable / parameter value.
52
+ # @param depth [Integer] Current recursion depth.
53
+ # @return [Object] Sanitized value (may be the original primitive).
54
+ def sanitize_value(key, value, depth = 0)
55
+ return "[FILTERED]" if sensitive_key?(key)
56
+ return sanitize_nested_object(value, depth) if value.is_a?(Hash)
57
+ return sanitize_array_value(value, depth) if value.is_a?(Array)
58
+ return value if primitive?(value)
59
+
60
+ # Anything else (AR records, dates, custom objects) collapses to
61
+ # a placeholder so we never accidentally serialize a huge graph.
62
+ "[Object]"
63
+ end
64
+
65
+ # Sanitize an ordered list of job arguments (positional). Returns
66
+ # an Array with each element sanitized as if its index were the
67
+ # key (no sensitive-key match for integers — only the nested
68
+ # structure matters at the top level).
69
+ #
70
+ # @param args [Array] Job arguments array.
71
+ # @return [Array] Sanitized arguments array.
72
+ def sanitize_args(args)
73
+ return [] unless args.is_a?(Array)
74
+
75
+ # Top-level array uses the same array-rules so giant arg lists
76
+ # truncate to a preview-with-count rather than ship verbatim.
77
+ sanitize_array_value(args, 0)
78
+ end
79
+
80
+ # Check whether a key matches a sensitive pattern. Public so the
81
+ # HTTP middleware can short-circuit early on identical keys.
82
+ #
83
+ # @param key [String, Symbol]
84
+ # @return [Boolean]
85
+ def sensitive_key?(key)
86
+ key_lower = key.to_s.downcase
87
+ return true if SENSITIVE_PATTERNS.any? { |pattern| key_lower.include?(pattern) }
88
+
89
+ user_patterns = EzLogsAgent.configuration.excluded_graphql_variable_keys || []
90
+ user_patterns.any? { |pattern| key_lower.include?(pattern.to_s.downcase) }
91
+ rescue
92
+ # Defensive: when in doubt, treat as sensitive.
93
+ true
94
+ end
95
+
96
+ private
97
+
98
+ def sanitize_nested_object(hash, depth)
99
+ return "[Object]" if depth >= MAX_NESTING_DEPTH
100
+ return {} if hash.empty?
101
+
102
+ hash.each_with_object({}) do |(key, value), result|
103
+ result[key] = sanitize_value(key, value, depth + 1)
104
+ end
105
+ end
106
+
107
+ def sanitize_array_value(array, depth)
108
+ return [] if array.empty?
109
+
110
+ all_primitives = array.all? { |item| primitive?(item) }
111
+
112
+ if all_primitives
113
+ if array.size <= MAX_ARRAY_DISPLAY_SIZE
114
+ array
115
+ else
116
+ preview = array.first(MAX_ARRAY_DISPLAY_SIZE)
117
+ "#{preview}... (#{array.size} total)"
118
+ end
119
+ else
120
+ if array.size <= MAX_ARRAY_DISPLAY_SIZE
121
+ array.map { |item| sanitize_array_item(item, depth) }
122
+ else
123
+ preview = array.first(MAX_ARRAY_DISPLAY_SIZE).map { |item| sanitize_array_item(item, depth) }
124
+ { "_truncated" => true, "_count" => array.size, "_preview" => preview }
125
+ end
126
+ end
127
+ end
128
+
129
+ def sanitize_array_item(item, depth)
130
+ if primitive?(item)
131
+ item
132
+ elsif item.is_a?(Hash)
133
+ sanitize_nested_object(item, depth + 1)
134
+ elsif item.is_a?(Array)
135
+ sanitize_array_value(item, depth + 1)
136
+ else
137
+ "[Object]"
138
+ end
139
+ end
140
+
141
+ def primitive?(value)
142
+ value.nil? ||
143
+ value.is_a?(String) ||
144
+ value.is_a?(Numeric) ||
145
+ value.is_a?(TrueClass) ||
146
+ value.is_a?(FalseClass)
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module EzLogsAgent
8
+ # HTTP Transport Client
9
+ #
10
+ # Lowest-level component that sends events to the EzLogs server.
11
+ # Performs a single HTTP POST request with no retries or backoff.
12
+ #
13
+ # Responsibilities:
14
+ # - Serialize events to JSON
15
+ # - POST to server endpoint
16
+ # - Return :success or :failure
17
+ #
18
+ # Does NOT:
19
+ # - Retry failed requests
20
+ # - Implement backoff logic
21
+ # - Read from Buffer
22
+ # - Call Buffer.flush
23
+ # - Raise exceptions to host app
24
+ class Transport
25
+ class << self
26
+ # Send events to the server
27
+ #
28
+ # @param events [Array<Hash>] Array of event hashes
29
+ # @return [Symbol] :success if 2xx response, :failure otherwise
30
+ def send(events)
31
+ return :success if events.nil? || events.empty?
32
+
33
+ response = post_events(events)
34
+ classify_response(response)
35
+ rescue => error
36
+ Logger.error("[Transport] send failed: #{error.class} - #{error.message}")
37
+ :failure
38
+ end
39
+
40
+ private
41
+
42
+ def post_events(events)
43
+ uri = build_uri
44
+ http = build_http_client(uri)
45
+ request = build_request(uri, events)
46
+
47
+ http.request(request)
48
+ end
49
+
50
+ def build_uri
51
+ server_url = EzLogsAgent.configuration.server_url
52
+ raise "server_url not configured" if server_url.nil? || server_url.empty?
53
+
54
+ URI.parse("#{server_url}/api/events")
55
+ rescue => error
56
+ raise "Invalid server_url: #{error.message}"
57
+ end
58
+
59
+ def build_http_client(uri)
60
+ http = Net::HTTP.new(uri.host, uri.port)
61
+ http.use_ssl = (uri.scheme == "https")
62
+ http.open_timeout = 5
63
+ http.read_timeout = 10
64
+ http
65
+ end
66
+
67
+ def build_request(uri, events)
68
+ request = Net::HTTP::Post.new(uri.path)
69
+ request["Content-Type"] = "application/json"
70
+
71
+ token = EzLogsAgent.configuration.project_token
72
+ request["Authorization"] = "Bearer #{token}" if token
73
+
74
+ request.body = JSON.generate({ events: events })
75
+ request
76
+ end
77
+
78
+ def classify_response(response)
79
+ status = response.code.to_i
80
+
81
+ if status >= 200 && status < 300
82
+ Logger.debug("[Transport] send succeeded (#{status})")
83
+ :success
84
+ else
85
+ Logger.error("[Transport] send failed (#{status})")
86
+ :failure
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EzLogsAgent
4
+ # Detects whether an HTTP request originated from an AI agent client
5
+ # (Claude Desktop, ChatGPT, Cursor, Windsurf, …) by inspecting the
6
+ # User-Agent string.
7
+ #
8
+ # Behavior is intentionally narrow: returns vendor metadata only when
9
+ # the UA starts with one of the known LLM-client prefixes. Anything
10
+ # else returns nil — we never *infer* agency from weak signals.
11
+ #
12
+ # The Next.js agent ships an identical detector
13
+ # (`src/user-agent-detector.ts`) so both wire-format emitters classify
14
+ # the same UA the same way.
15
+ module UserAgentDetector
16
+ # Patterns are matched case-insensitively against the start of the
17
+ # User-Agent string. New vendors must be added in BOTH agents.
18
+ PATTERNS = [
19
+ { prefix: "claude-", vendor: "claude", label: "Claude" },
20
+ { prefix: "anthropic-", vendor: "claude", label: "Claude" },
21
+ { prefix: "gpt-", vendor: "openai", label: "ChatGPT" },
22
+ { prefix: "chatgpt-", vendor: "openai", label: "ChatGPT" },
23
+ { prefix: "openai-", vendor: "openai", label: "ChatGPT" },
24
+ { prefix: "cursor-", vendor: "cursor", label: "Cursor" },
25
+ { prefix: "windsurf-", vendor: "windsurf", label: "Windsurf" }
26
+ ].freeze
27
+
28
+ # Classify a User-Agent string.
29
+ #
30
+ # @param user_agent [String, nil] Raw User-Agent header value.
31
+ # @return [Hash, nil] `{ kind: "agent", vendor:, label:, suggested_id: }`
32
+ # when the UA matches a known LLM client; nil otherwise.
33
+ def self.classify(user_agent)
34
+ return nil if user_agent.nil? || user_agent.empty?
35
+
36
+ ua = user_agent.downcase
37
+ match = PATTERNS.find { |p| ua.start_with?(p[:prefix]) }
38
+ return nil unless match
39
+
40
+ {
41
+ kind: "agent",
42
+ vendor: match[:vendor],
43
+ label: match[:label],
44
+ suggested_id: "agent:#{match[:vendor]}"
45
+ }
46
+ rescue
47
+ # Defensive: classifier must never crash the middleware.
48
+ nil
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EzLogsAgent
4
+ VERSION = "0.1.3"
5
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ez_logs_agent/version"
4
+ require_relative "ez_logs_agent/configuration"
5
+ require_relative "ez_logs_agent/configuration_validator"
6
+ require_relative "ez_logs_agent/logger"
7
+ require_relative "ez_logs_agent/correlation"
8
+ require_relative "ez_logs_agent/actor_validator"
9
+ require_relative "ez_logs_agent/actor"
10
+ require_relative "ez_logs_agent/user_agent_detector"
11
+ require_relative "ez_logs_agent/sanitizer"
12
+ require_relative "ez_logs_agent/event_builder"
13
+ require_relative "ez_logs_agent/resource_extractor"
14
+ require_relative "ez_logs_agent/buffer"
15
+ require_relative "ez_logs_agent/transport"
16
+ require_relative "ez_logs_agent/retry_sender"
17
+ require_relative "ez_logs_agent/flush_scheduler"
18
+ require_relative "ez_logs_agent/middleware/http_request"
19
+ require_relative "ez_logs_agent/capturers/job_capturer"
20
+ require_relative "ez_logs_agent/capturers/active_job_capturer"
21
+ require_relative "ez_logs_agent/capturers/database_capturer"
22
+
23
+ # Load Railtie only when Rails is present
24
+ require_relative "ez_logs_agent/railtie" if defined?(Rails::Railtie)
25
+
26
+ module EzLogsAgent
27
+ class Error < StandardError; end
28
+
29
+ class << self
30
+ attr_writer :configuration
31
+
32
+ def configuration
33
+ @configuration ||= Configuration.new
34
+ end
35
+
36
+ def configure
37
+ yield(configuration)
38
+ end
39
+
40
+ def reset_configuration!
41
+ @configuration = Configuration.new
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module EzLogsAgent
6
+ module Generators
7
+ class InstallGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Creates an EzLogsAgent initializer with all configuration options"
11
+
12
+ def show_welcome_message
13
+ say "\n"
14
+ say "=" * 70, :green
15
+ say " EzLogs Agent Installation", :green
16
+ say "=" * 70, :green
17
+ say "\n"
18
+ end
19
+
20
+ def detect_environment
21
+ @sidekiq_detected = defined?(Sidekiq)
22
+ @activejob_detected = defined?(ActiveJob)
23
+ @activerecord_detected = defined?(ActiveRecord)
24
+ @devise_detected = defined?(Devise)
25
+
26
+ say "Detected Components:", :cyan
27
+ rails_version = defined?(Rails::VERSION::STRING) ? Rails::VERSION::STRING : "unknown"
28
+ say " • Rails #{rails_version}"
29
+ say " • Sidekiq #{Sidekiq::VERSION}", :green if @sidekiq_detected
30
+ say " • ActiveJob", :green if @activejob_detected && !@sidekiq_detected
31
+ say " • ActiveRecord", :green if @activerecord_detected
32
+ say " • Devise", :green if @devise_detected
33
+ say "\n"
34
+ end
35
+
36
+ def create_initializer_file
37
+ initializer_path = File.join(destination_root, "config/initializers/ez_logs_agent.rb")
38
+
39
+ if File.exist?(initializer_path)
40
+ say "⚠️ Initializer already exists at config/initializers/ez_logs_agent.rb", :yellow
41
+ say " Skipping creation to avoid overwriting your existing configuration.", :yellow
42
+ say "\n"
43
+ return
44
+ end
45
+
46
+ template "ez_logs_agent.rb.tt", "config/initializers/ez_logs_agent.rb"
47
+ say "\n"
48
+ say "✅ Created config/initializers/ez_logs_agent.rb", :green
49
+ say "\n"
50
+ end
51
+
52
+ def show_next_steps
53
+ say "=" * 70, :cyan
54
+ say " Next Steps", :cyan
55
+ say "=" * 70, :cyan
56
+ say "\n"
57
+
58
+ say "1. Get your API key from EzLogs:", :bold
59
+ say " → Log in to your EzLogs dashboard"
60
+ say " → Go to Settings → API Keys"
61
+ say " → Create a new API key for this application\n\n"
62
+
63
+ say "2. Configure the agent:", :bold
64
+ say " → Open config/initializers/ez_logs_agent.rb"
65
+ say " → Set your server_url (e.g., https://app.ezlogs.io)"
66
+ say " → Set your project_token (the API key from step 1)\n\n"
67
+
68
+ if @devise_detected
69
+ say "3. Enable actor tracking (optional but recommended):", :bold
70
+ say " → Uncomment the actor_from_request block"
71
+ say " → The Devise example is already configured for you\n\n"
72
+ else
73
+ say "3. Enable actor tracking (optional):", :bold
74
+ say " → Uncomment the actor_from_request block in the initializer"
75
+ say " → Customize it for your authentication system\n\n"
76
+ end
77
+
78
+ say "4. Test the connection:", :bold
79
+ say " → Run: rails ez_logs_agent:test_connection"
80
+ say " → This verifies your configuration is correct\n\n"
81
+
82
+ say "5. Restart your Rails server:", :bold
83
+ say " → The agent will start capturing events automatically"
84
+ say " → Check your EzLogs dashboard to see activity\n\n"
85
+
86
+ say "=" * 70, :cyan
87
+ say "\n"
88
+
89
+ say "Need help? Check the README or visit https://docs.ezlogs.io", :cyan
90
+ say "\n"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ # EzLogsAgent Configuration
4
+ #
5
+ # All options below show their default values.
6
+ # Uncomment and change only the options you need to customize.
7
+
8
+ EzLogsAgent.configure do |config|
9
+ # =============================================================================
10
+ # REQUIRED SETTINGS
11
+ # =============================================================================
12
+
13
+ # URL of the EzLogs server (required)
14
+ # Default: nil (must be set for events to be sent)
15
+ # config.server_url = "https://your-ezlogs-server.com"
16
+
17
+ # API key for authentication (required)
18
+ # Get this from your EzLogs dashboard: Settings → API Keys
19
+ # Default: nil
20
+ # config.project_token = "your-api-key-here"
21
+
22
+ # =============================================================================
23
+ # CAPTURE SETTINGS
24
+ # =============================================================================
25
+
26
+ # Capture HTTP requests
27
+ # Default: true
28
+ # config.capture_http = true
29
+
30
+ # Capture background jobs (Sidekiq/ActiveJob)
31
+ # Default: true
32
+ # config.capture_jobs = true
33
+
34
+ # Capture database changes via model callbacks
35
+ # Default: true
36
+ # config.capture_database = true
37
+
38
+ # =============================================================================
39
+ # EXCLUSIONS
40
+ # =============================================================================
41
+
42
+ # Additional paths to exclude from HTTP capture (added to built-in defaults)
43
+ # Built-in: /rails/active_storage*, /assets*, /packs*, /vite*, /health*, /up, /favicon.ico
44
+ # config.excluded_paths = ["/admin*", "/internal*"]
45
+
46
+ # Additional tables to exclude from database capture (added to built-in defaults)
47
+ # Built-in: schema_migrations, ar_internal_metadata, sessions, session, active_storage_*, solid_queue_*, etc.
48
+ # config.excluded_tables = ["audit_logs", "versions"]
49
+
50
+ # Additional job classes to exclude from background job capture (added to built-in defaults)
51
+ # Built-in: SidekiqAlive::Worker, SolidQueue::CleanupJob, SolidQueue::RecurringJob
52
+ # config.excluded_job_classes = ["MyApp::HealthCheckJob", "MyApp::MetricsJob"]
53
+
54
+ # Additional GraphQL operations to exclude from capture (added to built-in defaults)
55
+ # Built-in: IntrospectionQuery, __* (all introspection fields like __schema, __type)
56
+ # Supports exact match and prefix match (patterns ending with *)
57
+ # config.excluded_graphql_operations = ["GetCurrentUser", "Internal*"]
58
+
59
+ # =============================================================================
60
+ # PERFORMANCE & RELIABILITY
61
+ # =============================================================================
62
+
63
+ # Max events in memory before oldest are dropped
64
+ # Default: 10000 (optimized for high-volume workloads)
65
+ # config.buffer_size = 10000
66
+
67
+ # Retry attempts for failed sends
68
+ # Default: 3
69
+ # config.retry_attempts = 3
70
+
71
+ # Seconds between automatic flushes
72
+ # Default: 3 (more frequent sends for better throughput)
73
+ # config.send_interval = 3
74
+
75
+ # =============================================================================
76
+ # LOGGING
77
+ # =============================================================================
78
+
79
+ # Agent log level (:debug, :info, :warn, :error)
80
+ # Default: :warn
81
+ # Set to :error in production to minimize output
82
+ # config.log_level = :warn
83
+
84
+ # =============================================================================
85
+ # DISPLAY NAMES (What Resource Was Affected?)
86
+ # =============================================================================
87
+
88
+ # Configure display name resolution per model
89
+ # When a database callback fires, the agent resolves a human-readable name
90
+ # for the affected record using the configured field.
91
+ #
92
+ # Fallback chain: configured field → name → title → number → nil
93
+ #
94
+ # Example:
95
+ # config.display_name_for = {
96
+ # "User" => :email, # Use email field for User models
97
+ # "Product" => :name, # Use name field for Product models
98
+ # "Order" => :number # Use number field for Order models
99
+ # }
100
+ #
101
+ # This enables action titles like:
102
+ # - "User updated 'john@example.com'" instead of just "User updated"
103
+ # - "Product created 'Premium Widget'" instead of just "Product created"
104
+ #
105
+ # IMPORTANT: Only use direct attributes, not associations (would trigger DB queries)
106
+
107
+ # =============================================================================
108
+ # ACTOR CONTEXT (Who Triggered This?)
109
+ # =============================================================================
110
+
111
+ # Optional: Extract actor (user identity) from HTTP requests
112
+ # This enables "who triggered this action?" tracking in EzLogs
113
+ #
114
+ # The hook receives (env, controller) and should return:
115
+ # { id: String, label: String } or nil if actor cannot be determined
116
+ #
117
+ # Example for Devise:
118
+ # config.actor_from_request = ->(env, controller) {
119
+ # return nil unless controller.respond_to?(:current_user)
120
+ # user = controller.current_user
121
+ # return nil unless user
122
+ #
123
+ # {
124
+ # id: user.id.to_s,
125
+ # label: user.email
126
+ # }
127
+ # }
128
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :ez_logs_agent do
4
+ desc "Test connection to EzLogs server"
5
+ task test_connection: :environment do
6
+ require "ez_logs_agent"
7
+
8
+ puts "\n🔍 EzLogs Agent Connection Test\n\n"
9
+
10
+ # Step 1: Validate configuration
11
+ puts "Step 1: Validating configuration..."
12
+ result = EzLogsAgent::ConfigurationValidator.validate(EzLogsAgent.configuration)
13
+
14
+ if result.errors.any?
15
+ puts "❌ Configuration validation failed:\n"
16
+ result.errors.each do |error|
17
+ puts " - #{error}"
18
+ end
19
+ puts "\n"
20
+ exit 1
21
+ end
22
+
23
+ if result.warnings.any?
24
+ puts "⚠️ Configuration warnings:\n"
25
+ result.warnings.each do |warning|
26
+ puts " - #{warning}"
27
+ end
28
+ puts "\n"
29
+ end
30
+
31
+ puts "✅ Configuration is valid\n\n"
32
+
33
+ # Step 2: Show configuration summary
34
+ config = EzLogsAgent.configuration
35
+ puts "Configuration Summary:"
36
+ puts " Server URL: #{config.server_url}"
37
+ puts " Project Token: #{config.project_token ? '***' + config.project_token[-4..-1] : '(not set)'}"
38
+ puts " Capture HTTP: #{config.capture_http}"
39
+ puts " Capture Jobs: #{config.capture_jobs}"
40
+ puts " Capture Database: #{config.capture_database}"
41
+ puts "\n"
42
+
43
+ # Step 2: Test connection
44
+ puts "Step 2: Testing connection to server..."
45
+
46
+ begin
47
+ uri = URI.parse(config.server_url)
48
+ uri.path = "/api/events" if uri.path.empty?
49
+
50
+ http = Net::HTTP.new(uri.host, uri.port)
51
+ http.use_ssl = uri.scheme == "https"
52
+ http.open_timeout = 5
53
+ http.read_timeout = 10
54
+
55
+ # Send a test event
56
+ test_event = {
57
+ kind: "test",
58
+ correlation_id: "test_connection_#{Time.now.to_i}",
59
+ occurred_at: Time.now.utc.iso8601(3),
60
+ context: {
61
+ message: "EzLogs Agent connection test",
62
+ source: "rake ez_logs_agent:test_connection"
63
+ }
64
+ }
65
+
66
+ request = Net::HTTP::Post.new(uri.path)
67
+ request["Content-Type"] = "application/json"
68
+ request["Authorization"] = "Bearer #{config.project_token}" if config.project_token
69
+ request.body = { events: [test_event] }.to_json
70
+
71
+ response = http.request(request)
72
+
73
+ case response.code.to_i
74
+ when 200..299
75
+ puts "✅ Connection successful (HTTP #{response.code})"
76
+ puts " Test event accepted by server\n\n"
77
+ puts "✅ All checks passed! EzLogs Agent is configured correctly.\n\n"
78
+ exit 0
79
+ when 401
80
+ puts "❌ Authentication failed (HTTP 401)"
81
+ puts " Check your project_token in config/initializers/ez_logs_agent.rb\n\n"
82
+ exit 1
83
+ when 404
84
+ puts "❌ Server endpoint not found (HTTP 404)"
85
+ puts " Check your server_url: #{config.server_url}\n\n"
86
+ exit 1
87
+ else
88
+ puts "❌ Server responded with HTTP #{response.code}"
89
+ puts " Response: #{response.body[0..200]}\n\n"
90
+ exit 1
91
+ end
92
+ rescue SocketError => e
93
+ puts "❌ Could not resolve host: #{e.message}"
94
+ puts " Check your server_url: #{config.server_url}\n\n"
95
+ exit 1
96
+ rescue Errno::ECONNREFUSED => e
97
+ puts "❌ Connection refused: #{e.message}"
98
+ puts " Is the EzLogs server running at #{config.server_url}?\n\n"
99
+ exit 1
100
+ rescue Timeout::Error => e
101
+ puts "❌ Connection timeout: #{e.message}"
102
+ puts " The server is not responding. Check firewall rules.\n\n"
103
+ exit 1
104
+ rescue StandardError => e
105
+ puts "❌ Connection failed: #{e.class} - #{e.message}"
106
+ puts " #{e.backtrace.first}\n\n"
107
+ exit 1
108
+ end
109
+ end
110
+ end