mobile_metrics 0.0.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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +35 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/mobile_metrics.rb +330 -0
- data/lib/mobile_metrics/version.rb +3 -0
- data/mobile_metrics.gemspec +25 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 66ed534e226cd7151aba4d7c2bae9ca551af7361
|
4
|
+
data.tar.gz: d9833bebadf111fab41104b16bfe15ebb321c52f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 130948656a1dcf4546ffe2a2fb85d53ec06952409fe17c3118679903680801fe0f5cb8ad5553f730e1621a507b47022d6654be8b3cf9082a7e11c8b8f2d263b7
|
7
|
+
data.tar.gz: b9a3295ba27805917a25cd024e6f51818ce5a330af93497a1503ed4c456344d15d561ba4cedd52f62779a0b03e84e88be2922c1197abe8e04e68db9808feb8ab
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
mobile_metrics (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.3)
|
10
|
+
rake (10.5.0)
|
11
|
+
rspec (3.7.0)
|
12
|
+
rspec-core (~> 3.7.0)
|
13
|
+
rspec-expectations (~> 3.7.0)
|
14
|
+
rspec-mocks (~> 3.7.0)
|
15
|
+
rspec-core (3.7.1)
|
16
|
+
rspec-support (~> 3.7.0)
|
17
|
+
rspec-expectations (3.7.0)
|
18
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
19
|
+
rspec-support (~> 3.7.0)
|
20
|
+
rspec-mocks (3.7.0)
|
21
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
22
|
+
rspec-support (~> 3.7.0)
|
23
|
+
rspec-support (3.7.1)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
bundler (~> 1.16)
|
30
|
+
mobile_metrics!
|
31
|
+
rake (~> 10.0)
|
32
|
+
rspec (~> 3.0)
|
33
|
+
|
34
|
+
BUNDLED WITH
|
35
|
+
1.16.1
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "mobile_metrics"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,330 @@
|
|
1
|
+
require "mobile_metrics/version"
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'json'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'pathname'
|
8
|
+
|
9
|
+
# Metric types
|
10
|
+
METRIC_TIMER = 'x.mobile_metric.timer'
|
11
|
+
METRIC_COUNTER = 'x.mobile_metric.counter'
|
12
|
+
METRIC_RATIO = 'x.mobile_metric.ratio'
|
13
|
+
METRIC_SIZE = 'x.mobile_metric.size'
|
14
|
+
|
15
|
+
# Misc Constants
|
16
|
+
UNKNOWN = 'UNKNOWN'
|
17
|
+
LOG_PREFIX = 'CI-METRICS:'
|
18
|
+
LOGGING_DIR = '/tmp/cimetricslogs'
|
19
|
+
UPLOAD_ENV_VAR = 'MOBILE_METRICS_UPLOAD_LOCATION'
|
20
|
+
|
21
|
+
module MobileMetrics
|
22
|
+
class Log
|
23
|
+
|
24
|
+
@@in_progress_timers = {}
|
25
|
+
@@log_file = nil
|
26
|
+
@@env_values = nil
|
27
|
+
@@verbose = false
|
28
|
+
@@upload_location = nil
|
29
|
+
|
30
|
+
#========================================
|
31
|
+
# Configuration
|
32
|
+
#=========================================
|
33
|
+
|
34
|
+
# Named params, with ability to override / leave out stuff we don't care about.
|
35
|
+
def self.set_default_values(
|
36
|
+
# Required params
|
37
|
+
project:,
|
38
|
+
# These params either aren't required, or have sensible defaults.
|
39
|
+
level: "info",
|
40
|
+
platform: "iOS",
|
41
|
+
buildUrl: ENV['BUILD_URL'],
|
42
|
+
gitUrl: ENV['GIT_URL'],
|
43
|
+
gitBranch: ENV['DOTCI_BRANCH'],
|
44
|
+
gitSha: ENV['DOTCI_SHA'],
|
45
|
+
# no sensible defaults for these, we'll strip them later
|
46
|
+
buildType: UNKNOWN,
|
47
|
+
brand: UNKNOWN,
|
48
|
+
# Controls log levels within this class
|
49
|
+
verbose: false,
|
50
|
+
# Upload location for logs
|
51
|
+
upload_location: nil
|
52
|
+
)
|
53
|
+
|
54
|
+
#TODO: May be able to support overridding these at some point in the future.
|
55
|
+
# It won't be threadsafe, but our builds aren't parallelized at the build script level anyway.
|
56
|
+
if @@env_values
|
57
|
+
print 'Can only override default values once! Aborting!'
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
if project.nil?
|
62
|
+
print 'Project value for logging MUST be non-nil, failing build.'
|
63
|
+
exit 14
|
64
|
+
return
|
65
|
+
end
|
66
|
+
|
67
|
+
@@verbose = verbose
|
68
|
+
|
69
|
+
# Upload location
|
70
|
+
@@upload_location = (upload_location || ENV[UPLOAD_ENV_VAR])
|
71
|
+
if @@upload_location.nil?
|
72
|
+
print 'Upload location value for logging MUST not be nil, exiting.'
|
73
|
+
exit 15
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create the logging dir + filename
|
78
|
+
FileUtils.mkdir_p(LOGGING_DIR)
|
79
|
+
filename = "metrics_#{project}_#{build_number}.log"
|
80
|
+
@@log_file = Pathname.new(LOGGING_DIR) + filename
|
81
|
+
print @@log_file if @@verbose
|
82
|
+
|
83
|
+
# Populate our env values
|
84
|
+
@@env_values = {}
|
85
|
+
|
86
|
+
# Required
|
87
|
+
@@env_values[:project] = project
|
88
|
+
# Optional
|
89
|
+
@@env_values[:level] = level
|
90
|
+
@@env_values[:platform] = platform
|
91
|
+
@@env_values[:gitUrl] = gitUrl
|
92
|
+
@@env_values[:gitBranch] = gitBranch
|
93
|
+
@@env_values[:gitSha] = gitSha
|
94
|
+
@@env_values[:buildUrl] = buildUrl
|
95
|
+
|
96
|
+
#TODO: Should any of these be required?
|
97
|
+
@@env_values[:buildType] = buildType if buildType != UNKNOWN
|
98
|
+
@@env_values[:brand] = brand if brand != UNKNOWN
|
99
|
+
end
|
100
|
+
|
101
|
+
# =begin
|
102
|
+
# # Sample timer metric
|
103
|
+
# {
|
104
|
+
# "time": "<yyyy-MM-ddThh:mm:ss[.sss]Z>",
|
105
|
+
# "name": "x.mobile_metric.<metric_type>",
|
106
|
+
# "level": "info",
|
107
|
+
# "data": {
|
108
|
+
# "name": "<metric_name>",
|
109
|
+
# … // additional data per metric type
|
110
|
+
# }
|
111
|
+
# "context": {
|
112
|
+
# "platform": "<ios_or_android>",
|
113
|
+
# "project": "<project_name>",
|
114
|
+
# "gitUrl": "<git_url>",
|
115
|
+
# "gitBranch": "<git_branch>",
|
116
|
+
# "gitSha": "<git_sha>",
|
117
|
+
# "buildType": "<build_type>",
|
118
|
+
# "brand": "<brand>",
|
119
|
+
# "host": "<machine_dns_name_or_device_name>",
|
120
|
+
# "processId": "<process_id_or_build_number>"
|
121
|
+
# }
|
122
|
+
# }
|
123
|
+
# =end
|
124
|
+
|
125
|
+
#=========================================
|
126
|
+
# Public Logging Methods
|
127
|
+
#=========================================
|
128
|
+
|
129
|
+
def self.log_ratio_metric(name:, ratio:)
|
130
|
+
overrides = {name: METRIC_RATIO, data: {name: name, ratio: ratio.to_f }}
|
131
|
+
metric = merge_hashes(default_metric, overrides).to_json
|
132
|
+
append_to_log(metric)
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.log_counter_metric(name:, count:)
|
136
|
+
overrides = {name: METRIC_COUNTER, data: {name: name, count: count.to_i }}
|
137
|
+
metric = merge_hashes(default_metric, overrides).to_json
|
138
|
+
append_to_log(metric)
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.log_size_metric(name:, sizeInBytes:, filename:, artifactUrl:)
|
142
|
+
overrides = {name: METRIC_SIZE, data: {name: name, filename: filename, sizeInBytes: sizeInBytes, artifactUrl: artifactUrl }}
|
143
|
+
metric = merge_hashes(default_metric, overrides).to_json
|
144
|
+
append_to_log(metric)
|
145
|
+
end
|
146
|
+
|
147
|
+
def self.start_timer_metric(name:)
|
148
|
+
if @@in_progress_timers.has_key?(name)
|
149
|
+
print "WARNING: #{name} already has a running timer, refusing to start a new timer"
|
150
|
+
return
|
151
|
+
end
|
152
|
+
@@in_progress_timers[name] = monotonic_timestamp
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.end_timer_metric(name:)
|
156
|
+
if !@@in_progress_timers.has_key?(name)
|
157
|
+
print "WARNING: #{name} does not have a running timer, the end_timer_metric call has no effect"
|
158
|
+
return
|
159
|
+
end
|
160
|
+
|
161
|
+
# Calculate delta
|
162
|
+
start = @@in_progress_timers[name]
|
163
|
+
now = monotonic_timestamp
|
164
|
+
delta_in_ms = ((now - start) * 1000).to_i
|
165
|
+
|
166
|
+
# remove existing timer
|
167
|
+
@@in_progress_timers.delete(name)
|
168
|
+
|
169
|
+
# log to file
|
170
|
+
overrides = {name: METRIC_TIMER, data: {name: name, duration: delta_in_ms }}
|
171
|
+
metric = merge_hashes(default_metric, overrides).to_json
|
172
|
+
append_to_log(metric)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Block based timer. This is the recommended way to timer operations.
|
176
|
+
def self.time(name:, &block)
|
177
|
+
self.start_timer_metric(name: name)
|
178
|
+
yield
|
179
|
+
self.end_timer_metric(name: name)
|
180
|
+
end
|
181
|
+
|
182
|
+
def self.upload_logs()
|
183
|
+
# Already called upload logs before, second time is a no-op
|
184
|
+
if @@log_file.nil? || !@@log_file.file?
|
185
|
+
print "WARN: Log file is empty or doesn't exist. Was upload_logs called previously?"
|
186
|
+
return
|
187
|
+
end
|
188
|
+
|
189
|
+
# Skip uploads for local dev machines
|
190
|
+
if !should_upload_logs()
|
191
|
+
print 'Detected local machine, refusing to upload build metrics. Removing intermediate log file'
|
192
|
+
remove_log_file()
|
193
|
+
return
|
194
|
+
end
|
195
|
+
|
196
|
+
# Warn for any open timers
|
197
|
+
if @@in_progress_timers.size > 0
|
198
|
+
@@in_progress_timers.each { |k,v|
|
199
|
+
print "WARN: Timer not closed when upload_logs was called: #{k}"
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
# Upload
|
204
|
+
upload_log_file()
|
205
|
+
|
206
|
+
# Remove log file
|
207
|
+
remove_log_file()
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.log_file()
|
211
|
+
@@log_file
|
212
|
+
end
|
213
|
+
|
214
|
+
def self.upload_location()
|
215
|
+
@@upload_location
|
216
|
+
end
|
217
|
+
|
218
|
+
# Tears down logging instance, removes any intermediate log files.
|
219
|
+
# Does not finalize or upload any in-progress logs.
|
220
|
+
# To use the logger again, you'll have to call set_default_values before any other methods.
|
221
|
+
def self.reset()
|
222
|
+
if @@log_file
|
223
|
+
remove_log_file()
|
224
|
+
end
|
225
|
+
@@log_file = nil
|
226
|
+
@@env_values = nil
|
227
|
+
@@in_progress_timers = {}
|
228
|
+
@@verbose = false
|
229
|
+
@@upload_location = nil
|
230
|
+
end
|
231
|
+
|
232
|
+
def self.remove_all_log_files()
|
233
|
+
FileUtils.rm_rf(LOGGING_DIR)
|
234
|
+
end
|
235
|
+
|
236
|
+
private
|
237
|
+
|
238
|
+
# Creates a stubbed metric with common fields
|
239
|
+
def self.default_metric()
|
240
|
+
if !@@env_values
|
241
|
+
print 'default_metric called before env_values initialized, aborting!'
|
242
|
+
exit 12
|
243
|
+
end
|
244
|
+
|
245
|
+
context = {
|
246
|
+
platform: @@env_values[:platform],
|
247
|
+
project: @@env_values[:project],
|
248
|
+
# name is overridden per metric, so not present here.
|
249
|
+
gitUrl: @@env_values[:gitUrl],
|
250
|
+
gitBranch: @@env_values[:gitBranch],
|
251
|
+
gitSha: @@env_values[:gitSha],
|
252
|
+
buildType: @@env_values[:buildType],
|
253
|
+
brand: @@env_values[:brand],
|
254
|
+
buildUrl: @@env_values[:buildUrl],
|
255
|
+
host: hostname(),
|
256
|
+
processId: build_number()
|
257
|
+
}.select { |k,v| !v.nil? }
|
258
|
+
{time: timestamp(), level:@@env_values[:level], context: context}
|
259
|
+
end
|
260
|
+
|
261
|
+
def self.append_to_log(value)
|
262
|
+
# Create the log file if it doesn't exist
|
263
|
+
if !@@log_file.file?
|
264
|
+
FileUtils.touch(@@log_file)
|
265
|
+
end
|
266
|
+
File.open(@@log_file, 'a+') { |f| f.puts(value + "\n") }
|
267
|
+
end
|
268
|
+
|
269
|
+
def self.remove_log_file()
|
270
|
+
FileUtils.rm_rf(@@log_file)
|
271
|
+
end
|
272
|
+
|
273
|
+
def self.upload_log_file()
|
274
|
+
destination = @@upload_location + "metrics_#{@@env_values[:project]}_#{build_number}.log"
|
275
|
+
command = "scp -v -o StrictHostKeyChecking=no #{@@log_file} #{destination}"
|
276
|
+
# This feels icky
|
277
|
+
print `#{command}`
|
278
|
+
end
|
279
|
+
|
280
|
+
# Returns a timestamp as a string in the correct logging format.
|
281
|
+
# Note: This format doesn't currently add milliseconds.
|
282
|
+
# Example: "2018-03-27T21:12:21Z"
|
283
|
+
# Note: DO NOT use this for benchmarking / measuring timers, see monotonic_timestamp instead.
|
284
|
+
def self.timestamp()
|
285
|
+
Time.now.utc.iso8601.to_s
|
286
|
+
end
|
287
|
+
|
288
|
+
# Returns a floating point monotonic timestamp suitable for timers
|
289
|
+
# Monotonic means we won't ever go back for things like leap seconds
|
290
|
+
# Unit is a floating point in seconds.
|
291
|
+
def self.monotonic_timestamp()
|
292
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
293
|
+
end
|
294
|
+
|
295
|
+
# Util method for merging the default hash values and overrides recursively as we want them
|
296
|
+
def self.merge_hashes(default, overrides)
|
297
|
+
default.merge(overrides) { |key, oldval, newval|
|
298
|
+
if key == :data || key == :contenxt
|
299
|
+
oldval.merge(newval)
|
300
|
+
else
|
301
|
+
newval
|
302
|
+
end
|
303
|
+
}
|
304
|
+
end
|
305
|
+
|
306
|
+
# Returns the host name of the machine we are currently running on
|
307
|
+
def self.hostname()
|
308
|
+
ENV['NODE_NAME'] || 'local-dev'
|
309
|
+
end
|
310
|
+
|
311
|
+
def self.build_number()
|
312
|
+
ENV['BUILD_NUMBER']
|
313
|
+
end
|
314
|
+
|
315
|
+
# This method checks against a bunch of default environment variables available through DOTCI
|
316
|
+
# The intent is never to upload timing logs from a local machine, so we make it unlikely that these
|
317
|
+
# env variables are present on a dev's local machine
|
318
|
+
def self.should_upload_logs()
|
319
|
+
have_workspace = ENV['WORKSPACE'] && !ENV['WORKSPACE'].empty?
|
320
|
+
have_build_number = ENV['BUILD_NUMBER'] && !ENV['BUILD_NUMBER'].empty?
|
321
|
+
have_jenkins_url = ENV['JENKINS_URL'] && !ENV['JENKINS_URL'].empty?
|
322
|
+
have_ci_var = ENV['CI'] && !ENV['CI'].empty?
|
323
|
+
have_workspace && have_build_number && have_jenkins_url && have_ci_var
|
324
|
+
end
|
325
|
+
|
326
|
+
def self.print(val)
|
327
|
+
puts("#{LOG_PREFIX} #{val}")
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "mobile_metrics/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "mobile_metrics"
|
8
|
+
spec.version = MobileMetrics::VERSION
|
9
|
+
spec.authors = ["dbeard"]
|
10
|
+
spec.email = ["dbeard@groupon.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Mobile logging format library}
|
13
|
+
# spec.homepage = "TODO: Put your gem's website or public repo URL here."
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features|README|scripts)/}) || f.match(%r{^(.ci|README.md)})
|
17
|
+
end
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.16"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mobile_metrics
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- dbeard
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-06-12 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- dbeard@groupon.com
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- ".rspec"
|
64
|
+
- ".travis.yml"
|
65
|
+
- Gemfile
|
66
|
+
- Gemfile.lock
|
67
|
+
- Rakefile
|
68
|
+
- bin/console
|
69
|
+
- bin/setup
|
70
|
+
- lib/mobile_metrics.rb
|
71
|
+
- lib/mobile_metrics/version.rb
|
72
|
+
- mobile_metrics.gemspec
|
73
|
+
homepage:
|
74
|
+
licenses: []
|
75
|
+
metadata: {}
|
76
|
+
post_install_message:
|
77
|
+
rdoc_options: []
|
78
|
+
require_paths:
|
79
|
+
- lib
|
80
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirements: []
|
91
|
+
rubyforge_project:
|
92
|
+
rubygems_version: 2.6.14
|
93
|
+
signing_key:
|
94
|
+
specification_version: 4
|
95
|
+
summary: Mobile logging format library
|
96
|
+
test_files: []
|