scout_apm 1.6.1 → 1.6.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cf9f6ca2e9039c82d55f3a6a120d3a9bb6b821d8
4
- data.tar.gz: f7530422c05a1437175806886a4d2e1873e4c59d
3
+ metadata.gz: 5dd32dd69f13bbb9d647caa56a2e17cfe89340e2
4
+ data.tar.gz: 4663534020a6d20226f566abd8ce9e3ba90d9263
5
5
  SHA512:
6
- metadata.gz: 5fd9a17e01253f7c2f1f1dea6933c94ea856411755e375442ab9dc85201f0618695c6aae72a3180f0d87e73827365a5b34e9b0f11591f1acd1965780346b708d
7
- data.tar.gz: 2f49baa4dff8fab01ad7244b3298ea06ff424d24c682dd2dcd50ed53f6bc9231e16b7b29f9764c6e0658330fc630da0c0c537ec3bb5653c1fe52bc46555b2eba
6
+ metadata.gz: 595e782a27c8721ba8fc7dc4760b672b94f37aed634b1ed68eced23d4d72401052dd2792b5889534cd287cecfb9fdcfbffdbf081fab38876b40c477346f5816d
7
+ data.tar.gz: 562921bd5380679a1efbf24dedced0becd8ab2da347897550b09468f6c967e9eada8e5702954d52c7dafcdbfd666ae79b1056c574656c61cbc8d269e6cca4293
data/.gitignore CHANGED
@@ -7,3 +7,4 @@ pkg/*
7
7
  .idea
8
8
  test/tmp/*coverage/*
9
9
  coverage/*
10
+ lib/*.bundle
data/CHANGELOG.markdown CHANGED
@@ -1,3 +1,8 @@
1
+ # 1.6.2
2
+
3
+ * Use a more flexible approach to storing "Layaway Files" (the temporary data
4
+ files the agent uses).
5
+
1
6
  # 1.6.1
2
7
 
3
8
  * Remove old & unused references to Stackprof. Prevent interaction with intentional usage of Stackprof
data/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ All components of this product, except where noted, are Copyright (c) 2013-2016 Zimuth, Inc DBA Scout. All rights reserved.
2
+
3
+ Certain inventions disclosed in this file may be claimed within patents owned or patent applications filed by Zimuth, Inc. or third parties.
4
+
5
+ Subject to the terms of this notice, Zimuth grants you a nonexclusive, nontransferable license, without the right to sublicense, to (a) install and execute one copy of these files on any number of workstations owned or controlled by you and (b) distribute verbatim copies of these files to third parties. As a condition to the foregoing grant, you must provide this notice along with each copy you distribute and you must not remove, alter, or obscure this notice. All other use, reproduction, modification, distribution, or other exploitation of these files is strictly prohibited, except as may be set forth in a separate written license agreement between you and Zimuth. The terms of any such license agreement will control over this notice. The license stated above will be automatically terminated and revoked if you exceed its scope or violate any of the terms of this notice.
6
+
7
+ This License does not grant permission to use the trade names, trademarks, service marks, or product names of Zimuth, except as required for reasonable and customary use in describing the origin of this file and reproducing the content of this notice. You may not mark or brand this file with any trade name, trademarks, service marks, or product names other than the original brand (if any) provided by Zimuth.
8
+
9
+ Unless otherwise expressly agreed by Zimuth in a separate written license agreement, these files are provided AS IS, WITHOUT WARRANTY OF ANY KIND, including without any implied warranties of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, or NON-INFRINGEMENT. As a condition to your use of these files, you are solely responsible for such use. Zimuth will have no liability to you for direct, indirect, consequential, incidental, special, or punitive damages or for lost profits or data.
@@ -24,17 +24,35 @@ module ScoutApm
24
24
  report_to_server
25
25
  end
26
26
 
27
- MAX_AGE_TO_REPORT = (10 * 60) # ten minutes as seconds
28
-
29
27
  # In a running app, one process will get one period ready for delivery, the others will see 0.
30
28
  def report_to_server
31
- reporting_periods = layaway.periods_ready_for_delivery
32
- reporting_periods.reject! {|rp| rp.timestamp.age_in_seconds > MAX_AGE_TO_REPORT }
33
- reporting_periods.each do |rp|
34
- deliver_period(rp)
29
+ period_to_report = ScoutApm::StoreReportingPeriodTimestamp.minutes_ago(2)
30
+
31
+ logger.debug("Attempting to claim #{period_to_report.to_s}")
32
+
33
+ did_write = layaway.with_claim(period_to_report) do |rps|
34
+ logger.debug("Succeeded claiming #{period_to_report.to_s}")
35
+
36
+ begin
37
+ merged = rps.inject { |memo, rp| memo.merge(rp) }
38
+ logger.debug("Merged #{rps.length} reporting periods, delivering")
39
+ deliver_period(merged)
40
+ true
41
+ rescue => e
42
+ logger.debug("Error merging reporting periods #{e.message}")
43
+ logger.debug("Error merging reporting periods #{e.backtrace}")
44
+ false
45
+ end
46
+
47
+ end
48
+
49
+ if !did_write
50
+ logger.debug("Failed to obtain claim for #{period_to_report.to_s}")
35
51
  end
52
+
36
53
  end
37
54
 
55
+
38
56
  def deliver_period(reporting_period)
39
57
  metrics = reporting_period.metrics_payload
40
58
  slow_transactions = reporting_period.slow_transactions_payload
@@ -1,53 +1,156 @@
1
- # Stores StoreReportingPeriod objects in a file before sending them to the server.
2
- # 1. A centralized store for multiple Agent processes. This way, only 1 checkin is sent to Scout rather than 1 per-process.
1
+ # Stores StoreReportingPeriod objects in a per-process file before sending them to the server.
2
+ # Coordinates a single process to collect up all individual files, merge them, then send.
3
+ #
4
+ # Each layaway file is named basedir/scout_#{timestamp}_#{pid}.data
5
+ # Where timestamp is in the format:
6
+ # And PID is the process id of the running process
7
+ #
3
8
  module ScoutApm
4
9
  class Layaway
5
- attr_accessor :file
10
+ # How old a file needs to be in Seconds before it gets reported.
11
+ REPORTING_AGE = 120
6
12
 
7
- def initialize
8
- @file = ScoutApm::LayawayFile.new
13
+ # How long to let a stale file sit before deleting it.
14
+ # Letting it sit a bit may be useful for debugging
15
+ STALE_AGE = 10 * 60
16
+
17
+ # A strftime format string for how we render timestamps in filenames.
18
+ # Must be sortable as an integer
19
+ TIME_FORMAT = "%Y%m%d%H%M"
20
+
21
+ def initialize(directory=nil)
22
+ @directory = directory
23
+ end
24
+
25
+ # Returns a Pathname object with the fully qualified directory where the layaway files can be placed.
26
+ # That directory must be writable by this process.
27
+ #
28
+ # Don't set this in initializer, since it relies on agent instance existing to figure out the value.
29
+ #
30
+ def directory
31
+ return @directory if @directory
32
+
33
+ data_file = ScoutApm::Agent.instance.config.value("data_file")
34
+ data_file = File.dirname(data_file) if data_file && !File.directory?
35
+
36
+ candidates = [
37
+ data_file,
38
+ "#{ScoutApm::Agent.instance.environment.root}/tmp",
39
+ "/tmp"
40
+ ].compact
41
+
42
+ found = candidates.detect { |dir| File.writable?(dir) }
43
+ ScoutApm::Agent.instance.logger.debug("Storing Layaway Files in #{found}")
44
+ @directory = Pathname.new(found)
45
+ end
46
+
47
+ def write_reporting_period(reporting_period)
48
+ filename = file_for(reporting_period.timestamp)
49
+ layaway_file = LayawayFile.new(filename)
50
+ layaway_file.write(reporting_period)
9
51
  end
10
52
 
11
- def add_reporting_period(time, reporting_period)
12
- file.read_and_write do |existing_data|
13
- existing_data ||= Hash.new
14
- ScoutApm::Agent.instance.logger.debug("AddReportingPeriod: Adding a reporting_period with timestamp: #{reporting_period.timestamp.to_s}, and #{reporting_period.request_count} requests")
53
+ # Claims a given timestamp (getting a lock on a particular filename),
54
+ # then yields ReportingPeriods collected up from all the files.
55
+ # If the yield returns truthy, delete the layaway files that made it up.
56
+ def with_claim(timestamp)
57
+ coordinator_file = glob_pattern(timestamp, :coordinator)
58
+
59
+
60
+ # This file gets deleted only by a process that successfully obtained a lock
61
+ f = File.open(coordinator_file, File::RDWR | File::CREAT)
62
+ begin
63
+ # Nonblocking, Exclusive lock.
64
+ if f.flock(File::LOCK_EX | File::LOCK_NB)
65
+
66
+ ScoutApm::Agent.instance.logger.debug("Obtained Reporting Lock")
15
67
 
16
- existing_data = existing_data.merge(time => reporting_period) {|key, old_val, new_val|
17
- old_req = old_val.request_count
18
- new_req = new_val.request_count
19
- ScoutApm::Agent.instance.logger.debug("Merging Two reporting periods (#{old_val.timestamp.to_s}, #{new_val.timestamp.to_s}): old req #{old_req}, new req #{new_req}")
68
+ files = all_files_for(timestamp).reject{|l| l.to_s == coordinator_file.to_s }
69
+ rps = files.map{ |layaway| LayawayFile.new(layaway).load }.compact
70
+ if rps.any?
71
+ yield rps
20
72
 
21
- old_val.merge(new_val)
22
- }
73
+ delete_files_for(timestamp) # also removes the coodinator_file
74
+ delete_stale_files(timestamp.to_time - STALE_AGE)
75
+ else
76
+ File.unlink(coordinator_file)
77
+ ScoutApm::Agent.instance.logger.debug("No layaway files to report")
78
+ end
23
79
 
24
- ScoutApm::Agent.instance.logger.debug("AddReportingPeriod: AfterMerge Timestamps: #{existing_data.keys.map(&:to_s).inspect}")
25
- existing_data
80
+ # Unlock the file when done!
81
+ f.flock(File::LOCK_UN | File::LOCK_NB)
82
+ f.close
83
+ true
84
+ else
85
+ # Didn't obtain lock, another process is reporting. Return false from this function, but otherwise no work
86
+ f.close
87
+ false
88
+ end
26
89
  end
27
90
  end
28
91
 
29
- REPORTING_INTERVAL = 60 # seconds
92
+ def delete_files_for(timestamp)
93
+ all_files_for(timestamp).each { |layaway| File.unlink(layaway) }
94
+ end
95
+
96
+ def delete_stale_files(older_than)
97
+ all_files_for(:all).
98
+ map { |filename| timestamp_from_filename(filename) }.
99
+ compact.
100
+ uniq.
101
+ select { |timestamp| timestamp.to_i < older_than.strftime(TIME_FORMAT).to_i }.
102
+ tap { |timestamps| ScoutApm::Agent.instance.logger.debug("Deleting stale layaway files with timestamps: #{timestamps.inspect}") }.
103
+ map { |timestamp| delete_files_for(timestamp) }
104
+ end
30
105
 
31
- # Returns an array of ReportingPeriod objects that are ready to be pushed to the server
32
- def periods_ready_for_delivery
33
- ready_for_delivery = []
34
- file.read_and_write do |existing_data|
35
- existing_data ||= {}
106
+ private
107
+
108
+ ##########################################
109
+ # Looking up files
110
+
111
+ def file_for(timestamp)
112
+ glob_pattern(timestamp)
113
+ end
36
114
 
37
- ScoutApm::Agent.instance.logger.debug("PeriodsReadyForDeliver: All Timestamps: #{existing_data.keys.map(&:to_s).inspect}")
115
+ def all_files_for(timestamp)
116
+ Dir[glob_pattern(timestamp, :all)]
117
+ end
38
118
 
39
- ready_for_delivery = existing_data.to_a.select {|time, rp| should_send?(rp) } # Select off the values we want. to_a is needed for compatibility with Ruby 1.8.7.
119
+ # Timestamp should be either :all or a Time-ish object that responds to strftime (StoreReportingPeriodTimestamp does)
120
+ # if timestamp == :all then find all timestamps, otherwise format it.
121
+ # if pid == :all, get the files for all
122
+ def glob_pattern(timestamp, pid=$$)
123
+ timestamp_pattern = format_timestamp(timestamp)
124
+ pid_pattern = format_pid(pid)
125
+ directory + "scout_#{timestamp_pattern}_#{pid_pattern}.data"
126
+ end
40
127
 
41
- # Rewrite anything not plucked out back to the file
42
- existing_data.reject {|k, v| ready_for_delivery.map(&:first).include?(k) }
128
+ def format_timestamp(timestamp)
129
+ if timestamp == :all
130
+ "*"
131
+ elsif timestamp.respond_to?(:strftime)
132
+ timestamp.strftime(TIME_FORMAT)
133
+ else
134
+ timestamp.to_s
43
135
  end
136
+ end
44
137
 
45
- return ready_for_delivery.map(&:last)
138
+ def format_pid(pid)
139
+ if pid == :all
140
+ "*"
141
+ else
142
+ pid.to_s
143
+ end
46
144
  end
47
145
 
48
- # We just want to send anything older than X
49
- def should_send?(reporting_period)
50
- reporting_period.timestamp.age_in_seconds > (REPORTING_INTERVAL * 2)
146
+ def timestamp_from_filename(filename)
147
+ match = filename.match(%r{scout_(.*)_.*\.data})
148
+ if match
149
+ match[1]
150
+ else
151
+ nil
152
+ end
51
153
  end
52
154
  end
53
155
  end
156
+
@@ -1,94 +1,36 @@
1
- # Logic for the serialized file access
1
+ # A single layaway file. See Layaway for the management of the group of files.
2
2
  module ScoutApm
3
3
  class LayawayFile
4
- def path
5
- return @path if @path
4
+ attr_reader :path
6
5
 
7
- candidates = [
8
- ScoutApm::Agent.instance.config.value("data_file"),
9
- "#{ScoutApm::Agent.instance.default_log_path}/scout_apm.db",
10
- "#{ScoutApm::Agent.instance.environment.root}/tmp/scout_apm.db"
11
- ]
12
-
13
- candidates.each do |candidate|
14
- next if candidate.nil?
15
-
16
- begin
17
- ScoutApm::Agent.instance.logger.debug("Checking Layaway File Location: #{candidate}")
18
- File.open(candidate, "w") { |f| } # Open & Close to check that we can
19
-
20
- # No exception, it is valid
21
- ScoutApm::Agent.instance.logger.info("Layaway File location found: #{candidate}")
22
- @path = candidate
23
- return @path
24
- rescue Exception
25
- ScoutApm::Agent.instance.logger.debug("Couldn't open layaway file for test write at #{candidate}")
26
- end
27
- end
28
-
29
- ScoutApm::Agent.instance.logger.error("No valid layaway file found, please set a location in the configuration key `data_file`")
30
- nil
6
+ def initialize(path)
7
+ @path = path
31
8
  end
32
9
 
33
- def dump(object)
34
- Marshal.dump(object)
35
- end
36
-
37
- def load(dump)
38
- if dump.size == 0
39
- ScoutApm::Agent.instance.logger.debug("No data in layaway file.")
40
- return nil
41
- end
42
- Marshal.load(dump)
10
+ def load
11
+ data = File.open(path, "r") { |f| read_raw(f) }
12
+ deserialize(data)
43
13
  rescue NameError, ArgumentError, TypeError => e
14
+ # Marshal error
44
15
  ScoutApm::Agent.instance.logger.info("Unable to load data from Layaway file, resetting.")
45
16
  ScoutApm::Agent.instance.logger.debug("#{e.message}, #{e.backtrace.join("\n\t")}")
46
17
  nil
47
18
  end
48
19
 
49
- def read_and_write
50
- File.open(path, File::RDWR | File::CREAT) do |f|
51
- f.flock(File::LOCK_EX)
52
- begin
53
- result = (yield get_data(f))
54
- f.rewind
55
- f.truncate(0)
56
- if result
57
- write(f, dump(result))
58
- end
59
- ensure
60
- f.flock(File::LOCK_UN)
61
- end
62
- end
63
- rescue Errno::ENOENT, Exception => e
64
- ScoutApm::Agent.instance.logger.error("Unable to access the layaway file [#{e.class} - #{e.message}]. " +
65
- "The user running the app must have read & write access. " +
66
- "Change the path by setting the `data_file` key in scout_apm.yml"
67
- )
68
- ScoutApm::Agent.instance.logger.debug(e.backtrace.join("\n\t"))
69
-
70
- # ensure the in-memory metric hash is cleared so data doesn't continue to accumulate.
71
- # ScoutApm::Agent.instance.store.metric_hash = {}
20
+ def write(data)
21
+ serialized_data = serialize(data)
22
+ File.open(path, "w") { |f| write_raw(f, serialized_data) }
72
23
  end
73
24
 
74
- def get_data(f)
75
- data = read_until_end(f)
76
- result = load(data)
77
- f.truncate(0)
78
- result
25
+ def serialize(data)
26
+ Marshal.dump(data)
79
27
  end
80
28
 
81
- def write(f, string)
82
- result = 0
83
- while (result < string.length)
84
- result += f.write_nonblock(string)
85
- end
86
- rescue Errno::EAGAIN, Errno::EINTR
87
- IO.select(nil, [f])
88
- retry
29
+ def deserialize(data)
30
+ Marshal.load(data)
89
31
  end
90
32
 
91
- def read_until_end(f)
33
+ def read_raw(f)
92
34
  contents = ""
93
35
  while true
94
36
  contents << f.read_nonblock(10_000)
@@ -99,5 +41,16 @@ module ScoutApm
99
41
  rescue EOFError
100
42
  contents
101
43
  end
44
+
45
+ def write_raw(f, data)
46
+ result = 0
47
+ while (result < data.length)
48
+ result += f.write_nonblock(data)
49
+ end
50
+ rescue Errno::EAGAIN, Errno::EINTR
51
+ IO.select(nil, [f])
52
+ retry
53
+ end
102
54
  end
103
55
  end
56
+
@@ -71,11 +71,10 @@ module ScoutApm
71
71
  reporting_periods.select { |time, rp| force || time.timestamp < current_timestamp.timestamp}.
72
72
  each { |time, rp|
73
73
  collect_samplers(rp)
74
- layaway.add_reporting_period(time, rp)
74
+ layaway.write_reporting_period(rp)
75
75
  reporting_periods.delete(time)
76
76
  }
77
77
  }
78
- ScoutApm::Agent.instance.logger.debug("Finished writing to layaway")
79
78
  end
80
79
 
81
80
  ######################################
@@ -108,8 +107,25 @@ module ScoutApm
108
107
  @timestamp = @raw_time.to_i - @raw_time.sec # The normalized time (integer) to compare by
109
108
  end
110
109
 
110
+ def self.minutes_ago(min, base_time=Time.now)
111
+ adjusted = base_time - (min * 60)
112
+ new(adjusted)
113
+ end
114
+
111
115
  def to_s
112
- Time.at(@timestamp).iso8601
116
+ strftime
117
+ end
118
+
119
+ def strftime(pattern=nil)
120
+ if pattern.nil?
121
+ to_time.iso8601
122
+ else
123
+ to_time.strftime(pattern)
124
+ end
125
+ end
126
+
127
+ def to_time
128
+ Time.at(@timestamp)
113
129
  end
114
130
 
115
131
  def eql?(o)
@@ -1,4 +1,4 @@
1
1
  module ScoutApm
2
- VERSION = "1.6.1"
2
+ VERSION = "1.6.2"
3
3
  end
4
4
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scout_apm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1
4
+ version: 1.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derek Haynes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-06-16 00:00:00.000000000 Z
12
+ date: 2016-06-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: minitest
@@ -77,10 +77,10 @@ files:
77
77
  - ".gitignore"
78
78
  - CHANGELOG.markdown
79
79
  - Gemfile
80
+ - LICENSE.md
80
81
  - README.markdown
81
82
  - Rakefile
82
83
  - data/cacert.pem
83
- - lib/allocations.bundle
84
84
  - lib/scout_apm.rb
85
85
  - lib/scout_apm/agent.rb
86
86
  - lib/scout_apm/agent/logging.rb
@@ -181,7 +181,6 @@ files:
181
181
  - lib/scout_apm/utils/time.rb
182
182
  - lib/scout_apm/utils/unique_id.rb
183
183
  - lib/scout_apm/version.rb
184
- - lib/stacks.bundle
185
184
  - scout_apm.gemspec
186
185
  - test/data/config_test_1.yml
187
186
  - test/test_helper.rb
Binary file
data/lib/stacks.bundle DELETED
Binary file