parse-stack-next 4.5.0
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/.bundle/config +2 -0
- data/.env.sample +112 -0
- data/.env.test +10 -0
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +49 -0
- data/.ruby-version +1 -0
- data/.solargraph.yml +22 -0
- data/CHANGELOG.md +5816 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +175 -0
- data/LICENSE.txt +23 -0
- data/Makefile +63 -0
- data/README.md +5655 -0
- data/Rakefile +573 -0
- data/bin/console +38 -0
- data/bin/parse-console +136 -0
- data/bin/server +17 -0
- data/bin/setup +7 -0
- data/config/parse-config.json +12 -0
- data/docs/TEST_SERVER.md +271 -0
- data/docs/_config.yml +1 -0
- data/docs/mcp_guide.md +3484 -0
- data/docs/mongodb_direct_guide.md +1348 -0
- data/docs/mongodb_index_optimization_guide.md +631 -0
- data/examples/transaction_example.rb +219 -0
- data/lib/parse/acl_scope.rb +728 -0
- data/lib/parse/agent/cancellation_token.rb +80 -0
- data/lib/parse/agent/constraint_translator.rb +480 -0
- data/lib/parse/agent/describe.rb +420 -0
- data/lib/parse/agent/errors.rb +133 -0
- data/lib/parse/agent/mcp_client.rb +557 -0
- data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
- data/lib/parse/agent/mcp_rack_app.rb +1143 -0
- data/lib/parse/agent/mcp_server.rb +376 -0
- data/lib/parse/agent/metadata_audit.rb +259 -0
- data/lib/parse/agent/metadata_dsl.rb +733 -0
- data/lib/parse/agent/metadata_registry.rb +794 -0
- data/lib/parse/agent/pipeline_validator.rb +82 -0
- data/lib/parse/agent/prompts.rb +351 -0
- data/lib/parse/agent/rate_limiter.rb +158 -0
- data/lib/parse/agent/relation_graph.rb +162 -0
- data/lib/parse/agent/result_formatter.rb +453 -0
- data/lib/parse/agent/tools.rb +5489 -0
- data/lib/parse/agent.rb +3249 -0
- data/lib/parse/api/aggregate.rb +79 -0
- data/lib/parse/api/all.rb +26 -0
- data/lib/parse/api/analytics.rb +18 -0
- data/lib/parse/api/batch.rb +33 -0
- data/lib/parse/api/cloud_functions.rb +58 -0
- data/lib/parse/api/config.rb +125 -0
- data/lib/parse/api/files.rb +29 -0
- data/lib/parse/api/hooks.rb +117 -0
- data/lib/parse/api/objects.rb +146 -0
- data/lib/parse/api/path_segment.rb +75 -0
- data/lib/parse/api/push.rb +20 -0
- data/lib/parse/api/schema.rb +49 -0
- data/lib/parse/api/server.rb +50 -0
- data/lib/parse/api/sessions.rb +24 -0
- data/lib/parse/api/users.rb +250 -0
- data/lib/parse/atlas_search/index_manager.rb +353 -0
- data/lib/parse/atlas_search/result.rb +204 -0
- data/lib/parse/atlas_search/search_builder.rb +604 -0
- data/lib/parse/atlas_search/session.rb +253 -0
- data/lib/parse/atlas_search.rb +995 -0
- data/lib/parse/client/authentication.rb +97 -0
- data/lib/parse/client/batch.rb +234 -0
- data/lib/parse/client/body_builder.rb +240 -0
- data/lib/parse/client/caching.rb +203 -0
- data/lib/parse/client/logging.rb +293 -0
- data/lib/parse/client/profiling.rb +181 -0
- data/lib/parse/client/protocol.rb +91 -0
- data/lib/parse/client/request.rb +233 -0
- data/lib/parse/client/response.rb +208 -0
- data/lib/parse/client.rb +1104 -0
- data/lib/parse/clp_scope.rb +361 -0
- data/lib/parse/live_query/circuit_breaker.rb +256 -0
- data/lib/parse/live_query/client.rb +1001 -0
- data/lib/parse/live_query/configuration.rb +224 -0
- data/lib/parse/live_query/event.rb +115 -0
- data/lib/parse/live_query/event_queue.rb +272 -0
- data/lib/parse/live_query/health_monitor.rb +214 -0
- data/lib/parse/live_query/logging.rb +149 -0
- data/lib/parse/live_query/subscription.rb +294 -0
- data/lib/parse/live_query.rb +163 -0
- data/lib/parse/lookup_rewriter.rb +445 -0
- data/lib/parse/model/acl.rb +968 -0
- data/lib/parse/model/associations/belongs_to.rb +275 -0
- data/lib/parse/model/associations/collection_proxy.rb +435 -0
- data/lib/parse/model/associations/has_many.rb +597 -0
- data/lib/parse/model/associations/has_one.rb +158 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
- data/lib/parse/model/bytes.rb +62 -0
- data/lib/parse/model/classes/audience.rb +262 -0
- data/lib/parse/model/classes/installation.rb +363 -0
- data/lib/parse/model/classes/job_schedule.rb +153 -0
- data/lib/parse/model/classes/job_status.rb +264 -0
- data/lib/parse/model/classes/product.rb +75 -0
- data/lib/parse/model/classes/push_status.rb +263 -0
- data/lib/parse/model/classes/role.rb +751 -0
- data/lib/parse/model/classes/session.rb +201 -0
- data/lib/parse/model/classes/user.rb +943 -0
- data/lib/parse/model/clp.rb +544 -0
- data/lib/parse/model/core/actions.rb +1268 -0
- data/lib/parse/model/core/builder.rb +139 -0
- data/lib/parse/model/core/create_lock.rb +386 -0
- data/lib/parse/model/core/describe.rb +382 -0
- data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
- data/lib/parse/model/core/errors.rb +38 -0
- data/lib/parse/model/core/fetching.rb +566 -0
- data/lib/parse/model/core/field_guards.rb +220 -0
- data/lib/parse/model/core/indexing.rb +382 -0
- data/lib/parse/model/core/parse_reference.rb +407 -0
- data/lib/parse/model/core/properties.rb +809 -0
- data/lib/parse/model/core/querying.rb +491 -0
- data/lib/parse/model/core/schema.rb +202 -0
- data/lib/parse/model/core/search_indexing.rb +174 -0
- data/lib/parse/model/date.rb +88 -0
- data/lib/parse/model/email.rb +213 -0
- data/lib/parse/model/file.rb +527 -0
- data/lib/parse/model/geojson.rb +271 -0
- data/lib/parse/model/geopoint.rb +261 -0
- data/lib/parse/model/model.rb +260 -0
- data/lib/parse/model/object.rb +2068 -0
- data/lib/parse/model/phone.rb +520 -0
- data/lib/parse/model/pointer.rb +443 -0
- data/lib/parse/model/polygon.rb +406 -0
- data/lib/parse/model/push.rb +975 -0
- data/lib/parse/model/shortnames.rb +8 -0
- data/lib/parse/model/time_zone.rb +141 -0
- data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
- data/lib/parse/model/validations.rb +96 -0
- data/lib/parse/mongodb.rb +2300 -0
- data/lib/parse/pipeline_security.rb +554 -0
- data/lib/parse/query/constraint.rb +198 -0
- data/lib/parse/query/constraints.rb +3279 -0
- data/lib/parse/query/cursor.rb +434 -0
- data/lib/parse/query/n_plus_one_detector.rb +445 -0
- data/lib/parse/query/operation.rb +104 -0
- data/lib/parse/query/ordering.rb +66 -0
- data/lib/parse/query.rb +7028 -0
- data/lib/parse/schema/index_migrator.rb +291 -0
- data/lib/parse/schema/search_index_migrator.rb +289 -0
- data/lib/parse/schema.rb +494 -0
- data/lib/parse/stack/generators/rails.rb +40 -0
- data/lib/parse/stack/generators/templates/model.erb +51 -0
- data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
- data/lib/parse/stack/generators/templates/model_role.rb +4 -0
- data/lib/parse/stack/generators/templates/model_session.rb +4 -0
- data/lib/parse/stack/generators/templates/model_user.rb +11 -0
- data/lib/parse/stack/generators/templates/parse.rb +12 -0
- data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
- data/lib/parse/stack/railtie.rb +18 -0
- data/lib/parse/stack/tasks.rb +563 -0
- data/lib/parse/stack/version.rb +11 -0
- data/lib/parse/stack.rb +455 -0
- data/lib/parse/two_factor_auth/user_extension.rb +449 -0
- data/lib/parse/two_factor_auth.rb +310 -0
- data/lib/parse/webhooks/payload.rb +360 -0
- data/lib/parse/webhooks/registration.rb +199 -0
- data/lib/parse/webhooks/replay_protection.rb +189 -0
- data/lib/parse/webhooks.rb +510 -0
- data/lib/parse-stack-next.rb +5 -0
- data/lib/parse-stack.rb +5 -0
- data/parse-stack-next.gemspec +82 -0
- data/parse-stack.png +0 -0
- data/scripts/debug-ips.js +35 -0
- data/scripts/docker/Dockerfile.parse +13 -0
- data/scripts/docker/atlas-init.js +284 -0
- data/scripts/docker/docker-compose.atlas.yml +76 -0
- data/scripts/docker/docker-compose.test.yml +106 -0
- data/scripts/docker/mongo-init.js +21 -0
- data/scripts/eval_mcp_with_lm_studio.rb +274 -0
- data/scripts/start-parse.sh +90 -0
- data/scripts/start_mcp_server.rb +78 -0
- data/scripts/test_server_connection.rb +82 -0
- metadata +377 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "faraday"
|
|
5
|
+
require "moneta"
|
|
6
|
+
require "digest"
|
|
7
|
+
require_relative "protocol"
|
|
8
|
+
|
|
9
|
+
module Parse
|
|
10
|
+
module Middleware
|
|
11
|
+
# This is a caching middleware for Parse queries using Moneta. The caching
|
|
12
|
+
# middleware will cache all GET requests made to the Parse REST API as long
|
|
13
|
+
# as the API responds with a successful non-empty result payload.
|
|
14
|
+
#
|
|
15
|
+
# Whenever an object is created or updated, the corresponding entry in the cache
|
|
16
|
+
# when fetching the particular record (using the specific non-Query based API)
|
|
17
|
+
# will be cleared.
|
|
18
|
+
class Caching < Faraday::Middleware
|
|
19
|
+
include Parse::Protocol
|
|
20
|
+
|
|
21
|
+
# List of status codes that can be cached:
|
|
22
|
+
# * 200 - 'OK'
|
|
23
|
+
# * 203 - 'Non-Authoritative Information'
|
|
24
|
+
# * 300 - 'Multiple Choices'
|
|
25
|
+
# * 301 - 'Moved Permanently'
|
|
26
|
+
# * 302 - 'Found'
|
|
27
|
+
# * 404 - 'Not Found' - removed
|
|
28
|
+
# * 410 - 'Gone' - removed
|
|
29
|
+
CACHEABLE_HTTP_CODES = [200, 203, 300, 301, 302].freeze
|
|
30
|
+
# Cache control header
|
|
31
|
+
CACHE_CONTROL = "Cache-Control"
|
|
32
|
+
# Request env key for the content length
|
|
33
|
+
CONTENT_LENGTH_KEY = "content-length"
|
|
34
|
+
# Header in response that is sent if this is a cached result
|
|
35
|
+
CACHE_RESPONSE_HEADER = "X-Cache-Response"
|
|
36
|
+
# Header in request to set caching information for the middleware.
|
|
37
|
+
CACHE_EXPIRES_DURATION = "X-Parse-Stack-Cache-Expires"
|
|
38
|
+
# Header in request to enable write-only cache mode (skip read, still write)
|
|
39
|
+
CACHE_WRITE_ONLY = "X-Parse-Stack-Cache-Write-Only"
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# @!attribute enabled
|
|
43
|
+
# @return [Boolean] whether the caching middleware should be enabled.
|
|
44
|
+
attr_writer :enabled
|
|
45
|
+
|
|
46
|
+
# @!attribute logging
|
|
47
|
+
# @return [Boolean] whether the logging should be enabled.
|
|
48
|
+
attr_accessor :logging
|
|
49
|
+
|
|
50
|
+
def enabled
|
|
51
|
+
@enabled = true if @enabled.nil?
|
|
52
|
+
@enabled
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Boolean] whether caching is enabled.
|
|
56
|
+
def caching?
|
|
57
|
+
@enabled
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @!attribute [rw] store
|
|
62
|
+
# The internal moneta cache store instance.
|
|
63
|
+
# @return [Moneta::Transformer,Moneta::Expires]
|
|
64
|
+
attr_accessor :store
|
|
65
|
+
|
|
66
|
+
# @!attribute [rw] expires
|
|
67
|
+
# The expiration time in seconds for this particular request.
|
|
68
|
+
# @return [Integer]
|
|
69
|
+
attr_accessor :expires
|
|
70
|
+
|
|
71
|
+
# Creates a new caching middleware.
|
|
72
|
+
# @param adapter [Faraday::Adapter] An instance of the Faraday adapter
|
|
73
|
+
# used for the connection. Defaults Faraday::Adapter::NetHttp.
|
|
74
|
+
# @param store [Moneta] An instance of the Moneta cache store to use.
|
|
75
|
+
# @param opts [Hash] additional options.
|
|
76
|
+
# @option opts [Integer] :expires the default expiration for a cache entry.
|
|
77
|
+
# @raise ArgumentError, if `store` is not a Moneta::Transformer or Moneta::Expires instance.
|
|
78
|
+
def initialize(adapter, store, opts = {})
|
|
79
|
+
super(adapter)
|
|
80
|
+
@store = store
|
|
81
|
+
@opts = { expires: 0 }
|
|
82
|
+
@opts.merge!(opts) if opts.is_a?(Hash)
|
|
83
|
+
@expires = @opts[:expires]
|
|
84
|
+
|
|
85
|
+
unless [:key?, :[], :delete, :store].all? { |method| @store.respond_to?(method) }
|
|
86
|
+
raise ArgumentError, "Caching store object must a Moneta key/value store."
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Thread-safety
|
|
91
|
+
# @!visibility private
|
|
92
|
+
def call(env)
|
|
93
|
+
dup.call!(env)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @!visibility private
|
|
97
|
+
def call!(env)
|
|
98
|
+
@request_headers = env[:request_headers]
|
|
99
|
+
|
|
100
|
+
# get default caching state
|
|
101
|
+
@enabled = self.class.enabled
|
|
102
|
+
# disable cache for this request if "no-cache" was passed
|
|
103
|
+
if @request_headers[CACHE_CONTROL] == "no-cache"
|
|
104
|
+
@enabled = false
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check for write-only mode (skip cache read, still write to cache)
|
|
108
|
+
# This is useful for fetch!/reload! which want fresh data but should update cache
|
|
109
|
+
@write_only = @request_headers[CACHE_WRITE_ONLY] == "true"
|
|
110
|
+
|
|
111
|
+
# get the expires information from header (per-request) or instance default
|
|
112
|
+
if @request_headers[CACHE_EXPIRES_DURATION].to_i > 0
|
|
113
|
+
@expires = @request_headers[CACHE_EXPIRES_DURATION].to_i
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# cleanup
|
|
117
|
+
@request_headers.delete(CACHE_CONTROL)
|
|
118
|
+
@request_headers.delete(CACHE_EXPIRES_DURATION)
|
|
119
|
+
@request_headers.delete(CACHE_WRITE_ONLY)
|
|
120
|
+
|
|
121
|
+
# if caching is enabled and we have a valid cache duration, use cache
|
|
122
|
+
# otherwise work as a passthrough.
|
|
123
|
+
return @app.call(env) unless @enabled && @store.present? && @expires > 0
|
|
124
|
+
|
|
125
|
+
url = env.url
|
|
126
|
+
method = env.method
|
|
127
|
+
@cache_key = url.to_s
|
|
128
|
+
|
|
129
|
+
if @request_headers.key?(SESSION_TOKEN)
|
|
130
|
+
@session_token = @request_headers[SESSION_TOKEN]
|
|
131
|
+
hashed_token = Digest::SHA256.hexdigest(@session_token.to_s)[0, 32]
|
|
132
|
+
@cache_key = "#{hashed_token}:#{@cache_key}" # prefix with hashed token
|
|
133
|
+
elsif @request_headers.key?(MASTER_KEY)
|
|
134
|
+
@cache_key = "mk:#{@cache_key}" # prefix for master key requests
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
# Skip cache read if write_only mode is enabled
|
|
139
|
+
if method == :get && @cache_key.present? && !@write_only && @store.key?(@cache_key)
|
|
140
|
+
puts("[Parse::Cache] Hit >> #{url}") if self.class.logging.present?
|
|
141
|
+
response = Faraday::Response.new
|
|
142
|
+
begin
|
|
143
|
+
cache_data = @store[@cache_key] # previous cached response
|
|
144
|
+
rescue => e
|
|
145
|
+
puts "[Parse::Cache] Error: #{e}"
|
|
146
|
+
cache_data = nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# check if the store was from a legacy parse-stack cache value which
|
|
150
|
+
# is stored as Faraday::Env. T\he new system stores less content in a simple hash
|
|
151
|
+
# for improved interoperability and access time.
|
|
152
|
+
if cache_data.is_a?(Faraday::Env)
|
|
153
|
+
body = cache_data.respond_to?(:body) ? cache_data.body : nil
|
|
154
|
+
response_headers = cache_data.response_headers || {}
|
|
155
|
+
elsif cache_data.is_a?(Hash)
|
|
156
|
+
body = cache_data[:body]
|
|
157
|
+
response_headers = cache_data[:headers] || {}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if cache_data.present? && body.present?
|
|
161
|
+
response_headers[CACHE_RESPONSE_HEADER] = "true"
|
|
162
|
+
response.finish({ status: 200, response_headers: response_headers, body: body })
|
|
163
|
+
return response
|
|
164
|
+
else
|
|
165
|
+
@store.delete @cache_key
|
|
166
|
+
end
|
|
167
|
+
elsif @cache_key.present?
|
|
168
|
+
#non GET requets should clear the cache for that same resource path.
|
|
169
|
+
#ex. a POST to /1/classes/Artist/<objectId> should delete the cache for a GET
|
|
170
|
+
# request for the same '/1/classes/Artist/<objectId>' where objectId are equivalent
|
|
171
|
+
@store.delete url.to_s # regular
|
|
172
|
+
@store.delete "mk:#{url.to_s}" # master key cache-key
|
|
173
|
+
@store.delete @cache_key # final key
|
|
174
|
+
end
|
|
175
|
+
rescue ::TypeError, Errno::EINVAL, Redis::CannotConnectError, Redis::TimeoutError => e
|
|
176
|
+
# if the cache store fails to connect, catch the exception but proceed
|
|
177
|
+
# with the regular request, but turn off caching for this request. It is possible
|
|
178
|
+
# that the cache connection resumes at a later point, so this is temporary.
|
|
179
|
+
@enabled = false
|
|
180
|
+
puts "[Parse::Cache] Error: #{e}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
@app.call(env).on_complete do |response_env|
|
|
184
|
+
# Only cache GET requests with valid HTTP status codes whose content-length
|
|
185
|
+
# is between 20 bytes and 1MB. Otherwise they could be errors, successes and empty result sets.
|
|
186
|
+
|
|
187
|
+
if @enabled && method == :get && CACHEABLE_HTTP_CODES.include?(response_env.status) &&
|
|
188
|
+
response_env.body.present? && response_env.response_headers[CONTENT_LENGTH_KEY].to_i.between?(20, 1_250_000)
|
|
189
|
+
begin
|
|
190
|
+
@store.store(@cache_key,
|
|
191
|
+
{ headers: response_env.response_headers, body: response_env.body },
|
|
192
|
+
expires: @expires)
|
|
193
|
+
rescue => e
|
|
194
|
+
puts "[Parse::Cache] Store Error: #{e}"
|
|
195
|
+
end
|
|
196
|
+
end # if
|
|
197
|
+
# do something with the response
|
|
198
|
+
# response_env[:response_headers].merge!(...)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end #Caching
|
|
202
|
+
end #Middleware
|
|
203
|
+
end
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "faraday"
|
|
5
|
+
require "logger"
|
|
6
|
+
|
|
7
|
+
module Parse
|
|
8
|
+
module Middleware
|
|
9
|
+
# Faraday middleware that logs Parse API requests and responses.
|
|
10
|
+
#
|
|
11
|
+
# This middleware provides detailed logging of HTTP requests and responses
|
|
12
|
+
# with configurable log levels and optional body truncation for large payloads.
|
|
13
|
+
#
|
|
14
|
+
# @example Basic setup
|
|
15
|
+
# Parse.logging = true
|
|
16
|
+
#
|
|
17
|
+
# @example Detailed configuration
|
|
18
|
+
# Parse.configure do |config|
|
|
19
|
+
# config.logging = true
|
|
20
|
+
# config.log_level = :debug
|
|
21
|
+
# config.logger = Rails.logger # or Logger.new(STDOUT)
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# Log levels:
|
|
25
|
+
# - :info - Logs request method, URL, status, and timing
|
|
26
|
+
# - :debug - Also logs headers and truncated body content
|
|
27
|
+
# - :warn - Only logs errors and warnings
|
|
28
|
+
#
|
|
29
|
+
class Logging < Faraday::Middleware
|
|
30
|
+
# Maximum length of body content to log before truncation
|
|
31
|
+
MAX_BODY_LENGTH = 500
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# @return [Boolean] Whether logging is enabled
|
|
35
|
+
attr_accessor :enabled
|
|
36
|
+
|
|
37
|
+
# @return [Symbol] The log level (:info, :debug, :warn)
|
|
38
|
+
attr_accessor :log_level
|
|
39
|
+
|
|
40
|
+
# @return [Logger] The logger instance to use
|
|
41
|
+
attr_accessor :logger
|
|
42
|
+
|
|
43
|
+
# @return [Integer] Maximum body length to log (defaults to MAX_BODY_LENGTH)
|
|
44
|
+
attr_accessor :max_body_length
|
|
45
|
+
|
|
46
|
+
# Default logger instance
|
|
47
|
+
# @return [Logger]
|
|
48
|
+
def default_logger
|
|
49
|
+
@default_logger ||= begin
|
|
50
|
+
l = Logger.new(STDOUT)
|
|
51
|
+
l.progname = "Parse"
|
|
52
|
+
l.formatter = proc do |severity, datetime, progname, msg|
|
|
53
|
+
"[#{progname}] #{msg}\n"
|
|
54
|
+
end
|
|
55
|
+
l
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Get the configured logger or default
|
|
60
|
+
# @return [Logger]
|
|
61
|
+
def current_logger
|
|
62
|
+
logger || default_logger
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get the current log level (defaults to :info)
|
|
66
|
+
# @return [Symbol]
|
|
67
|
+
def current_log_level
|
|
68
|
+
log_level || :info
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get the max body length (defaults to MAX_BODY_LENGTH)
|
|
72
|
+
# @return [Integer]
|
|
73
|
+
def current_max_body_length
|
|
74
|
+
max_body_length || MAX_BODY_LENGTH
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Thread-safety: duplicate the middleware for each request
|
|
79
|
+
# @!visibility private
|
|
80
|
+
def call(env)
|
|
81
|
+
dup.call!(env)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @!visibility private
|
|
85
|
+
def call!(env)
|
|
86
|
+
return @app.call(env) unless self.class.enabled
|
|
87
|
+
|
|
88
|
+
start_time = Time.now
|
|
89
|
+
log_request(env)
|
|
90
|
+
|
|
91
|
+
@app.call(env).on_complete do |response_env|
|
|
92
|
+
elapsed_ms = ((Time.now - start_time) * 1000).round(2)
|
|
93
|
+
log_response(response_env, elapsed_ms)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def log_request(env)
|
|
100
|
+
logger = self.class.current_logger
|
|
101
|
+
level = self.class.current_log_level
|
|
102
|
+
|
|
103
|
+
method = env[:method].to_s.upcase
|
|
104
|
+
url = sanitize_url(env[:url].to_s)
|
|
105
|
+
|
|
106
|
+
case level
|
|
107
|
+
when :debug
|
|
108
|
+
logger.debug "▶ #{method} #{url}"
|
|
109
|
+
log_headers(env[:request_headers], "Request")
|
|
110
|
+
log_body(env[:body], "Request")
|
|
111
|
+
when :info
|
|
112
|
+
logger.info "▶ #{method} #{url}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def log_response(response_env, elapsed_ms)
|
|
117
|
+
logger = self.class.current_logger
|
|
118
|
+
level = self.class.current_log_level
|
|
119
|
+
status = response_env[:status]
|
|
120
|
+
|
|
121
|
+
# Determine if this is an error response
|
|
122
|
+
is_error = status >= 400
|
|
123
|
+
|
|
124
|
+
case level
|
|
125
|
+
when :debug
|
|
126
|
+
log_debug_response(logger, response_env, elapsed_ms, is_error)
|
|
127
|
+
when :info
|
|
128
|
+
log_info_response(logger, response_env, elapsed_ms, is_error)
|
|
129
|
+
when :warn
|
|
130
|
+
log_warn_response(logger, response_env, elapsed_ms) if is_error
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def log_debug_response(logger, response_env, elapsed_ms, is_error)
|
|
135
|
+
status = response_env[:status]
|
|
136
|
+
status_indicator = is_error ? "✗" : "◀"
|
|
137
|
+
|
|
138
|
+
logger.debug "#{status_indicator} #{status} (#{elapsed_ms}ms)"
|
|
139
|
+
log_body(response_body_content(response_env), "Response")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def log_info_response(logger, response_env, elapsed_ms, is_error)
|
|
143
|
+
status = response_env[:status]
|
|
144
|
+
status_indicator = is_error ? "✗" : "◀"
|
|
145
|
+
|
|
146
|
+
if is_error
|
|
147
|
+
logger.info "#{status_indicator} #{status} (#{elapsed_ms}ms) - #{error_summary(response_env)}"
|
|
148
|
+
else
|
|
149
|
+
logger.info "#{status_indicator} #{status} (#{elapsed_ms}ms)"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def log_warn_response(logger, response_env, elapsed_ms)
|
|
154
|
+
status = response_env[:status]
|
|
155
|
+
logger.warn "✗ #{status} (#{elapsed_ms}ms) - #{error_summary(response_env)}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def log_headers(headers, prefix)
|
|
159
|
+
return unless headers
|
|
160
|
+
logger = self.class.current_logger
|
|
161
|
+
headers.each do |key, value|
|
|
162
|
+
# Don't log sensitive headers. Reuses the canonical denylist on
|
|
163
|
+
# Parse::Middleware::BodyBuilder so Authorization, Cookie, and
|
|
164
|
+
# X-Parse-JavaScript-Key are also redacted (the prior regex only
|
|
165
|
+
# caught master-key / api-key / session-token shaped names).
|
|
166
|
+
if Parse::Middleware::BodyBuilder::REDACTED_HEADERS.include?(key.to_s.downcase)
|
|
167
|
+
logger.debug " [#{prefix} Header] #{key}: [FILTERED]"
|
|
168
|
+
else
|
|
169
|
+
logger.debug " [#{prefix} Header] #{key}: #{value}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def log_body(body, prefix)
|
|
175
|
+
return unless body
|
|
176
|
+
logger = self.class.current_logger
|
|
177
|
+
max_length = self.class.current_max_body_length
|
|
178
|
+
|
|
179
|
+
content = if body.is_a?(String)
|
|
180
|
+
body
|
|
181
|
+
else
|
|
182
|
+
begin
|
|
183
|
+
body.to_json
|
|
184
|
+
rescue JSON::GeneratorError, Encoding::UndefinedConversionError
|
|
185
|
+
body.to_s
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
if content.length > max_length
|
|
190
|
+
logger.debug " [#{prefix} Body] #{content[0...max_length]}... (truncated, #{content.length} total)"
|
|
191
|
+
elsif content.length > 0
|
|
192
|
+
logger.debug " [#{prefix} Body] #{content}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def response_body_content(response_env)
|
|
197
|
+
body = response_env[:body]
|
|
198
|
+
if body.is_a?(Parse::Response)
|
|
199
|
+
begin
|
|
200
|
+
body.result.to_json
|
|
201
|
+
rescue JSON::GeneratorError, Encoding::UndefinedConversionError
|
|
202
|
+
body.to_s
|
|
203
|
+
end
|
|
204
|
+
else
|
|
205
|
+
body
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def error_summary(response_env)
|
|
210
|
+
body = response_env[:body]
|
|
211
|
+
if body.is_a?(Parse::Response) && body.error?
|
|
212
|
+
"#{body.code}: #{body.error}"
|
|
213
|
+
elsif body.is_a?(Hash)
|
|
214
|
+
body["error"] || body[:error] || "Unknown error"
|
|
215
|
+
else
|
|
216
|
+
"HTTP #{response_env[:status]}"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def sanitize_url(url)
|
|
221
|
+
# Remove sensitive query parameters from logged URLs
|
|
222
|
+
url.gsub(/([?&])(sessionToken|masterKey|apiKey)=[^&]*/, '\1\2=[FILTERED]')
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Module-level configuration methods for logging
|
|
228
|
+
class << self
|
|
229
|
+
# Enable or disable request/response logging
|
|
230
|
+
# @example Enable logging
|
|
231
|
+
# Parse.logging_enabled = true
|
|
232
|
+
# @param value [Boolean]
|
|
233
|
+
def logging_enabled=(value)
|
|
234
|
+
Middleware::Logging.enabled = value
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# @return [Boolean] whether logging is enabled
|
|
238
|
+
def logging_enabled
|
|
239
|
+
Middleware::Logging.enabled
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Set the log level for Parse requests
|
|
243
|
+
# @example Set debug level
|
|
244
|
+
# Parse.log_level = :debug
|
|
245
|
+
# @param value [Symbol] one of :info, :debug, :warn
|
|
246
|
+
def log_level=(value)
|
|
247
|
+
unless [:info, :debug, :warn].include?(value)
|
|
248
|
+
raise ArgumentError, "Invalid log level: #{value}. Use :info, :debug, or :warn"
|
|
249
|
+
end
|
|
250
|
+
Middleware::Logging.log_level = value
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# @return [Symbol] the current log level
|
|
254
|
+
def log_level
|
|
255
|
+
Middleware::Logging.current_log_level
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Set a custom logger for Parse requests
|
|
259
|
+
# @example Use Rails logger
|
|
260
|
+
# Parse.logger = Rails.logger
|
|
261
|
+
# @param value [Logger]
|
|
262
|
+
def logger=(value)
|
|
263
|
+
Middleware::Logging.logger = value
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# @return [Logger] the current logger
|
|
267
|
+
def logger
|
|
268
|
+
Middleware::Logging.current_logger
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Set the maximum body length to log before truncation
|
|
272
|
+
# @param value [Integer]
|
|
273
|
+
def log_max_body_length=(value)
|
|
274
|
+
Middleware::Logging.max_body_length = value.to_i
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# @return [Integer] the maximum body length
|
|
278
|
+
def log_max_body_length
|
|
279
|
+
Middleware::Logging.current_max_body_length
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Configure Parse logging with a block
|
|
283
|
+
# @example
|
|
284
|
+
# Parse.configure_logging do |config|
|
|
285
|
+
# config.enabled = true
|
|
286
|
+
# config.log_level = :debug
|
|
287
|
+
# config.logger = Rails.logger
|
|
288
|
+
# end
|
|
289
|
+
def configure_logging
|
|
290
|
+
yield Middleware::Logging if block_given?
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "faraday"
|
|
5
|
+
|
|
6
|
+
module Parse
|
|
7
|
+
module Middleware
|
|
8
|
+
# Faraday middleware that profiles Parse API requests.
|
|
9
|
+
#
|
|
10
|
+
# This middleware provides detailed timing information for HTTP requests
|
|
11
|
+
# including network time and overall request duration.
|
|
12
|
+
#
|
|
13
|
+
# @example Enable profiling
|
|
14
|
+
# Parse.profiling_enabled = true
|
|
15
|
+
#
|
|
16
|
+
# @example Access profile data in callbacks
|
|
17
|
+
# Parse.on_request_complete do |profile|
|
|
18
|
+
# puts "Request to #{profile[:url]} took #{profile[:duration_ms]}ms"
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# @example Get recent profiles
|
|
22
|
+
# Parse.recent_profiles.each do |profile|
|
|
23
|
+
# puts "#{profile[:method]} #{profile[:url]}: #{profile[:duration_ms]}ms"
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
class Profiling < Faraday::Middleware
|
|
27
|
+
# Maximum number of profiles to keep in memory
|
|
28
|
+
MAX_PROFILES = 100
|
|
29
|
+
|
|
30
|
+
class << self
|
|
31
|
+
# @return [Boolean] Whether profiling is enabled
|
|
32
|
+
attr_accessor :enabled
|
|
33
|
+
|
|
34
|
+
# @return [Array<Hash>] Recent profile data
|
|
35
|
+
def profiles
|
|
36
|
+
@profiles ||= []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Clear all stored profiles
|
|
40
|
+
def clear_profiles!
|
|
41
|
+
@profiles = []
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [Array<Proc>] Callbacks to execute on request completion
|
|
45
|
+
def callbacks
|
|
46
|
+
@callbacks ||= []
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Register a callback to be executed when a request completes
|
|
50
|
+
# @yield [Hash] the profile data for the completed request
|
|
51
|
+
def on_request_complete(&block)
|
|
52
|
+
callbacks << block if block_given?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Clear all registered callbacks
|
|
56
|
+
def clear_callbacks!
|
|
57
|
+
@callbacks = []
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Add a profile entry
|
|
61
|
+
# @param profile [Hash] the profile data
|
|
62
|
+
def add_profile(profile)
|
|
63
|
+
profiles << profile
|
|
64
|
+
# Keep only the most recent profiles
|
|
65
|
+
profiles.shift while profiles.size > MAX_PROFILES
|
|
66
|
+
|
|
67
|
+
# Execute callbacks
|
|
68
|
+
callbacks.each { |cb| cb.call(profile) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get aggregate statistics for recent profiles
|
|
72
|
+
# @return [Hash] statistics including count, avg, min, max durations
|
|
73
|
+
def statistics
|
|
74
|
+
return {} if profiles.empty?
|
|
75
|
+
|
|
76
|
+
durations = profiles.map { |p| p[:duration_ms] }
|
|
77
|
+
{
|
|
78
|
+
count: profiles.size,
|
|
79
|
+
total_ms: durations.sum,
|
|
80
|
+
avg_ms: (durations.sum.to_f / durations.size).round(2),
|
|
81
|
+
min_ms: durations.min,
|
|
82
|
+
max_ms: durations.max,
|
|
83
|
+
by_method: profiles.group_by { |p| p[:method] }.transform_values(&:size),
|
|
84
|
+
by_status: profiles.group_by { |p| p[:status] }.transform_values(&:size),
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Thread-safety: duplicate the middleware for each request
|
|
90
|
+
# @!visibility private
|
|
91
|
+
def call(env)
|
|
92
|
+
dup.call!(env)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @!visibility private
|
|
96
|
+
def call!(env)
|
|
97
|
+
return @app.call(env) unless self.class.enabled
|
|
98
|
+
|
|
99
|
+
start_time = Time.now
|
|
100
|
+
|
|
101
|
+
@app.call(env).on_complete do |response_env|
|
|
102
|
+
end_time = Time.now
|
|
103
|
+
duration_ms = ((end_time - start_time) * 1000).round(2)
|
|
104
|
+
|
|
105
|
+
profile = {
|
|
106
|
+
method: env[:method].to_s.upcase,
|
|
107
|
+
url: sanitize_url(env[:url].to_s),
|
|
108
|
+
status: response_env[:status],
|
|
109
|
+
duration_ms: duration_ms,
|
|
110
|
+
started_at: start_time.iso8601(3),
|
|
111
|
+
completed_at: end_time.iso8601(3),
|
|
112
|
+
request_size: env[:body].to_s.bytesize,
|
|
113
|
+
response_size: response_body_size(response_env),
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
self.class.add_profile(profile)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def sanitize_url(url)
|
|
123
|
+
# Remove sensitive query parameters
|
|
124
|
+
url.gsub(/([?&])(sessionToken|masterKey|apiKey)=[^&]*/, '\1\2=[FILTERED]')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def response_body_size(response_env)
|
|
128
|
+
body = response_env[:body]
|
|
129
|
+
if body.is_a?(Parse::Response)
|
|
130
|
+
body.result.to_json.bytesize rescue 0
|
|
131
|
+
elsif body.is_a?(String)
|
|
132
|
+
body.bytesize
|
|
133
|
+
else
|
|
134
|
+
body.to_s.bytesize
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Module-level profiling configuration
|
|
141
|
+
class << self
|
|
142
|
+
# Enable or disable request profiling
|
|
143
|
+
# @param value [Boolean]
|
|
144
|
+
def profiling_enabled=(value)
|
|
145
|
+
Middleware::Profiling.enabled = value
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @return [Boolean] whether profiling is enabled
|
|
149
|
+
def profiling_enabled
|
|
150
|
+
Middleware::Profiling.enabled
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Get recent profile data
|
|
154
|
+
# @return [Array<Hash>]
|
|
155
|
+
def recent_profiles
|
|
156
|
+
Middleware::Profiling.profiles
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Clear all stored profiles
|
|
160
|
+
def clear_profiles!
|
|
161
|
+
Middleware::Profiling.clear_profiles!
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Get profiling statistics
|
|
165
|
+
# @return [Hash]
|
|
166
|
+
def profiling_statistics
|
|
167
|
+
Middleware::Profiling.statistics
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Register a callback for request completion
|
|
171
|
+
# @yield [Hash] profile data
|
|
172
|
+
def on_request_complete(&block)
|
|
173
|
+
Middleware::Profiling.on_request_complete(&block)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Clear all profiling callbacks
|
|
177
|
+
def clear_profiling_callbacks!
|
|
178
|
+
Middleware::Profiling.clear_callbacks!
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|