Class: Kward::TelemetryStats
- Inherits:
-
Object
- Object
- Kward::TelemetryStats
- Defined in:
- lib/kward/telemetry/stats.rb
Defined Under Namespace
Classes: Result
Constant Summary collapse
- DEFAULT_RANGE =
"1 week"- UNITS =
%w[minute hour day week month year].freeze
- USAGE =
"Usage: /stats [N minutes|hours|days|weeks|months|years] (default: 1 week)".freeze
- TOKEN_CSV_HEADER =
%w[bucket_start bucket_end provider model events input_tokens output_tokens cache_read_tokens cache_write_tokens total_tokens].freeze
- TOKEN_BUCKETS =
%w[second minute hour day week month year].freeze
Class Method Summary collapse
- .calendar_start(now, count, unit) ⇒ Object
- .format(result) ⇒ Object
- .format_counts(counts) ⇒ Object
- .format_errors(errors) ⇒ Object
- .format_performance(performance) ⇒ Object
- .format_tokens(tokens) ⇒ Object
- .format_tools(tools) ⇒ Object
- .inline_counts(counts) ⇒ Object
- .normalize_bucket(bucket) ⇒ Object
- .normalize_unit(unit) ⇒ Object
- .parse_range(argument, now: Time.now.utc) ⇒ Object
- .shift_month_start(now, offset) ⇒ Object
Instance Method Summary collapse
- #collect(argument = "") ⇒ Object
- #enabled_categories ⇒ Object
-
#initialize(telemetry_logger: TelemetryLogger.new, clock: Time) ⇒ TelemetryStats
constructor
A new instance of TelemetryStats.
- #log_dir ⇒ Object
- #token_usage_csv(argument = "", bucket: nil) ⇒ Object
Constructor Details
#initialize(telemetry_logger: TelemetryLogger.new, clock: Time) ⇒ TelemetryStats
Returns a new instance of TelemetryStats.
37 38 39 40 |
# File 'lib/kward/telemetry/stats.rb', line 37 def initialize(telemetry_logger: TelemetryLogger.new, clock: Time) @telemetry_logger = telemetry_logger @clock = clock end |
Class Method Details
.calendar_start(now, count, unit) ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/kward/telemetry/stats.rb', line 134 def self.calendar_start(now, count, unit) case unit when "minute" Time.utc(now.year, now.month, now.day, now.hour, now.min) - ((count - 1) * 60) when "hour" Time.utc(now.year, now.month, now.day, now.hour) - ((count - 1) * 60 * 60) when "day" Time.utc(now.year, now.month, now.day) - ((count - 1) * 24 * 60 * 60) when "week" start_of_week = Time.utc(now.year, now.month, now.day) - ((now.wday + 6) % 7 * 24 * 60 * 60) start_of_week - ((count - 1) * 7 * 24 * 60 * 60) when "month" shift_month_start(now, -(count - 1)) when "year" Time.utc(now.year - count + 1, 1, 1) else raise ArgumentError, USAGE end end |
.format(result) ⇒ Object
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 |
# File 'lib/kward/telemetry/stats.rb', line 92 def self.format(result) lines = [] range = result.range lines << "Stats for #{range[:input]} (#{range[:start_at].iso8601} to #{range[:end_at].iso8601})" lines << "Log directory: #{result.log_dir}" lines << "Enabled categories: #{result.enabled_categories.join(", ")}" lines << "Records: #{result.record_count}" lines << "" lines << "Records by category:" lines.concat(format_counts(result.records_by_category)) lines << "" lines << "Records by event:" lines.concat(format_counts(result.records_by_event)) lines << "" lines << "Tokens:" lines.concat(format_tokens(result.tokens)) lines << "" lines << "Performance:" lines.concat(format_performance(result.performance)) lines << "" lines << "Tools:" lines.concat(format_tools(result.tools)) lines << "" lines << "Errors:" lines.concat(format_errors(result.errors)) lines.join("\n") end |
.format_counts(counts) ⇒ Object
161 162 163 164 165 |
# File 'lib/kward/telemetry/stats.rb', line 161 def self.format_counts(counts) return [" none"] if counts.empty? counts.sort_by { |key, value| [-value, key.to_s] }.map { |key, value| " #{key}: #{value}" } end |
.format_errors(errors) ⇒ Object
195 196 197 198 199 200 201 202 |
# File 'lib/kward/telemetry/stats.rb', line 195 def self.format_errors(errors) lines = [" events: #{errors[:count]}"] lines << " by event: #{inline_counts(errors[:byEvent])}" lines << " by class: #{inline_counts(errors[:byClass])}" lines << " by provider: #{inline_counts(errors[:byProvider])}" lines << " by code: #{inline_counts(errors[:byCode])}" lines end |
.format_performance(performance) ⇒ Object
177 178 179 180 181 182 183 184 185 186 |
# File 'lib/kward/telemetry/stats.rb', line 177 def self.format_performance(performance) return [" none"] if performance[:events].empty? lines = [] performance[:events].sort.each do |event, stats| lines << " #{event}: count=#{stats[:count]}, min=#{stats[:durationMs][:min]}, avg=#{stats[:durationMs][:avg]}, max=#{stats[:durationMs][:max]} ms" lines << " statuses: #{inline_counts(stats[:statuses])}" unless stats[:statuses].empty? end lines end |
.format_tokens(tokens) ⇒ Object
167 168 169 170 171 172 173 174 175 |
# File 'lib/kward/telemetry/stats.rb', line 167 def self.format_tokens(tokens) lines = [" model usage events: #{tokens[:modelUsageEvents]}"] if tokens[:totals].empty? lines << " token totals: none" else tokens[:totals].sort.each { |key, value| lines << " #{key}: #{value}" } end lines end |
.format_tools(tools) ⇒ Object
188 189 190 191 192 193 |
# File 'lib/kward/telemetry/stats.rb', line 188 def self.format_tools(tools) lines = [" calls: #{tools[:calls]}", " result bytes: #{tools[:resultBytes]}"] lines << " by tool: #{inline_counts(tools[:byName])}" lines << " by status: #{inline_counts(tools[:byStatus])}" lines end |
.inline_counts(counts) ⇒ Object
204 205 206 207 208 |
# File 'lib/kward/telemetry/stats.rb', line 204 def self.inline_counts(counts) return "none" if counts.empty? counts.sort_by { |key, value| [-value, key.to_s] }.map { |key, value| "#{key}=#{value}" }.join(", ") end |
.normalize_bucket(bucket) ⇒ Object
126 127 128 129 130 131 132 |
# File 'lib/kward/telemetry/stats.rb', line 126 def self.normalize_bucket(bucket) text = bucket.to_s.downcase.strip text = text.delete_suffix("s") raise ArgumentError, "Bucket must be one of: #{TOKEN_BUCKETS.join(", ")}" unless TOKEN_BUCKETS.include?(text) text end |
.normalize_unit(unit) ⇒ Object
120 121 122 123 124 |
# File 'lib/kward/telemetry/stats.rb', line 120 def self.normalize_unit(unit) text = unit.to_s.downcase text = text.delete_suffix("s") UNITS.include?(text) ? text : nil end |
.parse_range(argument, now: Time.now.utc) ⇒ Object
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/kward/telemetry/stats.rb', line 73 def self.parse_range(argument, now: Time.now.utc) text = argument.to_s.strip text = DEFAULT_RANGE if text.empty? match = text.match(/\A(\d+)\s+([A-Za-z]+)\z/) raise ArgumentError, USAGE unless match count = match[1].to_i unit = normalize_unit(match[2]) raise ArgumentError, USAGE unless count.positive? && unit { input: text, count: count, unit: unit, start_at: calendar_start(now.utc, count, unit), end_at: now.utc } end |
.shift_month_start(now, offset) ⇒ Object
154 155 156 157 158 159 |
# File 'lib/kward/telemetry/stats.rb', line 154 def self.shift_month_start(now, offset) month_index = (now.year * 12) + (now.month - 1) + offset year = month_index / 12 month = (month_index % 12) + 1 Time.utc(year, month, 1) end |
Instance Method Details
#collect(argument = "") ⇒ Object
50 51 52 53 54 55 56 57 |
# File 'lib/kward/telemetry/stats.rb', line 50 def collect(argument = "") categories = enabled_categories raise ArgumentError, "Telemetry logging is disabled. Enable logging and at least one category before using /stats." if categories.empty? range = self.class.parse_range(argument, now: @clock.now.utc) records = read_records(range[:start_at], range[:end_at], categories) build_result(range, categories, records) end |
#enabled_categories ⇒ Object
42 43 44 |
# File 'lib/kward/telemetry/stats.rb', line 42 def enabled_categories @telemetry_logger.enabled_categories end |
#log_dir ⇒ Object
46 47 48 |
# File 'lib/kward/telemetry/stats.rb', line 46 def log_dir @telemetry_logger.log_directory end |
#token_usage_csv(argument = "", bucket: nil) ⇒ Object
59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/kward/telemetry/stats.rb', line 59 def token_usage_csv(argument = "", bucket: nil) categories = enabled_categories raise ArgumentError, "Token telemetry logging is disabled. Enable logging and token logging before exporting token CSV." unless categories.include?("tokens") range = self.class.parse_range(argument, now: @clock.now.utc) bucket = self.class.normalize_bucket(bucket || range[:unit]) buckets = token_usage_buckets(range, bucket) lines = [csv_row(TOKEN_CSV_HEADER)] buckets.each do |_key, values| lines << csv_row(TOKEN_CSV_HEADER.map { |column| token_csv_value(values, column) }) end lines.join("\n") + "\n" end |