scout_apm 2.3.5 → 2.4.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.markdown +0 -23
  3. data/lib/scout_apm.rb +21 -10
  4. data/lib/scout_apm/agent.rb +98 -336
  5. data/lib/scout_apm/agent/exit_handler.rb +64 -0
  6. data/lib/scout_apm/agent/preconditions.rb +69 -0
  7. data/lib/scout_apm/agent_context.rb +226 -0
  8. data/lib/scout_apm/app_server_load.rb +20 -18
  9. data/lib/scout_apm/background_job_integrations/resque.rb +7 -8
  10. data/lib/scout_apm/background_job_integrations/sidekiq.rb +2 -8
  11. data/lib/scout_apm/background_recorder.rb +8 -3
  12. data/lib/scout_apm/background_worker.rb +14 -7
  13. data/lib/scout_apm/config.rb +35 -29
  14. data/lib/scout_apm/context.rb +11 -4
  15. data/lib/scout_apm/db_query_metric_set.rb +17 -5
  16. data/lib/scout_apm/debug.rb +1 -1
  17. data/lib/scout_apm/environment.rb +10 -14
  18. data/lib/scout_apm/framework_integrations/sinatra.rb +1 -1
  19. data/lib/scout_apm/git_revision.rb +13 -8
  20. data/lib/scout_apm/histogram.rb +1 -1
  21. data/lib/scout_apm/instant/middleware.rb +7 -7
  22. data/lib/scout_apm/instant_reporting.rb +7 -7
  23. data/lib/scout_apm/instrument_manager.rb +87 -0
  24. data/lib/scout_apm/instruments/action_controller_rails_2.rb +12 -7
  25. data/lib/scout_apm/instruments/action_controller_rails_3_rails4.rb +16 -11
  26. data/lib/scout_apm/instruments/action_view.rb +11 -7
  27. data/lib/scout_apm/instruments/active_record.rb +28 -51
  28. data/lib/scout_apm/instruments/elasticsearch.rb +10 -6
  29. data/lib/scout_apm/instruments/grape.rb +12 -8
  30. data/lib/scout_apm/instruments/http_client.rb +11 -10
  31. data/lib/scout_apm/instruments/influxdb.rb +10 -6
  32. data/lib/scout_apm/instruments/middleware_detailed.rb +11 -5
  33. data/lib/scout_apm/instruments/middleware_summary.rb +11 -5
  34. data/lib/scout_apm/instruments/mongoid.rb +10 -5
  35. data/lib/scout_apm/instruments/moped.rb +11 -6
  36. data/lib/scout_apm/instruments/net_http.rb +11 -9
  37. data/lib/scout_apm/instruments/percentile_sampler.rb +8 -6
  38. data/lib/scout_apm/instruments/process/process_cpu.rb +8 -4
  39. data/lib/scout_apm/instruments/process/process_memory.rb +15 -10
  40. data/lib/scout_apm/instruments/rails_router.rb +12 -6
  41. data/lib/scout_apm/instruments/redis.rb +10 -6
  42. data/lib/scout_apm/instruments/samplers.rb +11 -0
  43. data/lib/scout_apm/instruments/sinatra.rb +5 -4
  44. data/lib/scout_apm/layaway.rb +26 -39
  45. data/lib/scout_apm/layaway_file.rb +8 -3
  46. data/lib/scout_apm/layer.rb +1 -1
  47. data/lib/scout_apm/layer_converters/converter_base.rb +4 -2
  48. data/lib/scout_apm/layer_converters/database_converter.rb +1 -1
  49. data/lib/scout_apm/layer_converters/histograms.rb +2 -2
  50. data/lib/scout_apm/layer_converters/slow_job_converter.rb +4 -3
  51. data/lib/scout_apm/layer_converters/slow_request_converter.rb +5 -4
  52. data/lib/scout_apm/logger.rb +143 -0
  53. data/lib/scout_apm/middleware.rb +7 -9
  54. data/lib/scout_apm/periodic_work.rb +28 -0
  55. data/lib/scout_apm/remote/server.rb +0 -2
  56. data/lib/scout_apm/reporter.rb +14 -8
  57. data/lib/scout_apm/reporting.rb +135 -0
  58. data/lib/scout_apm/request_manager.rb +4 -7
  59. data/lib/scout_apm/serializers/payload_serializer.rb +1 -1
  60. data/lib/scout_apm/slow_job_policy.rb +6 -2
  61. data/lib/scout_apm/slow_job_record.rb +5 -5
  62. data/lib/scout_apm/slow_request_policy.rb +6 -2
  63. data/lib/scout_apm/slow_transaction.rb +5 -5
  64. data/lib/scout_apm/store.rb +22 -16
  65. data/lib/scout_apm/synchronous_recorder.rb +7 -3
  66. data/lib/scout_apm/tasks/doctor.rb +75 -0
  67. data/lib/scout_apm/tasks/support.rb +22 -0
  68. data/lib/scout_apm/tracer.rb +5 -5
  69. data/lib/scout_apm/tracked_request.rb +23 -35
  70. data/lib/scout_apm/utils/backtrace_parser.rb +1 -1
  71. data/lib/scout_apm/utils/installed_gems.rb +7 -3
  72. data/lib/scout_apm/utils/klass_helper.rb +8 -2
  73. data/lib/scout_apm/utils/scm.rb +1 -1
  74. data/lib/scout_apm/utils/sql_sanitizer.rb +4 -6
  75. data/lib/scout_apm/version.rb +1 -1
  76. data/lib/tasks/doctor.rake +11 -0
  77. data/test/test_helper.rb +15 -2
  78. data/test/unit/agent_test.rb +1 -54
  79. data/test/unit/config_test.rb +9 -5
  80. data/test/unit/context_test.rb +4 -4
  81. data/test/unit/db_query_metric_set_test.rb +11 -4
  82. data/test/unit/fake_store_test.rb +1 -1
  83. data/test/unit/git_revision_test.rb +3 -3
  84. data/test/unit/instruments/net_http_test.rb +2 -1
  85. data/test/unit/instruments/percentile_sampler_test.rb +5 -9
  86. data/test/unit/layaway_test.rb +10 -5
  87. data/test/unit/layer_converters/metric_converter_test.rb +2 -2
  88. data/test/unit/slow_request_policy_test.rb +7 -3
  89. data/test/unit/sql_sanitizer_test.rb +0 -6
  90. data/test/unit/store_test.rb +11 -8
  91. metadata +15 -7
  92. data/lib/scout_apm/agent/logging.rb +0 -74
  93. data/lib/scout_apm/agent/reporting.rb +0 -129
  94. data/lib/scout_apm/utils/null_logger.rb +0 -13
@@ -4,10 +4,14 @@
4
4
 
5
5
  module ScoutApm
6
6
  class SynchronousRecorder
7
- attr_reader :logger
7
+ attr_reader :context
8
8
 
9
- def initialize(logger)
10
- @logger = logger
9
+ def initialize(context)
10
+ @context = context
11
+ end
12
+
13
+ def logger
14
+ context.logger
11
15
  end
12
16
 
13
17
  def start
@@ -0,0 +1,75 @@
1
+ module ScoutApm
2
+ module Tasks
3
+ class Doctor
4
+ def self.run!
5
+ new.run!
6
+ end
7
+
8
+ def initialize()
9
+ end
10
+
11
+ def run!
12
+ puts "Scout Doctor"
13
+ puts "============"
14
+ puts
15
+ puts "Detected App Server: #{agent_context.environment.app_server_integration.name}"
16
+ # puts "Detected Background Job: #{agent_context.environment.background_job_integration.name}"
17
+ puts
18
+ puts "Instruments:"
19
+ puts "----------------------------------------"
20
+ puts installed_instruments
21
+ puts
22
+ puts
23
+ puts "Configuration Settings:"
24
+ puts "-------------|------------------------------|-------"
25
+ puts " From | Key | Value "
26
+ puts "-------------|------------------------------|-------"
27
+ puts configuration_settings
28
+ puts
29
+ puts
30
+ puts "Misc:"
31
+ puts "---------------"
32
+ puts "Layaway Files stored at: #{agent_context.layaway.directory}"
33
+ puts "Logs stored at: #{log_details}"
34
+
35
+ end
36
+
37
+ def agent_context
38
+ ScoutApm::Agent.instance.context
39
+ end
40
+
41
+ def installed_instruments
42
+ ScoutApm::Agent.
43
+ instance.
44
+ instrument_manager.
45
+ installed_instruments.
46
+ map{|instance| "#{instance.installed? ? "Installed " : "Not Installed"} - #{instance.class.to_s}"}.
47
+ join("\n")
48
+ end
49
+
50
+ def configuration_settings
51
+ all_settings = agent_context.config.all_settings
52
+
53
+ longest_key = all_settings.
54
+ map{|setting| setting[:key] }.
55
+ inject(0) { |len, key| key.length > len ? key.length : len }
56
+
57
+ format_string = "%12s | %-#{longest_key}s | %s"
58
+
59
+ all_settings.
60
+ map{|setting| sprintf format_string, setting[:source], setting[:key], setting[:value]}.
61
+ join("\n")
62
+ end
63
+
64
+ def log_details
65
+ if agent_context.logger.log_destination == STDOUT
66
+ "STDOUT"
67
+ elsif agent_context.logger.log_destination == STDERR
68
+ "STDERR"
69
+ else
70
+ "#{agent_context.logger.log_file_path}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,22 @@
1
+ module ScoutApm
2
+ module Tasks
3
+ class Support
4
+ def self.run!
5
+ puts "Support Task"
6
+ new.run!
7
+ end
8
+
9
+ def initialize
10
+ @doctor = ScoutApm::Tasks::Doctor.new
11
+ end
12
+
13
+ def run!
14
+ instruments = @doctor.installed_instruments
15
+ config = @doctor.configuration_settings
16
+ collect_logs
17
+
18
+ post_data
19
+ end
20
+ end
21
+ end
22
+ end
@@ -45,13 +45,13 @@ module ScoutApm
45
45
  # type - "View" or "ActiveRecord" and similar
46
46
  # name - "users/show", "App#find"
47
47
  def instrument_method(method_name, options = {})
48
- ScoutApm::Agent.instance.logger.info "Instrumenting #{method_name}"
48
+ ScoutApm::Agent.instance.context.logger.info "Instrumenting #{method_name}"
49
49
  type = options[:type] || "Custom"
50
50
  name = options[:name] || "#{self.name}/#{method_name.to_s}"
51
51
 
52
52
  instrumented_name, uninstrumented_name = _determine_instrumented_name(method_name, type)
53
53
 
54
- ScoutApm::Agent.instance.logger.info "Instrumenting #{instrumented_name}, #{uninstrumented_name}"
54
+ ScoutApm::Agent.instance.context.logger.info "Instrumenting #{instrumented_name}, #{uninstrumented_name}"
55
55
 
56
56
  return if !_instrumentable?(method_name) or _instrumented?(instrumented_name, method_name)
57
57
 
@@ -91,7 +91,7 @@ module ScoutApm
91
91
  name = begin
92
92
  "#{name}"
93
93
  rescue => e
94
- ScoutApm::Agent.instance.logger.error("Error raised while interpreting instrumented name: %s, %s" % ['#{name}', e.message])
94
+ ScoutApm::Agent.instance.context.logger.error("Error raised while interpreting instrumented name: %s, %s" % ['#{name}', e.message])
95
95
  "Unknown"
96
96
  end
97
97
 
@@ -110,14 +110,14 @@ module ScoutApm
110
110
  # The method must exist to be instrumented.
111
111
  def _instrumentable?(method_name)
112
112
  exists = method_defined?(method_name) || private_method_defined?(method_name)
113
- ScoutApm::Agent.instance.logger.warn "The method [#{self.name}##{method_name}] does not exist and will not be instrumented" unless exists
113
+ ScoutApm::Agent.instance.context.logger.warn "The method [#{self.name}##{method_name}] does not exist and will not be instrumented" unless exists
114
114
  exists
115
115
  end
116
116
 
117
117
  # +True+ if the method is already instrumented.
118
118
  def _instrumented?(instrumented_name, method_name)
119
119
  instrumented = method_defined?(instrumented_name)
120
- ScoutApm::Agent.instance.logger.warn("The method [#{self.name}##{method_name}] has already been instrumented") if instrumented
120
+ ScoutApm::Agent.instance.context.logger.warn("The method [#{self.name}##{method_name}] has already been instrumented") if instrumented
121
121
  instrumented
122
122
  end
123
123
 
@@ -39,26 +39,23 @@ module ScoutApm
39
39
  # this is set in the controller instumentation (ActionControllerRails3Rails4 according)
40
40
  attr_accessor :instant_key
41
41
 
42
- # Whereas the instant_key gets set per-request in reponse to a URL param, dev_trace is set in the config file
43
- attr_accessor :dev_trace
44
-
45
42
  # An object that responds to `record!(TrackedRequest)` to store this tracked request
46
43
  attr_reader :recorder
47
44
 
48
- def initialize(store)
45
+ def initialize(agent_context, store)
46
+ @agent_context = agent_context
49
47
  @store = store #this is passed in so we can use a real store (normal operation) or fake store (instant mode only)
50
48
  @layers = []
51
49
  @call_set = Hash.new { |h, k| h[k] = CallSet.new }
52
50
  @annotations = {}
53
51
  @ignoring_children = 0
54
- @context = Context.new
52
+ @context = Context.new(agent_context)
55
53
  @root_layer = nil
56
54
  @error = false
57
55
  @stopping = false
58
56
  @instant_key = nil
59
57
  @mem_start = mem_usage
60
- @dev_trace = ScoutApm::Agent.instance.config.value('dev_trace') && ScoutApm::Agent.instance.environment.env == "development"
61
- @recorder = ScoutApm::Agent.instance.recorder
58
+ @recorder = agent_context.recorder
62
59
 
63
60
  ignore_request! if @recorder.nil?
64
61
  end
@@ -152,9 +149,15 @@ module ScoutApm
152
149
  @call_set[layer.name].update!(layer.desc)
153
150
  end
154
151
 
152
+ # Grab backtraces more aggressively when running in dev trace mode
153
+ def backtrace_threshold
154
+ @agent_context.dev_trace_enabled? ? 0.05 : 0.5 # the minimum threshold in seconds to record the backtrace for a metric.
155
+ end
156
+
155
157
  # This may be in bytes or KB based on the OSX. We store this as-is here and only do conversion to MB in Layer Converters.
158
+ # XXX: Move this to environment?
156
159
  def mem_usage
157
- ScoutApm::Instruments::Process::ProcessMemory.rss
160
+ ScoutApm::Instruments::Process::ProcessMemory.new(@agent_context).rss
158
161
  end
159
162
 
160
163
  def capture_mem_delta!
@@ -261,7 +264,7 @@ module ScoutApm
261
264
  restore_store if @store.nil?
262
265
 
263
266
  # Bail out early if the user asked us to ignore this uri
264
- return if ScoutApm::Agent.instance.ignored_uris.ignore?(annotations[:uri])
267
+ return if @agent_context.ignored_uris.ignore?(annotations[:uri])
265
268
 
266
269
  converters = [
267
270
  LayerConverters::Histograms,
@@ -279,7 +282,7 @@ module ScoutApm
279
282
  layer_finder = LayerConverters::FindLayerByType.new(self)
280
283
  walker = LayerConverters::DepthFirstWalker.new(self.root_layer)
281
284
  converters = converters.map do |klass|
282
- instance = klass.new(self, layer_finder, @store)
285
+ instance = klass.new(@agent_context, self, layer_finder, @store)
283
286
  instance.register_hooks(walker)
284
287
  instance
285
288
  end
@@ -292,26 +295,8 @@ module ScoutApm
292
295
  trace = converter.call
293
296
  ScoutApm::InstantReporting.new(trace, instant_key).call
294
297
  end
295
-
296
- if web? || job?
297
- ensure_background_worker
298
- end
299
- end
300
-
301
- # Ensure the background worker thread is up & running - a fallback if other
302
- # detection doesn't achieve this at boot.
303
- def ensure_background_worker
304
- agent = ScoutApm::Agent.instance
305
- agent.start(:skip_app_server_check => true) unless agent.started?
306
-
307
- if agent.start_background_worker(:quiet)
308
- agent.logger.info("Force Started BG Worker")
309
- end
310
- rescue => e
311
- true
312
298
  end
313
299
 
314
-
315
300
  # Only call this after the request is complete
316
301
  def unique_name
317
302
  return nil if ignoring_request?
@@ -376,11 +361,6 @@ module ScoutApm
376
361
  @ignoring_children > 0
377
362
  end
378
363
 
379
- # Grab backtraces more aggressively when running in dev trace mode
380
- def backtrace_threshold
381
- dev_trace ? 0.05 : 0.5 # the minimum threshold in seconds to record the backtrace for a metric.
382
- end
383
-
384
364
  ################################################################################
385
365
  # Ignoring the rest of a request
386
366
  ################################################################################
@@ -390,6 +370,10 @@ module ScoutApm
390
370
  # layers, and delete any existing layer info. This class will still exist,
391
371
  # and respond to methods as normal, but `record!` won't be called, and no
392
372
  # data will be recorded.
373
+ #
374
+ # We still need to keep track of the current layer depth (via
375
+ # @ignoring_depth counter) so we know when to report that the class was
376
+ # "reported", and ready to be recreated for the next request.
393
377
 
394
378
  def ignore_request!
395
379
  return if @ignoring_request
@@ -425,9 +409,13 @@ module ScoutApm
425
409
  end
426
410
 
427
411
  def logger
428
- ScoutApm::Agent.instance.logger
412
+ @agent_context.logger
429
413
  end
430
414
 
415
+ ###########################
416
+ # Serialization Helpers
417
+ ###########################
418
+
431
419
  # Actually go fetch & make-real any lazily created data.
432
420
  # Clean up any cleverness in objects.
433
421
  # Makes this object ready to be Marshal Dumped (or otherwise serialized)
@@ -440,7 +428,7 @@ module ScoutApm
440
428
  # Go re-fetch the store based on what the Agent's official one is. Used
441
429
  # after hydrating a dumped TrackedRequest
442
430
  def restore_store
443
- @store = ScoutApm::Agent.instance.store
431
+ @store = @agent_context.store
444
432
  end
445
433
  end
446
434
  end
@@ -11,7 +11,7 @@ module ScoutApm
11
11
 
12
12
  attr_reader :call_stack
13
13
 
14
- def initialize(call_stack, root=ScoutApm::Environment.instance.root)
14
+ def initialize(call_stack, root=ScoutApm::Agent.instance.context.environment.root)
15
15
  @call_stack = call_stack
16
16
  # We can't use a constant as it'd be too early to fetch environment info
17
17
  #
@@ -1,10 +1,14 @@
1
1
  module ScoutApm
2
2
  module Utils
3
3
  class InstalledGems
4
- attr_reader :logger
4
+ attr_reader :context
5
5
 
6
- def initialize(logger=ScoutApm::Agent.instance.logger)
7
- @logger = logger
6
+ def initialize(context)
7
+ @context = context
8
+ end
9
+
10
+ def logger
11
+ context.logger
8
12
  end
9
13
 
10
14
  def run
@@ -5,6 +5,12 @@ module ScoutApm
5
5
  # KlassHelper.defined?("ActiveRecord::Base") #=> true / false
6
6
 
7
7
  def self.defined?(*names)
8
+ lookup(*names) != :missing_class
9
+ end
10
+
11
+ # KlassHelper.lookup("ActiveRecord::Base") => ActiveRecord::Base
12
+ # KlassHelper.lookup("ActiveRecord::SomethingThatDoesNotExist") => :missing_class
13
+ def self.lookup(*names)
8
14
  if names.length == 1
9
15
  names = names[0].split("::")
10
16
  end
@@ -15,11 +21,11 @@ module ScoutApm
15
21
  begin
16
22
  obj = obj.const_get(name)
17
23
  rescue NameError
18
- return false
24
+ return :missing_class
19
25
  end
20
26
  end
21
27
 
22
- true
28
+ obj
23
29
  end
24
30
  end
25
31
  end
@@ -4,7 +4,7 @@ module ScoutApm
4
4
  class Scm
5
5
  # Takes an *already relative* path +path+
6
6
  # Returns a relative path, prepending the configured +scm_subdirectory+ environment string
7
- def self.relative_scm_path(path, scm_subdirectory = ScoutApm::Environment.instance.scm_subdirectory)
7
+ def self.relative_scm_path(path, scm_subdirectory = ScoutApm::Agent.instance.context.environment.scm_subdirectory)
8
8
  @@scm_subdirectory ||= scm_subdirectory.sub(/^\//, '')
9
9
  @@scm_subdirectoy_blank ||= @@scm_subdirectory.empty?
10
10
  @@scm_subdirectoy_blank ? path : File.join(@@scm_subdirectory, path)
@@ -16,7 +16,7 @@ module ScoutApm
16
16
 
17
17
  def initialize(sql)
18
18
  @raw_sql = sql
19
- @database_engine = ScoutApm::Environment.instance.database_engine
19
+ @database_engine = ScoutApm::Agent.instance.context.environment.database_engine
20
20
  @sanitized = false # only sanitize once.
21
21
  end
22
22
 
@@ -74,19 +74,17 @@ module ScoutApm
74
74
  encodings.all?{|enc| Encoding.find(enc) rescue false}
75
75
  end
76
76
 
77
- MAX_SQL_LENGTH = 16384
78
-
79
77
  def scrubbed(str)
80
- return '' if !str.is_a?(String) || str.length > MAX_SQL_LENGTH # safeguard - don't sanitize or scrub large SQL statements
78
+ return '' if !str.is_a?(String) || str.length > 4000 # safeguard - don't sanitize or scrub large SQL statements
81
79
  return str if !str.respond_to?(:encode) # Ruby <= 1.8 doesn't have string encoding
82
80
  return str if str.valid_encoding? # Whatever encoding it is, it is valid and we can operate on it
83
- ScoutApm::Agent.instance.logger.debug "Scrubbing invalid sql encoding."
81
+ ScoutApm::Agent.instance.context.logger.debug "Scrubbing invalid sql encoding."
84
82
  if str.respond_to?(:scrub) # Prefer to scrub before we have to convert
85
83
  return str.scrub('_')
86
84
  elsif has_encodings?(['UTF-8', 'binary'])
87
85
  return str.encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '_')
88
86
  end
89
- ScoutApm::Agent.instance.logger.debug "Unable to scrub invalid sql encoding."
87
+ ScoutApm::Agent.instance.context.logger.debug "Unable to scrub invalid sql encoding."
90
88
  ''
91
89
  end
92
90
 
@@ -1,4 +1,4 @@
1
1
  module ScoutApm
2
- VERSION = "2.3.5"
2
+ VERSION = "2.4.0.pre"
3
3
  end
4
4
 
@@ -0,0 +1,11 @@
1
+ namespace :scout do
2
+ desc "Prints out details of the detected environment"
3
+ task :doctor => :environment do
4
+ ScoutApm::Tasks::Doctor.run!
5
+ end
6
+
7
+ desc "Collect logs, settings and environment to help debug issues"
8
+ task :support => :environment do
9
+ ScoutApm::Tasks::Support.run!
10
+ end
11
+ end
data/test/test_helper.rb CHANGED
@@ -41,6 +41,14 @@ class FakeConfigOverlay
41
41
  def has_key?(key)
42
42
  @values.has_key?(key)
43
43
  end
44
+
45
+ def name
46
+ "agent-test-config-overlay"
47
+ end
48
+
49
+ def any_keys_found?
50
+ true
51
+ end
44
52
  end
45
53
 
46
54
  class FakeEnvironment
@@ -66,7 +74,7 @@ class Minitest::Test
66
74
  end
67
75
 
68
76
  def teardown
69
- ScoutApm::Agent.instance.shutdown
77
+ ScoutApm::Agent.instance.stop_background_worker
70
78
  File.delete(DATA_FILE_PATH) if File.exist?(DATA_FILE_PATH)
71
79
  end
72
80
 
@@ -85,8 +93,13 @@ class Minitest::Test
85
93
  FakeEnvironment.new(values)
86
94
  end
87
95
 
96
+ # XXX: Make it easy to override context here?
88
97
  def make_fake_config(values)
89
- ScoutApm::Config.new(FakeConfigOverlay.new(values))
98
+ ScoutApm::Config.new(agent_context, [FakeConfigOverlay.new(values), ScoutApm::Config::ConfigNull.new] )
99
+ end
100
+
101
+ def agent_context
102
+ ScoutApm::AgentContext.new
90
103
  end
91
104
 
92
105
  DATA_FILE_DIR = File.dirname(__FILE__) + '/tmp'