supports collecting metrics and logs (#7)
supports collecting metrics and logs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80a611c..d8655ff 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
==================
Release Notes.
+0.2.0
+------------------
+#### Features
+- Supports collecting metrics and logs.
+
+#### Plugins
+
+#### Documentation
+
0.1.0
------------------
#### Features
diff --git a/Gemfile b/Gemfile
index 3f5e0e4..993d132 100644
--- a/Gemfile
+++ b/Gemfile
@@ -17,4 +17,8 @@
gemspec name: 'skywalking'
-ruby ">= 3.0.0"
\ No newline at end of file
+ruby ">= 3.0.0"
+
+# Optional dependencies for enhanced performance
+# Uncomment the following line to enable FFI for better system call performance
+# gem 'ffi', '~> 1.17'
\ No newline at end of file
diff --git a/docs/en/agent/meter-log-reporter.md b/docs/en/agent/meter-log-reporter.md
new file mode 100644
index 0000000..89128c6
--- /dev/null
+++ b/docs/en/agent/meter-log-reporter.md
@@ -0,0 +1,59 @@
+# Ruby Agent Meter and Log Reporter
+
+The meter reporter feature enables collection and reporting of runtime metrics to the SkyWalking OAP backend.
+
+### Runtime Metrics (Enabled by Default)
+
+The agent automatically collects Ruby runtime metrics when `meter_reporter_active` is enabled (default: true).
+
+#### Collected Runtime Metrics
+
+**CPU Metrics:**
+
+- `instance_ruby_cpu_usage_percent` - Ruby process CPU usage percentage
+
+**Memory Metrics:**
+
+- `instance_ruby_memory_rss_mb` - Ruby process RSS memory usage in MB
+- `instance_ruby_memory_usage_percent` - Ruby process memory usage percentage
+
+**Garbage Collection Metrics:**
+
+- `instance_ruby_gc_count_total` - Total GC execution count
+- `instance_ruby_gc_minor_count_total` - Minor GC count (if available)
+- `instance_ruby_gc_major_count_total` - Major GC count (if available)
+- `instance_ruby_gc_time_total` - Total GC time in milliseconds (cumulative)
+- `instance_ruby_heap_usage_percent` - Heap memory usage percentage
+- `instance_ruby_heap_live_slots_count` - Number of live heap slots
+- `instance_ruby_heap_available_slots_count` - Number of available heap slots
+
+**Thread Metrics:**
+
+- `instance_ruby_thread_count_active` - Number of active threads (all alive threads)
+- `instance_ruby_thread_count_running` - Number of threads in running state
+
+### Log Integration
+
+When log reporter is enabled, the agent automatically:
+
+- **Intercepts Ruby Standard Logger**: Automatically patches the `Logger` class to collect logs
+- **Integrates Trace Context**: Adds trace ID, segment ID, and span ID to log messages when available
+
+#### Supported Logger
+
+Currently, the agent supports:
+
+- **Ruby Standard Logger** (`Logger` class) - Automatically intercepted and collected
+
+#### Log Configuration Options
+
+```ruby
+# Configure log reporter level (default: Logger::INFO)
+config[:log_reporter_level] = Logger::INFO
+
+# Configure log report period in seconds (default: 5)
+config[:log_report_period] = 5
+
+# Configure maximum log queue size (default: 1000)
+config[:max_log_queue_size] = 1000
+```
diff --git a/docs/en/setup/quick-start.md b/docs/en/setup/quick-start.md
index 13670c7..4434057 100644
--- a/docs/en/setup/quick-start.md
+++ b/docs/en/setup/quick-start.md
@@ -33,7 +33,7 @@
If successful, the following will be displayed:
-```ruby
+```shell
Successfully built RubyGem
Name: skywalking
Version: <version>
@@ -84,27 +84,39 @@
Skywalking.start(
service_name: 'sw-srv',
instance_name: 'sw-inst',
- collector_backend_services: 'oap:11800'
+ collector_backend_services: 'oap:11800',
+ meter_reporter_active: true,
+ log_reporter_active: true,
+ meter_report_period: 30,
+ log_report_period: 10
)
~~~
The following is an example of a configuration file:
+
~~~yaml
common: &defaults
service_name: Ruby-Agent-Common
log_level: debug
+ meter_reporter_active: true
+ log_reporter_active: true
+ meter_report_period: 20
+ log_report_period: 5
development:
<<: *defaults
service_name: Ruby-Agent-Development
+ log_reporter_level: 0 # DEBUG
test:
<<: *defaults
service_name: Ruby-Agent-Test
+ log_reporter_level: 1 # INFO
production:
<<: *defaults
service_name: Ruby-Agent-Production
+ log_reporter_level: 2 # WARN
~~~
The following lists all the configuration options:
@@ -127,3 +139,10 @@
| collector_heartbeat_period | SW_AGENT_COLLECTOR_HEARTBEAT_PERIOD | 30 | he agent will send heartbeat to OAP every `collector_heartbeat_period` seconds. |
| properties_report_period_factor | SW_AGENT_PROPERTIES_REPORT_PERIOD_FACTOR | 10 | The agent will report service instance properties every `collector_heartbeat_period * properties_report_period_factor` seconds. |
| max_queue_size | SW_AGENT_MAX_QUEUE_SIZE | 10000 | The maximum queue size for reporting data. |
+| meter_reporter_active | SW_AGENT_METER_REPORTER_ACTIVE | true | Enable/disable meter reporter for runtime metrics collection. |
+| meter_report_period | SW_AGENT_METER_REPORT_PERIOD | 60 | Meter report period in seconds. |
+| max_meter_queue_size | SW_AGENT_MAX_METER_QUEUE_SIZE | 1000 | Maximum meter queue size for buffering metrics data. |
+| log_reporter_active | SW_AGENT_LOG_REPORTER_ACTIVE | true | Enable/disable log reporter for log collection. |
+| log_reporter_level | SW_AGENT_LOG_REPORTER_LEVEL | 1 (INFO) | Minimum log level to report (Logger::DEBUG=0, INFO=1, WARN=2, ERROR=3, FATAL=4). |
+| log_report_period | SW_AGENT_LOG_REPORT_PERIOD | 5 | Log report period in seconds. |
+| max_log_queue_size | SW_AGENT_MAX_LOG_QUEUE_SIZE | 1000 | Maximum log queue size for buffering log data. |
diff --git a/docs/menu.yml b/docs/menu.yml
index 2fa797f..c283149 100644
--- a/docs/menu.yml
+++ b/docs/menu.yml
@@ -23,6 +23,8 @@
catalog:
- name: Supported Plugins
path: /en/agent/plugins
+ - name: Meter and Log Report
+ path: /en/agent/meter-and-log-report
- name: Development and Contribution
catalog:
- name: How to Release
diff --git a/lib/skywalking/agent.rb b/lib/skywalking/agent.rb
index 2c61272..977de01 100644
--- a/lib/skywalking/agent.rb
+++ b/lib/skywalking/agent.rb
@@ -17,6 +17,7 @@
require_relative 'environment'
require_relative 'plugins_manager'
require_relative 'reporter/report'
+require_relative 'reporter/log_buffer_trigger'
require_relative 'tracing/span_context'
require_relative 'tracing/carrier_item'
require_relative 'tracing/segment'
@@ -62,13 +63,21 @@
end
attr_reader :logger, :agent_config
+
+ # Get the singleton instance
+ # @return [Agent, nil] the agent instance or nil if not started
+ def instance
+ @agent
+ end
end
- attr_reader :plugins, :reporter
+ attr_reader :plugins, :reporter, :log_buffer, :config
def initialize(config)
+ @config = config
@plugins = Plugins::PluginsManager.new(config)
@reporter = Reporter::Report.new(config)
+ @log_buffer = Reporter::LogBufferTrigger.new(config)
add_shutdown_hook
end
@@ -80,6 +89,9 @@
def start!
@plugins.init_plugins
@reporter.init_reporter
+ # Start log reporting thread
+ start_log_reporting_thread if @config[:log_reporter_active]
+
self
end
@@ -94,5 +106,46 @@
shutdown
end
end
+
+ # Check if log reporter is active
+ # @return [Boolean] true if log reporter is active
+ def log_reporter_active?
+ @config[:log_reporter_active]
+ end
+
+ private
+
+ # Start the log reporting thread
+ def start_log_reporting_thread
+ Thread.new do
+ loop do
+ break unless log_reporter_active?
+
+ process_log_queue
+ sleep @config[:log_report_period]
+ end
+ end
+ end
+
+ # Process the log queue and send data to the server
+ def process_log_queue
+ log_count = 0
+ enumerator = Enumerator.new do |yielder|
+ while (log_data = log_buffer.stream_data)
+ log_data.each do |data|
+ log_count += 1
+ yielder << data
+ end
+ end
+ end
+
+ enumerator.each_slice(10) do |batch|
+ begin
+ reporter.report_log(batch)
+ rescue => e
+ Agent.logger.warn "Failed to report log data: #{e.message}"
+ end
+ end
+ end
end
end
diff --git a/lib/skywalking/configuration.rb b/lib/skywalking/configuration.rb
index 6efe3ae..0e4dc16 100644
--- a/lib/skywalking/configuration.rb
+++ b/lib/skywalking/configuration.rb
@@ -100,6 +100,46 @@
default: 10000,
desc: 'The maximum queue size for reporting data'
},
+ :meter_reporter_active => {
+ type: :bool,
+ default: true,
+ desc: 'Enable meter reporter'
+ },
+ :runtime_meter_reporter_active => {
+ type: :bool,
+ default: true,
+ desc: 'Enable Ruby runtime meter reporter'
+ },
+ :meter_report_period => {
+ type: :int,
+ default: 60,
+ desc: 'Meter report period in seconds'
+ },
+ :max_meter_queue_size => {
+ type: :int,
+ default: 1000,
+ desc: 'Maximum meter queue size'
+ },
+ :log_reporter_active => {
+ type: :bool,
+ default: true,
+ desc: 'Enable log reporter'
+ },
+ :log_reporter_level => {
+ type: :int,
+ default: Logger::INFO,
+ desc: 'Minimum log level to report (Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR, Logger::FATAL)'
+ },
+ :log_report_period => {
+ type: :int,
+ default: 5,
+ desc: 'Log report period in seconds'
+ },
+ :max_log_queue_size => {
+ type: :int,
+ default: 1000,
+ desc: 'Maximum log queue size'
+ }
}.freeze
# @api private
@@ -173,18 +213,18 @@
next if env_value.nil?
type = env_schema[:type]
- case type
- when :string
- new_config[env_key] = env_value.to_s
- when :bool
- # rubocop:disable Performance/CollectionLiteralInLoop
- new_config[env_key] = !%w[0 false].include?(env_value.strip.downcase)
- # rubocop:enable Performance/CollectionLiteralInLoop
- when :int
- new_config[env_key] = env_value.to_s
- else
- env_value
- end
+ new_config[env_key] = case type
+ when :string
+ env_value.to_s
+ when :bool
+ # rubocop:disable Performance/CollectionLiteralInLoop
+ !%w[0 false].include?(env_value.strip.downcase)
+ # rubocop:enable Performance/CollectionLiteralInLoop
+ when :int
+ env_value.to_i
+ else
+ env_value
+ end
end
new_config
diff --git a/lib/skywalking/environment.rb b/lib/skywalking/environment.rb
index aa914e2..8fa1efd 100644
--- a/lib/skywalking/environment.rb
+++ b/lib/skywalking/environment.rb
@@ -37,7 +37,7 @@
rescue
nil
end
-
+
def env
::Rails.env
end
@@ -63,7 +63,7 @@
rescue
"Sinatra"
end
-
+
def env
ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
end
@@ -81,7 +81,7 @@
def app_name
"Ruby"
end
-
+
def env
ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development'
end
@@ -111,7 +111,7 @@
"."
end
end
-
+
def framework_env
@framework_env ||= framework_info.env
end
diff --git a/lib/skywalking/meter.rb b/lib/skywalking/meter.rb
new file mode 100644
index 0000000..7673fc5
--- /dev/null
+++ b/lib/skywalking/meter.rb
@@ -0,0 +1,29 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require_relative 'meter/base'
+require_relative 'meter/meter_service'
+require_relative 'meter/runtime/cpu_data_source'
+require_relative 'meter/runtime/mem_data_source'
+require_relative 'meter/runtime/gc_data_source'
+require_relative 'meter/runtime/thread_data_source'
+
+module Skywalking
+ # Main module for meter functionality
+ module Meter
+ # Classes are already defined through require statements above
+ # No need to reassign them to themselves
+ end
+end
diff --git a/lib/skywalking/meter/base.rb b/lib/skywalking/meter/base.rb
new file mode 100644
index 0000000..fac3f2b
--- /dev/null
+++ b/lib/skywalking/meter/base.rb
@@ -0,0 +1,79 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require_relative '../reporter/client/proto'
+
+module Skywalking
+ module Meter
+ # Base class for all data sources
+ class DataSource
+ # Automatically register all generator methods as gauges
+ # @param meter_service [MeterService] the service to register gauges with
+ def register(meter_service)
+ methods.grep(/_generator$/).each do |method_name|
+ metric_name = "instance_ruby_#{method_name.to_s.sub('_generator', '')}"
+ # Create a lambda that calls the generator method
+ getter = lambda { send(method_name) }
+ gauge = Gauge.new(metric_name, getter)
+ meter_service.register(gauge)
+ end
+ end
+ end
+
+ # Represents a gauge metric that reports instantaneous values
+ class Gauge
+ attr_reader :name
+
+ # @param name [String] metric name
+ # @param getter [Proc] a callable that returns the current value
+ def initialize(name, getter)
+ @name = name
+ @getter = getter
+ @labels = []
+ end
+
+ # Add a label to this gauge
+ # @param key [String] label key
+ # @param value [String] label value
+ # @return [self]
+ def add_label(key, value)
+ @labels << Label.new(name: key, value: value)
+ self
+ end
+
+ # Collect current metric value
+ # @return [MeterData] meter data
+ def collect
+ value = @getter.call
+ MeterData.new(
+ singleValue: MeterSingleValue.new(
+ name: @name,
+ value: value.to_f,
+ labels: @labels
+ )
+ )
+ rescue
+ # Return zero value if getter fails
+ MeterData.new(
+ singleValue: MeterSingleValue.new(
+ name: @name,
+ value: 0.0,
+ labels: @labels
+ )
+ )
+ end
+ end
+ end
+end
diff --git a/lib/skywalking/meter/meter_service.rb b/lib/skywalking/meter/meter_service.rb
new file mode 100644
index 0000000..0d8eff7
--- /dev/null
+++ b/lib/skywalking/meter/meter_service.rb
@@ -0,0 +1,99 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module Skywalking
+ module Meter
+ # Service to manage metric collection and reporting
+ class MeterService
+ include Log::Logging
+
+ # @param config [Hash] configuration
+ # @param meter_trigger [MeterBufferTrigger] buffer trigger for queuing data
+ def initialize(config, meter_trigger)
+ @config = config
+ @gauges = []
+ @meter_trigger = meter_trigger
+ @running = false
+ @mutex = Mutex.new
+ @collector_thread = nil
+ end
+
+ # Register a gauge for collection
+ # @param gauge [Gauge] the gauge to register
+ def register(gauge)
+ @mutex.synchronize do
+ @gauges << gauge
+ end
+ end
+
+ # Start the meter collection service
+ def start
+ return if @running
+
+ @running = true
+
+ @collector_thread = Thread.new do
+ Thread.current.name = 'MeterCollector'
+ run_collection_loop
+ end
+ end
+
+ # Stop the meter collection service
+ def stop
+ return unless @running
+
+ @running = false
+ @collector_thread&.join(5)
+ end
+
+ private
+
+ def run_collection_loop
+ period = @config[:meter_report_period]
+
+ while @running
+ begin
+ collect_and_queue_metrics
+ sleep period
+ rescue => e
+ error "Error in meter collection loop: #{e.message}"
+ error e.backtrace.join("\n")
+ sleep period
+ end
+ end
+ end
+
+ def collect_and_queue_metrics
+ @mutex.synchronize do
+ collected_count = 0
+ @gauges.each do |gauge|
+ begin
+ meter_data = gauge.collect
+ if meter_data
+ meter_data.service = @config[:service_name]
+ meter_data.serviceInstance = @config[:instance_name]
+ meter_data.timestamp = (Time.now.to_f * 1000).to_i
+ @meter_trigger << meter_data
+ collected_count += 1
+ end
+ rescue => e
+ warn "Error collecting gauge #{gauge.name}: #{e.message}"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/skywalking/meter/runtime/cpu_data_source.rb b/lib/skywalking/meter/runtime/cpu_data_source.rb
new file mode 100644
index 0000000..29269bc
--- /dev/null
+++ b/lib/skywalking/meter/runtime/cpu_data_source.rb
@@ -0,0 +1,57 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require_relative '../base'
+
+module Skywalking
+ module Meter
+ module Runtime
+ # DataSource for collecting Ruby process CPU usage metrics
+ class CpuDataSource < DataSource
+ def initialize
+ @last_cpu_time = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
+ @last_wall_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ end
+
+ # Ruby process CPU usage percentage
+ def cpu_usage_percent_generator
+ begin
+ current_cpu_time = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
+ current_wall_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+
+ cpu_time_diff = current_cpu_time - @last_cpu_time
+ wall_time_diff = current_wall_time - @last_wall_time
+
+ # Calculate CPU usage as percentage of wall time
+ if wall_time_diff > 0
+ cpu_usage = (cpu_time_diff / wall_time_diff) * 100.0
+ cpu_usage = [cpu_usage, 0.0].max
+ else
+ cpu_usage = 0.0
+ end
+
+ # Update last values for next calculation
+ @last_cpu_time = current_cpu_time
+ @last_wall_time = current_wall_time
+
+ cpu_usage
+ rescue
+ 0.0
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/skywalking/meter/runtime/gc_data_source.rb b/lib/skywalking/meter/runtime/gc_data_source.rb
new file mode 100644
index 0000000..428a709
--- /dev/null
+++ b/lib/skywalking/meter/runtime/gc_data_source.rb
@@ -0,0 +1,99 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require_relative '../base'
+
+module Skywalking
+ module Meter
+ module Runtime
+ # DataSource for collecting GC metrics
+ class GcDataSource < DataSource
+ def initialize
+ @cached_stats = nil
+ @cached_count = nil
+ @cache_time = 0
+ @cache_duration = 60
+ end
+
+ # Total GC count
+ def gc_count_total_generator
+ get_gc_data
+ @cached_count || 0
+ rescue
+ 0
+ end
+
+ # Minor GC count (if available)
+ def gc_minor_count_total_generator
+ stats = get_gc_stats
+ stats[:minor_gc_count] || 0
+ rescue
+ 0
+ end
+
+ # Major GC count (if available)
+ def gc_major_count_total_generator
+ stats = get_gc_stats
+ stats[:major_gc_count] || 0
+ rescue
+ 0
+ end
+
+ # GC total time
+ def gc_time_total_generator
+ stats = get_gc_stats
+ stats[:time] || 0
+ rescue
+ 0
+ end
+
+ # Heap usage percentage
+ def heap_usage_percent_generator
+ stats = get_gc_stats
+ if stats[:heap_available_slots] && stats[:heap_available_slots] > 0
+ (stats[:heap_live_slots].to_f / stats[:heap_available_slots] * 100)
+ else
+ 0.0
+ end
+ rescue
+ 0.0
+ end
+
+ private
+
+ # Get cached GC statistics, refresh if cache is expired
+ def get_gc_stats
+ current_time = Time.now.to_i
+ if @cached_stats.nil? || (current_time - @cache_time) > @cache_duration
+ @cached_stats = GC.stat
+ @cache_time = current_time
+ end
+ @cached_stats
+ end
+
+ # Get both GC.count and GC.stat data, refresh if cache is expired
+ def get_gc_data
+ current_time = Time.now.to_i
+ if @cached_stats.nil? || (current_time - @cache_time) > @cache_duration
+ @cached_stats = GC.stat
+ @cached_count = GC.count
+ @cache_time = current_time
+ end
+ @cached_stats
+ end
+ end
+ end
+ end
+end
diff --git a/lib/skywalking/meter/runtime/mem_data_source.rb b/lib/skywalking/meter/runtime/mem_data_source.rb
new file mode 100644
index 0000000..3aba09f
--- /dev/null
+++ b/lib/skywalking/meter/runtime/mem_data_source.rb
@@ -0,0 +1,137 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'get_process_mem'
+require_relative '../base'
+
+module Skywalking
+ module Meter
+ module Runtime
+ # DataSource for collecting memory usage metrics
+ class MemDataSource < DataSource
+ def initialize
+ @process_mem = GetProcessMem.new
+ @cached_total_memory = nil
+ @cache_time = 0
+ @cache_duration = 60
+ @ffi_loaded = false
+ load_ffi_if_available
+ end
+
+ # Process RSS (Resident Set Size) in MB
+ def memory_rss_mb_generator
+ @process_mem.bytes / (1024.0 * 1024.0)
+ rescue
+ 0.0
+ end
+
+ # Process memory usage percentage
+ def memory_usage_percent_generator
+ rss_bytes = @process_mem.bytes
+ total_memory = get_total_memory
+
+ if total_memory && total_memory > 0
+ (rss_bytes.to_f / total_memory * 100).round(2)
+ end
+ rescue
+ nil
+ end
+
+ private
+
+ # Load FFI if available for more efficient system calls
+ def load_ffi_if_available
+ require 'ffi'
+ @ffi_loaded = true
+ rescue LoadError
+ @ffi_loaded = false
+ end
+
+ # Get total system memory with caching to avoid frequent system calls
+ def get_total_memory
+ current_time = Time.now.to_i
+
+ if @cached_total_memory.nil? || (current_time - @cache_time) > @cache_duration
+ @cached_total_memory = fetch_total_memory
+ @cache_time = current_time
+ end
+
+ @cached_total_memory
+ end
+
+ # Fetch total memory using platform-specific methods
+ def fetch_total_memory
+ if RUBY_PLATFORM.include?('linux')
+ fetch_linux_total_memory
+ elsif RUBY_PLATFORM.include?('darwin')
+ fetch_macos_total_memory
+ end
+ end
+
+ # Fetch total memory on Linux using /proc/meminfo
+ def fetch_linux_total_memory
+ meminfo = File.read('/proc/meminfo')
+ total = (meminfo.match(/MemTotal:\s+(\d+)/)&.captures&.first || '0').to_i * 1024
+ total > 0 ? total : nil
+ rescue
+ nil
+ end
+
+ # Fetch total memory on macOS using sysctl
+ def fetch_macos_total_memory
+ if @ffi_loaded
+ result = fetch_macos_total_memory_ffi
+ return result if result
+ end
+
+ fetch_macos_total_memory_sysctl
+ end
+
+ # Use FFI to call sysctl directly
+ def fetch_macos_total_memory_ffi
+ extend FFI::Library
+ ffi_lib 'c'
+
+ attach_function :sysctl, [:pointer, :uint, :pointer, :pointer, :pointer, :size_t], :int
+
+ # sysctlnametomib for hw.memsize
+ mib = FFI::MemoryPointer.new(:int, 2)
+ mib.put_int(0, 6) # CTL_HW
+ mib.put_int(4, 24) # HW_MEMSIZE
+
+ size = FFI::MemoryPointer.new(:size_t)
+ size.put_size_t(0, 8)
+
+ result = FFI::MemoryPointer.new(:uint64_t)
+
+ if sysctl(mib, 2, result, size, nil, 0) == 0
+ value = result.read_uint64
+ value > 0 ? value : nil
+ end
+ rescue
+ nil
+ end
+
+ # Fallback to sysctl command (still forks but cached)
+ def fetch_macos_total_memory_sysctl
+ result = `sysctl -n hw.memsize 2>/dev/null`.strip.to_i
+ result > 0 ? result : nil
+ rescue
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/skywalking/meter/runtime/ruby_runtime_data_source.rb b/lib/skywalking/meter/runtime/ruby_runtime_data_source.rb
new file mode 100644
index 0000000..23daf13
--- /dev/null
+++ b/lib/skywalking/meter/runtime/ruby_runtime_data_source.rb
@@ -0,0 +1,59 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require_relative '../base'
+
+module Skywalking
+ module Meter
+ module Runtime
+ # Enhanced Ruby-specific runtime metrics
+ class RubyRuntimeDataSource < DataSource
+ def initialize
+ @cached_stats = nil
+ @cache_time = 0
+ @cache_duration = 60
+ end
+
+ # Heap live slots count - important for memory pressure
+ def heap_live_slots_count_generator
+ stats = get_gc_stats
+ stats[:heap_live_slots] || 0
+ rescue
+ 0
+ end
+
+ # Heap available slots count - total capacity
+ def heap_available_slots_count_generator
+ stats = get_gc_stats
+ stats[:heap_available_slots] || 0
+ rescue
+ 0
+ end
+
+ private
+
+ # Get cached GC statistics, refresh if cache is expired
+ def get_gc_stats
+ current_time = Time.now.to_i
+ if @cached_stats.nil? || (current_time - @cache_time) > @cache_duration
+ @cached_stats = GC.stat
+ @cache_time = current_time
+ end
+ @cached_stats
+ end
+ end
+ end
+ end
+end
diff --git a/lib/skywalking/meter/runtime/thread_data_source.rb b/lib/skywalking/meter/runtime/thread_data_source.rb
new file mode 100644
index 0000000..35549f7
--- /dev/null
+++ b/lib/skywalking/meter/runtime/thread_data_source.rb
@@ -0,0 +1,39 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require_relative '../base'
+
+module Skywalking
+ module Meter
+ module Runtime
+ # DataSource for collecting thread metrics
+ class ThreadDataSource < DataSource
+ # Active thread count (alive threads)
+ def thread_count_active_generator
+ Thread.list.count(&:alive?)
+ rescue
+ 0
+ end
+
+ # Running thread count (threads in run state)
+ def thread_count_running_generator
+ Thread.list.count { |t| t.alive? && t.status == "run" }
+ rescue
+ 0
+ end
+ end
+ end
+ end
+end
diff --git a/lib/skywalking/plugins/logger.rb b/lib/skywalking/plugins/logger.rb
new file mode 100644
index 0000000..b865f2a
--- /dev/null
+++ b/lib/skywalking/plugins/logger.rb
@@ -0,0 +1,168 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'logger'
+
+module Skywalking
+ module Plugins
+ # Logger plugin intercepts Ruby's standard Logger to collect logs
+ class Logger < PluginsManager::SWPlugin
+ module LoggerIntercept
+ # Thread-local flag to prevent recursive log collection
+ COLLECTING_LOG_KEY = :skywalking_collecting_log
+
+ # Map Logger severity levels to string names
+ SEVERITY_NAMES = {
+ ::Logger::DEBUG => 'DEBUG',
+ ::Logger::INFO => 'INFO',
+ ::Logger::WARN => 'WARN',
+ ::Logger::ERROR => 'ERROR',
+ ::Logger::FATAL => 'FATAL',
+ ::Logger::UNKNOWN => 'UNKNOWN'
+ }.freeze
+
+ # Map severity names to levels
+ SEVERITY_LEVELS = {
+ 'DEBUG' => ::Logger::DEBUG,
+ 'INFO' => ::Logger::INFO,
+ 'WARN' => ::Logger::WARN,
+ 'ERROR' => ::Logger::ERROR,
+ 'FATAL' => ::Logger::FATAL,
+ 'UNKNOWN' => ::Logger::UNKNOWN
+ }.freeze
+
+ # Override the add method to intercept log calls
+ # @param severity [Integer] log severity level
+ # @param message [String, nil] log message
+ # @param progname [String, nil] program name
+ # @param block [Proc, nil] block that returns message
+ def add(severity, message = nil, progname = nil, &block)
+ # Call original method first
+ result = super
+
+ # Skip if we're already collecting logs to prevent recursion
+ return result if Thread.current[COLLECTING_LOG_KEY]
+
+ # Skip if log reporter is not active
+ agent = Agent.instance
+ return result unless agent&.log_reporter_active?
+
+ # Check severity threshold
+ configured_level = agent.config[:log_reporter_level]
+ min_level = if configured_level.is_a?(String)
+ SEVERITY_LEVELS[configured_level.upcase] || ::Logger::INFO
+ else
+ configured_level || ::Logger::INFO
+ end
+
+ return result unless severity >= min_level
+
+ # Set flag to prevent recursion
+ Thread.current[COLLECTING_LOG_KEY] = true
+ begin
+ collect_log_data(severity, message, progname, &block)
+ rescue => e
+ agent.logger.warn("SkyWalking log collection error: #{e.message}") if agent.config[:debug_mode]
+ ensure
+ # Always clear the flag
+ Thread.current[COLLECTING_LOG_KEY] = false
+ end
+
+ result
+ end
+
+ private
+
+ # Collect log data and send to SkyWalking
+ # @param severity [Integer] log severity level
+ # @param message [String, nil] log message
+ # @param progname [String, nil] program name
+ # @param block [Proc, nil] block that returns message
+ def collect_log_data(severity, message, progname, &block)
+ # Format the message
+ msg = if message.nil?
+ if block_given?
+ yield
+ else
+ progname
+ end
+ else
+ message
+ end
+
+ return if msg.nil?
+
+ # Get current context
+ begin
+ context = Tracing::ContextManager.current_context
+ trace_context = if context && !context.is_a?(Tracing::IgnoredContext) && context.segment
+ segment = context.segment
+ span = context.active_span
+
+ V3::TraceContext.new(
+ traceId: segment.related_traces.first&.to_s,
+ traceSegmentId: segment.segment_id.to_s,
+ spanId: span ? span.span_id : -1
+ )
+ end
+ rescue
+ # If tracing context is not available, continue without it
+ trace_context = nil
+ end
+
+ # Create log data
+ agent = Agent.instance
+ return unless agent # Safety check
+
+ log_data = V3::LogData.new(
+ timestamp: (Time.now.to_f * 1000).to_i,
+ service: agent.config[:service_name],
+ serviceInstance: agent.config[:instance_name],
+ endpoint: context&.active_span&.operation || '',
+ body: V3::LogDataBody.new(
+ type: 'text',
+ text: V3::TextLog.new(text: msg.to_s)
+ ),
+ traceContext: trace_context,
+ tags: V3::LogTags.new(
+ data: [
+ V3::KeyStringValuePair.new(key: 'level', value: SEVERITY_NAMES[severity] || 'UNKNOWN'),
+ V3::KeyStringValuePair.new(key: 'logger', value: progname || self.class.name),
+ V3::KeyStringValuePair.new(key: 'thread', value: Thread.current.name || Thread.current.object_id.to_s)
+ ]
+ ),
+ layer: 'GENERAL'
+ )
+
+ # Send to log buffer
+ agent.log_buffer << log_data
+ end
+ end
+
+ # Check if the plugin can be installed
+ # @return [Boolean] true if Logger is defined
+ def plugin_valid?
+ defined?(::Logger)
+ end
+
+ # Install the plugin by prepending the intercept module
+ def install
+ ::Logger.prepend LoggerIntercept
+ end
+
+ register :logger
+ end
+ end
+end
diff --git a/lib/skywalking/reporter/client/grpc_client.rb b/lib/skywalking/reporter/client/grpc_client.rb
index 2d32df1..a87e8a8 100644
--- a/lib/skywalking/reporter/client/grpc_client.rb
+++ b/lib/skywalking/reporter/client/grpc_client.rb
@@ -33,10 +33,11 @@
def report_instance_properties
begin
+ properties = gen_service_instance
req = InstanceProperties.new(
service: @config[:service_name],
serviceInstance: @config[:instance_name],
- properties: gen_service_instance
+ properties: properties
)
@management_service.report_instance_properties(req)
@@ -108,6 +109,49 @@
error "Error to report trace segment: #{e.message}}"
end
end
+
+ class MeterReportServiceGrpc
+ include Log::Logging
+
+ def initialize(config)
+ @config = config
+ @meter_service ||= MeterReportServiceStub.new(
+ @config[:collector_backend_services],
+ :this_channel_is_insecure
+ )
+ end
+
+ def report_meter(enumerator)
+ @meter_service.collect(enumerator)
+ rescue Exception => e
+ error "Error to report meter data: #{e.message}"
+ end
+ end
+
+ class LogReportServiceGrpc
+ include Log::Logging
+
+ def initialize(config)
+ @config = config
+ @log_service ||= V3::LogReportService::Stub.new(
+ @config[:collector_backend_services],
+ :this_channel_is_insecure
+ )
+ end
+
+ def report_log(log_data_array)
+ return if log_data_array.nil? || log_data_array.empty?
+
+ # Create an enumerator that yields log data
+ enumerator = Enumerator.new do |yielder|
+ log_data_array.each { |log_data| yielder << log_data }
+ end
+
+ @log_service.collect(enumerator)
+ rescue Exception => e
+ error "Error to report log data: #{e.message}"
+ end
+ end
end
end
end
diff --git a/lib/skywalking/reporter/client/proto.rb b/lib/skywalking/reporter/client/proto.rb
index de1c639..2f3b6bf 100644
--- a/lib/skywalking/reporter/client/proto.rb
+++ b/lib/skywalking/reporter/client/proto.rb
@@ -16,6 +16,10 @@
require_relative '../../proto/management/Management_services_pb'
require_relative '../../proto/language-agent/Tracing_services_pb'
require_relative '../../proto/language-agent/Tracing_pb'
+require_relative '../../proto/language-agent/Meter_services_pb'
+require_relative '../../proto/language-agent/Meter_pb'
+require_relative '../../proto/logging/Logging_services_pb'
+require_relative '../../proto/logging/Logging_pb'
module Skywalking
ManagementServiceStub = Skywalking::V3::ManagementService::Stub
@@ -26,4 +30,10 @@
SpanObject = Skywalking::V3::SpanObject
SegmentReference = Skywalking::V3::SegmentReference
KeyStringValuePair = Skywalking::V3::KeyStringValuePair
-end
\ No newline at end of file
+ MeterReportServiceStub = Skywalking::V3::MeterReportService::Stub
+ MeterData = Skywalking::V3::MeterData
+ MeterSingleValue = Skywalking::V3::MeterSingleValue
+ MeterHistogram = Skywalking::V3::MeterHistogram
+ MeterBucketValue = Skywalking::V3::MeterBucketValue
+ Label = Skywalking::V3::Label
+end
diff --git a/lib/skywalking/reporter/grpc.rb b/lib/skywalking/reporter/grpc.rb
index db0ee4f..fcd6c3c 100644
--- a/lib/skywalking/reporter/grpc.rb
+++ b/lib/skywalking/reporter/grpc.rb
@@ -23,6 +23,8 @@
@cfg = config
@ms_client = Skywalking::Reporter::Client::GrpcClient::ManagementServiceGrpc.new(config)
@trace_client = Skywalking::Reporter::Client::GrpcClient::TraceSegmentReportServiceGrpc.new(config)
+ @meter_client = Skywalking::Reporter::Client::GrpcClient::MeterReportServiceGrpc.new(config)
+ @log_client = Skywalking::Reporter::Client::GrpcClient::LogReportServiceGrpc.new(config)
@send_properties_counter = 0
@counter_mutex = Mutex.new
end
@@ -41,6 +43,14 @@
def report_segment(enumerator)
@trace_client.report_segment(enumerator)
end
+
+ def report_meter(enumerator)
+ @meter_client.report_meter(enumerator)
+ end
+
+ def report_log(log_data_array)
+ @log_client.report_log(log_data_array)
+ end
end
end
end
diff --git a/lib/skywalking/reporter/log_buffer_trigger.rb b/lib/skywalking/reporter/log_buffer_trigger.rb
new file mode 100644
index 0000000..b5de8ce
--- /dev/null
+++ b/lib/skywalking/reporter/log_buffer_trigger.rb
@@ -0,0 +1,86 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require_relative 'client/proto'
+
+module Skywalking
+ module Reporter
+ # LogBufferTrigger manages the log data queue and generates protocol messages
+ class LogBufferTrigger
+ include Enumerable
+
+ extend Forwardable
+ def_delegators :@buffer, :push
+
+ # Initialize the log buffer trigger
+ # @param config [Hash] configuration options
+ def initialize(config)
+ @config = config
+ @max_size = @config[:max_log_queue_size] || 1000
+ @buffer = Queue.new
+ @mutex = Mutex.new
+ @closed = false
+ end
+
+ # Check if the buffer is empty
+ # @return [Boolean] true if empty
+ def empty?
+ @buffer.empty?
+ end
+
+ # Add log data to the buffer
+ # @param log_data [LogData] the log data to add
+ def <<(log_data)
+ @mutex.synchronize do
+ clear_queue if @buffer.size >= @max_size
+ @buffer.push(log_data)
+ end
+ end
+
+ # Clear the queue
+ def clear_queue
+ @buffer.clear
+ end
+
+ # Close the queue
+ def close_queue
+ @mutex.synchronize do
+ @buffer.close
+ @closed = true
+ end
+ end
+
+ # Check if the queue is closed
+ # @return [Boolean] true if closed
+ def closed?
+ @closed
+ end
+
+ # Stream log data from the buffer
+ # @return [Enumerator, nil] enumerator of log data or nil if empty
+ def stream_data
+ begin
+ log_data = @buffer.pop(false)
+ rescue ThreadError
+ return nil
+ end
+
+ Enumerator.new do |yielder|
+ yielder << log_data unless log_data.nil?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/skywalking/reporter/meter_buffer_trigger.rb b/lib/skywalking/reporter/meter_buffer_trigger.rb
new file mode 100644
index 0000000..8401b5f
--- /dev/null
+++ b/lib/skywalking/reporter/meter_buffer_trigger.rb
@@ -0,0 +1,94 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'forwardable'
+
+module Skywalking
+ module Reporter
+ # Buffer trigger for meter data collection
+ class MeterBufferTrigger
+ include Enumerable
+
+ extend Forwardable
+ def_delegators :@buffer, :push
+
+ # @param config [Hash] configuration
+ def initialize(config)
+ @config = config
+ @max_size = @config[:max_meter_queue_size] || 1000
+ @buffer = Queue.new
+ @mutex = Mutex.new
+ @closed = false
+ end
+
+ # Check if buffer is empty
+ # @return [Boolean]
+ def empty?
+ @buffer.empty?
+ end
+
+ # Add meter data to buffer
+ # @param meter_data [MeterData] the meter data to add
+ def <<(meter_data)
+ @mutex.synchronize do
+ clear_queue if @buffer.size >= @max_size
+ @buffer.push(meter_data)
+ end
+ end
+
+ # Clear all data from the queue
+ def clear_queue
+ @buffer.clear
+ end
+
+ # Close the queue
+ def close_queue
+ @mutex.synchronize do
+ @buffer.close
+ @closed = true
+ end
+ end
+
+ # Check if the queue is closed
+ # @return [Boolean]
+ def closed?
+ @closed
+ end
+
+ # Stream meter data from the buffer
+ # @return [Enumerator, nil]
+ def stream_data
+ data_batch = []
+
+ # Collect up to 100 items or until timeout
+ deadline = Time.now + 0.1
+ while Time.now < deadline && data_batch.size < 100
+ begin
+ meter_data = @buffer.pop(true)
+ data_batch << meter_data
+ rescue ThreadError
+ break
+ end
+ end
+
+ return nil if data_batch.empty?
+
+ Enumerator.new do |yielder|
+ data_batch.each { |data| yielder << data }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/skywalking/reporter/protocol.rb b/lib/skywalking/reporter/protocol.rb
index 2e5fb28..833bb54 100644
--- a/lib/skywalking/reporter/protocol.rb
+++ b/lib/skywalking/reporter/protocol.rb
@@ -19,7 +19,7 @@
def report_heartbeat
raise NotImplementedError 'The report_heartbeat method has not been implemented'
end
-
+
def report_segment
raise NotImplementedError 'The report_segment method has not been implemented'
end
diff --git a/lib/skywalking/reporter/report.rb b/lib/skywalking/reporter/report.rb
index 6c397d5..59db2cd 100644
--- a/lib/skywalking/reporter/report.rb
+++ b/lib/skywalking/reporter/report.rb
@@ -16,12 +16,24 @@
require_relative 'grpc'
require_relative 'scheduler'
require_relative 'buffer_trigger'
+require_relative 'meter_buffer_trigger'
+require_relative 'reporter_manager'
+require_relative '../meter/base'
+require_relative '../meter/meter_service'
+require_relative '../meter/runtime/cpu_data_source'
+require_relative '../meter/runtime/mem_data_source'
+require_relative '../meter/runtime/gc_data_source'
+require_relative '../meter/runtime/thread_data_source'
+require_relative '../meter/runtime/ruby_runtime_data_source'
module Skywalking
module Reporter
class Report
def initialize(config)
@config = config
+ @meter_service = nil
+ @scheduler_loop = nil
+ @reporter_manager = nil
init_proto
end
@@ -37,22 +49,48 @@
end
def init_reporter
- @daemon_loop = []
-
+ # Initialize scheduler for heartbeat
@scheduler_loop = Scheduler.new
- @daemon_loop << Thread.new do
+ @scheduler_thread = Thread.new do
+ Thread.current.name = 'Scheduler'
init_worker_loop
@scheduler_loop.run
end
- @@trigger = BufferTrigger.new(@config)
- @daemon_loop << Thread.new do
- report_segment
+ # Initialize reporter manager
+ @reporter_manager = ReporterManager.new(@config, @protocol)
+
+ # Register segment reporter
+ segment_trigger = BufferTrigger.new(@config)
+ @reporter_manager.register_reporter(:segment, segment_trigger, :report_segment)
+
+ # Register meter reporter if enabled
+ if @config[:meter_reporter_active]
+ meter_trigger = MeterBufferTrigger.new(@config)
+ @reporter_manager.register_reporter(:meter, meter_trigger, :report_meter)
+
+ # Initialize meter service
+ @meter_service = Skywalking::Meter::MeterService.new(@config, meter_trigger)
+
+ if @config[:runtime_meter_reporter_active]
+ register_runtime_data_sources
+ end
+
+ @meter_service.start
end
+
+ # Start all reporters
+ @reporter_manager.start
end
+ # Deprecated: Use instance method instead
def self.trigger
- @@trigger
+ warn "[DEPRECATED] Report.trigger is deprecated. Use instance method 'trigger' instead."
+ default_instance.trigger
+ end
+
+ def self.default_instance
+ @default_instance ||= new(Skywalking::Configuration.new)
end
def init_worker_loop
@@ -60,23 +98,42 @@
end
def stop
- @scheduler_loop.shutdown
- @@trigger.close_queue
- @daemon_loop.each do |daemon|
- if daemon.alive?
- daemon.wakeup
- daemon.join
- end
- end
+ @scheduler_loop&.shutdown
+ @scheduler_thread&.join(5)
+ @reporter_manager&.stop
+ @meter_service&.stop
end
def report_heartbeat
@protocol.report_heartbeat
end
- def report_segment
- @protocol.report_segment(@@trigger.stream_data) until @@trigger.closed?
+ # Accessor methods for triggers
+ def trigger
+ @reporter_manager&.trigger(:segment)
+ end
+
+ def meter_trigger
+ @reporter_manager&.trigger(:meter)
+ end
+
+ # Report log data to the backend
+ # @param log_data_array [Array<LogData>] array of log data to report
+ def report_log(log_data_array)
+ @protocol.report_log(log_data_array) if @protocol
+ rescue => e
+ warn "Failed to report log data: #{e.message}"
+ end
+
+ private
+
+ def register_runtime_data_sources
+ Skywalking::Meter::Runtime::CpuDataSource.new.register(@meter_service)
+ Skywalking::Meter::Runtime::MemDataSource.new.register(@meter_service)
+ Skywalking::Meter::Runtime::GcDataSource.new.register(@meter_service)
+ Skywalking::Meter::Runtime::ThreadDataSource.new.register(@meter_service)
+ Skywalking::Meter::Runtime::RubyRuntimeDataSource.new.register(@meter_service)
end
end
end
-end
\ No newline at end of file
+end
diff --git a/lib/skywalking/reporter/reporter_manager.rb b/lib/skywalking/reporter/reporter_manager.rb
new file mode 100644
index 0000000..7be9a9f
--- /dev/null
+++ b/lib/skywalking/reporter/reporter_manager.rb
@@ -0,0 +1,92 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module Skywalking
+ module Reporter
+ # Manages all reporters in a more elegant way using dependency injection
+ class ReporterManager
+ include Log::Logging
+
+ attr_reader :reporters, :triggers
+
+ def initialize(config, protocol)
+ @config = config
+ @protocol = protocol
+ @reporters = {}
+ @triggers = {}
+ @threads = []
+ @running = false
+ end
+
+ # Register a reporter with its trigger
+ # @param name [Symbol] reporter name
+ # @param trigger [Object] buffer trigger instance
+ # @param report_method [Symbol] method name on protocol to call
+ def register_reporter(name, trigger, report_method)
+ @triggers[name] = trigger
+ @reporters[name] = {
+ trigger: trigger,
+ report_method: report_method,
+ thread: nil
+ }
+ end
+
+ # Start all registered reporters
+ def start
+ return if @running
+ @running = true
+
+ @reporters.each do |name, reporter|
+ thread = Thread.new do
+ Thread.current.name = "Reporter-#{name}"
+ report_loop(reporter[:trigger], reporter[:report_method])
+ end
+ reporter[:thread] = thread
+ @threads << thread
+ end
+ end
+
+ # Stop all reporters
+ def stop
+ @running = false
+
+ @triggers.each_value(&:close_queue)
+ @threads.each do |thread|
+ thread.join(5) if thread.alive?
+ end
+ @threads.clear
+ end
+
+ # Get trigger by name
+ # @param name [Symbol] trigger name
+ # @return [Object, nil] trigger instance
+ def trigger(name)
+ @triggers[name]
+ end
+
+ private
+
+ def report_loop(trigger, method_name)
+ while @running && !trigger.closed?
+ data = trigger.stream_data
+ @protocol.send(method_name, data) if data
+ end
+ rescue => e
+ error "Error in reporter loop: #{e.message}"
+ error e.backtrace.join("\n")
+ end
+ end
+ end
+end
diff --git a/lib/skywalking/tracing/span_context.rb b/lib/skywalking/tracing/span_context.rb
index fa882cc..39279a6 100644
--- a/lib/skywalking/tracing/span_context.rb
+++ b/lib/skywalking/tracing/span_context.rb
@@ -147,7 +147,9 @@
spans.delete(span)
@n_spans -= 1
if @n_spans.zero?
- Reporter::Report.trigger << @segment
+ if (trigger = Agent.instance&.reporter&.trigger)
+ trigger << @segment
+ end
return true
end
diff --git a/lib/skywalking/version.rb b/lib/skywalking/version.rb
index cdaca0b..b29b4b2 100644
--- a/lib/skywalking/version.rb
+++ b/lib/skywalking/version.rb
@@ -14,5 +14,5 @@
# limitations under the License.
module Skywalking
- VERSION = "0.1.0".freeze
+ VERSION = "0.2.0".freeze
end
diff --git a/skywalking.gemspec b/skywalking.gemspec
index fb0c5b7..824936e 100644
--- a/skywalking.gemspec
+++ b/skywalking.gemspec
@@ -43,6 +43,11 @@
# Communication with OAP
spec.add_dependency 'grpc', '~> 1.68.0'
+
+ # Meter dependencies
+ spec.add_dependency 'sys-cpu', '~> 1.0'
+ spec.add_dependency 'get_process_mem', '~> 0.2'
+ spec.add_dependency 'ffi', '~> 1.17', '>= 1.17.0'
# Base dev dependency
spec.add_development_dependency 'bundler', '~> 2.0'
diff --git a/spec/scenarios/elasticsearch/docker-compose.yml b/spec/scenarios/elasticsearch/docker-compose.yml
index 880ec79..4a38f1d 100644
--- a/spec/scenarios/elasticsearch/docker-compose.yml
+++ b/spec/scenarios/elasticsearch/docker-compose.yml
@@ -61,7 +61,7 @@
entrypoint:
- "sh"
- "-c"
- - "gem install sinatra rackup puma elasticsearch && ruby /app/spec/scenarios/elasticsearch/elasticsearch.rb"
+ - "gem install sinatra rackup puma elasticsearch:8.16.1 && ruby /app/spec/scenarios/elasticsearch/elasticsearch.rb"
depends_on:
oap:
condition: service_healthy