appsignal 2.5.0.alpha.1-java
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 +33 -0
- data/.rspec +4 -0
- data/.rubocop.yml +66 -0
- data/.rubocop_todo.yml +124 -0
- data/.travis.yml +72 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +639 -0
- data/Gemfile +3 -0
- data/LICENSE +20 -0
- data/README.md +264 -0
- data/Rakefile +214 -0
- data/appsignal.gemspec +42 -0
- data/benchmark.rake +77 -0
- data/bin/appsignal +13 -0
- data/ext/Rakefile +27 -0
- data/ext/agent.yml +64 -0
- data/ext/appsignal_extension.c +692 -0
- data/ext/base.rb +79 -0
- data/ext/extconf.rb +35 -0
- data/gemfiles/capistrano2.gemfile +7 -0
- data/gemfiles/capistrano3.gemfile +7 -0
- data/gemfiles/grape.gemfile +7 -0
- data/gemfiles/no_dependencies.gemfile +5 -0
- data/gemfiles/padrino.gemfile +7 -0
- data/gemfiles/que.gemfile +5 -0
- data/gemfiles/rails-3.2.gemfile +6 -0
- data/gemfiles/rails-4.0.gemfile +6 -0
- data/gemfiles/rails-4.1.gemfile +6 -0
- data/gemfiles/rails-4.2.gemfile +10 -0
- data/gemfiles/rails-5.0.gemfile +5 -0
- data/gemfiles/rails-5.1.gemfile +5 -0
- data/gemfiles/resque.gemfile +12 -0
- data/gemfiles/sequel-435.gemfile +11 -0
- data/gemfiles/sequel.gemfile +11 -0
- data/gemfiles/sinatra.gemfile +6 -0
- data/gemfiles/webmachine.gemfile +5 -0
- data/lib/appsignal.rb +804 -0
- data/lib/appsignal/auth_check.rb +65 -0
- data/lib/appsignal/capistrano.rb +10 -0
- data/lib/appsignal/cli.rb +108 -0
- data/lib/appsignal/cli/demo.rb +63 -0
- data/lib/appsignal/cli/diagnose.rb +500 -0
- data/lib/appsignal/cli/helpers.rb +72 -0
- data/lib/appsignal/cli/install.rb +277 -0
- data/lib/appsignal/cli/notify_of_deploy.rb +113 -0
- data/lib/appsignal/config.rb +287 -0
- data/lib/appsignal/demo.rb +107 -0
- data/lib/appsignal/event_formatter.rb +74 -0
- data/lib/appsignal/event_formatter/action_view/render_formatter.rb +24 -0
- data/lib/appsignal/event_formatter/active_record/instantiation_formatter.rb +14 -0
- data/lib/appsignal/event_formatter/active_record/sql_formatter.rb +14 -0
- data/lib/appsignal/event_formatter/elastic_search/search_formatter.rb +32 -0
- data/lib/appsignal/event_formatter/faraday/request_formatter.rb +19 -0
- data/lib/appsignal/event_formatter/mongo_ruby_driver/query_formatter.rb +89 -0
- data/lib/appsignal/event_formatter/moped/query_formatter.rb +80 -0
- data/lib/appsignal/extension.rb +63 -0
- data/lib/appsignal/extension/jruby.rb +460 -0
- data/lib/appsignal/garbage_collection_profiler.rb +48 -0
- data/lib/appsignal/hooks.rb +105 -0
- data/lib/appsignal/hooks/action_cable.rb +113 -0
- data/lib/appsignal/hooks/active_support_notifications.rb +52 -0
- data/lib/appsignal/hooks/celluloid.rb +30 -0
- data/lib/appsignal/hooks/data_mapper.rb +18 -0
- data/lib/appsignal/hooks/delayed_job.rb +19 -0
- data/lib/appsignal/hooks/mongo_ruby_driver.rb +21 -0
- data/lib/appsignal/hooks/net_http.rb +29 -0
- data/lib/appsignal/hooks/passenger.rb +22 -0
- data/lib/appsignal/hooks/puma.rb +35 -0
- data/lib/appsignal/hooks/que.rb +21 -0
- data/lib/appsignal/hooks/rake.rb +39 -0
- data/lib/appsignal/hooks/redis.rb +30 -0
- data/lib/appsignal/hooks/sequel.rb +60 -0
- data/lib/appsignal/hooks/shoryuken.rb +43 -0
- data/lib/appsignal/hooks/sidekiq.rb +144 -0
- data/lib/appsignal/hooks/unicorn.rb +40 -0
- data/lib/appsignal/hooks/webmachine.rb +23 -0
- data/lib/appsignal/integrations/capistrano/appsignal.cap +39 -0
- data/lib/appsignal/integrations/capistrano/capistrano_2_tasks.rb +52 -0
- data/lib/appsignal/integrations/data_mapper.rb +33 -0
- data/lib/appsignal/integrations/delayed_job_plugin.rb +54 -0
- data/lib/appsignal/integrations/grape.rb +53 -0
- data/lib/appsignal/integrations/mongo_ruby_driver.rb +55 -0
- data/lib/appsignal/integrations/object.rb +35 -0
- data/lib/appsignal/integrations/padrino.rb +84 -0
- data/lib/appsignal/integrations/que.rb +43 -0
- data/lib/appsignal/integrations/railtie.rb +41 -0
- data/lib/appsignal/integrations/rake.rb +2 -0
- data/lib/appsignal/integrations/resque.rb +20 -0
- data/lib/appsignal/integrations/resque_active_job.rb +30 -0
- data/lib/appsignal/integrations/sinatra.rb +17 -0
- data/lib/appsignal/integrations/webmachine.rb +38 -0
- data/lib/appsignal/js_exception_transaction.rb +54 -0
- data/lib/appsignal/marker.rb +63 -0
- data/lib/appsignal/minutely.rb +42 -0
- data/lib/appsignal/rack/generic_instrumentation.rb +49 -0
- data/lib/appsignal/rack/js_exception_catcher.rb +70 -0
- data/lib/appsignal/rack/rails_instrumentation.rb +51 -0
- data/lib/appsignal/rack/sinatra_instrumentation.rb +99 -0
- data/lib/appsignal/rack/streaming_listener.rb +73 -0
- data/lib/appsignal/system.rb +81 -0
- data/lib/appsignal/transaction.rb +498 -0
- data/lib/appsignal/transmitter.rb +107 -0
- data/lib/appsignal/utils.rb +127 -0
- data/lib/appsignal/utils/params_sanitizer.rb +59 -0
- data/lib/appsignal/utils/query_params_sanitizer.rb +55 -0
- data/lib/appsignal/version.rb +3 -0
- data/lib/sequel/extensions/appsignal_integration.rb +3 -0
- data/resources/appsignal.yml.erb +39 -0
- data/resources/cacert.pem +3866 -0
- data/spec/.rubocop.yml +7 -0
- data/spec/lib/appsignal/auth_check_spec.rb +80 -0
- data/spec/lib/appsignal/capistrano2_spec.rb +224 -0
- data/spec/lib/appsignal/capistrano3_spec.rb +237 -0
- data/spec/lib/appsignal/cli/demo_spec.rb +67 -0
- data/spec/lib/appsignal/cli/diagnose_spec.rb +988 -0
- data/spec/lib/appsignal/cli/helpers_spec.rb +171 -0
- data/spec/lib/appsignal/cli/install_spec.rb +632 -0
- data/spec/lib/appsignal/cli/notify_of_deploy_spec.rb +168 -0
- data/spec/lib/appsignal/cli_spec.rb +56 -0
- data/spec/lib/appsignal/config_spec.rb +637 -0
- data/spec/lib/appsignal/demo_spec.rb +87 -0
- data/spec/lib/appsignal/event_formatter/action_view/render_formatter_spec.rb +44 -0
- data/spec/lib/appsignal/event_formatter/active_record/instantiation_formatter_spec.rb +21 -0
- data/spec/lib/appsignal/event_formatter/active_record/sql_formatter_spec.rb +21 -0
- data/spec/lib/appsignal/event_formatter/elastic_search/search_formatter_spec.rb +52 -0
- data/spec/lib/appsignal/event_formatter/faraday/request_formatter_spec.rb +21 -0
- data/spec/lib/appsignal/event_formatter/mongo_ruby_driver/query_formatter_spec.rb +113 -0
- data/spec/lib/appsignal/event_formatter/moped/query_formatter_spec.rb +112 -0
- data/spec/lib/appsignal/event_formatter_spec.rb +100 -0
- data/spec/lib/appsignal/extension/jruby_spec.rb +43 -0
- data/spec/lib/appsignal/extension_spec.rb +137 -0
- data/spec/lib/appsignal/garbage_collection_profiler_spec.rb +66 -0
- data/spec/lib/appsignal/hooks/action_cable_spec.rb +370 -0
- data/spec/lib/appsignal/hooks/active_support_notifications_spec.rb +92 -0
- data/spec/lib/appsignal/hooks/celluloid_spec.rb +35 -0
- data/spec/lib/appsignal/hooks/data_mapper_spec.rb +39 -0
- data/spec/lib/appsignal/hooks/delayed_job_spec.rb +358 -0
- data/spec/lib/appsignal/hooks/mongo_ruby_driver_spec.rb +44 -0
- data/spec/lib/appsignal/hooks/net_http_spec.rb +53 -0
- data/spec/lib/appsignal/hooks/passenger_spec.rb +30 -0
- data/spec/lib/appsignal/hooks/puma_spec.rb +80 -0
- data/spec/lib/appsignal/hooks/que_spec.rb +19 -0
- data/spec/lib/appsignal/hooks/rake_spec.rb +73 -0
- data/spec/lib/appsignal/hooks/redis_spec.rb +55 -0
- data/spec/lib/appsignal/hooks/sequel_spec.rb +46 -0
- data/spec/lib/appsignal/hooks/shoryuken_spec.rb +192 -0
- data/spec/lib/appsignal/hooks/sidekiq_spec.rb +419 -0
- data/spec/lib/appsignal/hooks/unicorn_spec.rb +52 -0
- data/spec/lib/appsignal/hooks/webmachine_spec.rb +35 -0
- data/spec/lib/appsignal/hooks_spec.rb +195 -0
- data/spec/lib/appsignal/integrations/data_mapper_spec.rb +65 -0
- data/spec/lib/appsignal/integrations/grape_spec.rb +225 -0
- data/spec/lib/appsignal/integrations/mongo_ruby_driver_spec.rb +127 -0
- data/spec/lib/appsignal/integrations/object_spec.rb +249 -0
- data/spec/lib/appsignal/integrations/padrino_spec.rb +323 -0
- data/spec/lib/appsignal/integrations/que_spec.rb +174 -0
- data/spec/lib/appsignal/integrations/railtie_spec.rb +129 -0
- data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +83 -0
- data/spec/lib/appsignal/integrations/resque_spec.rb +92 -0
- data/spec/lib/appsignal/integrations/sinatra_spec.rb +73 -0
- data/spec/lib/appsignal/integrations/webmachine_spec.rb +69 -0
- data/spec/lib/appsignal/js_exception_transaction_spec.rb +128 -0
- data/spec/lib/appsignal/marker_spec.rb +51 -0
- data/spec/lib/appsignal/minutely_spec.rb +50 -0
- data/spec/lib/appsignal/rack/generic_instrumentation_spec.rb +90 -0
- data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +147 -0
- data/spec/lib/appsignal/rack/rails_instrumentation_spec.rb +117 -0
- data/spec/lib/appsignal/rack/sinatra_instrumentation_spec.rb +213 -0
- data/spec/lib/appsignal/rack/streaming_listener_spec.rb +161 -0
- data/spec/lib/appsignal/system_spec.rb +131 -0
- data/spec/lib/appsignal/transaction_spec.rb +1146 -0
- data/spec/lib/appsignal/transmitter_spec.rb +152 -0
- data/spec/lib/appsignal/utils/params_sanitizer_spec.rb +136 -0
- data/spec/lib/appsignal/utils/query_params_sanitizer_spec.rb +192 -0
- data/spec/lib/appsignal/utils_spec.rb +150 -0
- data/spec/lib/appsignal_spec.rb +1049 -0
- data/spec/spec_helper.rb +116 -0
- data/spec/support/fixtures/containers/cgroups/docker +14 -0
- data/spec/support/fixtures/containers/cgroups/docker_systemd +8 -0
- data/spec/support/fixtures/containers/cgroups/lxc +10 -0
- data/spec/support/fixtures/containers/cgroups/no_permission +0 -0
- data/spec/support/fixtures/containers/cgroups/none +1 -0
- data/spec/support/fixtures/generated_config.yml +24 -0
- data/spec/support/fixtures/uploaded_file.txt +0 -0
- data/spec/support/helpers/api_request_helper.rb +19 -0
- data/spec/support/helpers/cli_helpers.rb +26 -0
- data/spec/support/helpers/config_helpers.rb +21 -0
- data/spec/support/helpers/dependency_helper.rb +73 -0
- data/spec/support/helpers/directory_helper.rb +27 -0
- data/spec/support/helpers/env_helpers.rb +33 -0
- data/spec/support/helpers/example_exception.rb +13 -0
- data/spec/support/helpers/example_standard_error.rb +13 -0
- data/spec/support/helpers/log_helpers.rb +22 -0
- data/spec/support/helpers/std_streams_helper.rb +66 -0
- data/spec/support/helpers/system_helpers.rb +8 -0
- data/spec/support/helpers/time_helpers.rb +11 -0
- data/spec/support/helpers/transaction_helpers.rb +37 -0
- data/spec/support/matchers/contains_log.rb +7 -0
- data/spec/support/mocks/fake_gc_profiler.rb +19 -0
- data/spec/support/mocks/mock_extension.rb +6 -0
- data/spec/support/project_fixture/config/application.rb +0 -0
- data/spec/support/project_fixture/config/appsignal.yml +32 -0
- data/spec/support/project_fixture/config/environments/development.rb +0 -0
- data/spec/support/project_fixture/config/environments/production.rb +0 -0
- data/spec/support/project_fixture/config/environments/test.rb +0 -0
- data/spec/support/project_fixture/log/.gitkeep +0 -0
- data/spec/support/rails/my_app.rb +6 -0
- data/spec/support/shared_examples/instrument.rb +43 -0
- data/spec/support/stubs/delayed_job.rb +0 -0
- metadata +483 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
class MockFormatter < Appsignal::EventFormatter
|
|
2
|
+
register "mock"
|
|
3
|
+
|
|
4
|
+
attr_reader :body
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@body = "some value"
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def format(_payload)
|
|
11
|
+
["title", body]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class MissingFormatMockFormatter < Appsignal::EventFormatter
|
|
16
|
+
def transform(_payload)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class IncorrectFormatMockFormatter < Appsignal::EventFormatter
|
|
21
|
+
def format
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class MockDependentFormatter < Appsignal::EventFormatter
|
|
26
|
+
register "mock.dependent"
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
NonsenseDependency.something
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe Appsignal::EventFormatter do
|
|
34
|
+
before do
|
|
35
|
+
Appsignal::EventFormatter.initialize_formatters
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
let(:klass) { Appsignal::EventFormatter }
|
|
39
|
+
|
|
40
|
+
context "registering and unregistering formatters" do
|
|
41
|
+
it "should register a formatter" do
|
|
42
|
+
expect(klass.formatters["mock"]).to be_instance_of(MockFormatter)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "should know wether a formatter is registered" do
|
|
46
|
+
expect(klass.registered?("mock")).to be_truthy
|
|
47
|
+
expect(klass.registered?("mock", MockFormatter)).to be_truthy
|
|
48
|
+
expect(klass.registered?("mock", Hash)).to be_falsy
|
|
49
|
+
expect(klass.registered?("nonsense")).to be_falsy
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "doesn't register formatters that raise a name error in the initializer" do
|
|
53
|
+
expect(klass.registered?("mock.dependent")).to be_falsy
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "doesn't register formatters that don't have a format(payload) method" do
|
|
57
|
+
klass.register("mock.missing_format", MissingFormatMockFormatter)
|
|
58
|
+
klass.register("mock.incorrect_format", IncorrectFormatMockFormatter)
|
|
59
|
+
|
|
60
|
+
Appsignal::EventFormatter.initialize_formatters
|
|
61
|
+
|
|
62
|
+
expect(klass.registered?("mock.missing_format")).to be_falsy
|
|
63
|
+
expect(klass.registered?("mock.incorrect_format")).to be_falsy
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "should register a custom formatter" do
|
|
67
|
+
klass.register("mock.specific", MockFormatter)
|
|
68
|
+
Appsignal::EventFormatter.initialize_formatters
|
|
69
|
+
|
|
70
|
+
expect(klass.formatter_classes["mock.specific"]).to eq MockFormatter
|
|
71
|
+
expect(klass.registered?("mock.specific")).to be_truthy
|
|
72
|
+
expect(klass.formatters["mock.specific"]).to be_instance_of(MockFormatter)
|
|
73
|
+
expect(klass.formatters["mock.specific"].body).to eq "some value"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "should not have a formatter that's not registered" do
|
|
77
|
+
expect(klass.formatters["nonsense"]).to be_nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "should unregister a formatter if the registered one has the same class" do
|
|
81
|
+
klass.register("mock.unregister", MockFormatter)
|
|
82
|
+
|
|
83
|
+
klass.unregister("mock.unregister", Hash)
|
|
84
|
+
expect(klass.registered?("mock.unregister")).to be_truthy
|
|
85
|
+
|
|
86
|
+
klass.unregister("mock.unregister", MockFormatter)
|
|
87
|
+
expect(klass.registered?("mock.unregister")).to be_falsy
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
context "calling formatters" do
|
|
92
|
+
it "should return nil if there is no formatter registered" do
|
|
93
|
+
expect(klass.format("nonsense", {})).to be_nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "should call the formatter if it is registered and use a value set in the initializer" do
|
|
97
|
+
expect(klass.format("mock", {})).to eq ["title", "some value"]
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
if Appsignal::System.jruby?
|
|
2
|
+
describe Appsignal::Extension::Jruby do
|
|
3
|
+
let(:extension) { Appsignal::Extension }
|
|
4
|
+
|
|
5
|
+
describe "string conversions" do
|
|
6
|
+
it "keeps the same value during string type conversions" do
|
|
7
|
+
# UTF-8 string with NULL
|
|
8
|
+
# Tests if the conversions between the conversions without breaking on
|
|
9
|
+
# NULL terminated strings in C.
|
|
10
|
+
string = "Merry Christmas! \u0000 🎄"
|
|
11
|
+
|
|
12
|
+
appsignal_string = extension.make_appsignal_string(string)
|
|
13
|
+
ruby_string = extension.make_ruby_string(appsignal_string)
|
|
14
|
+
|
|
15
|
+
expect(ruby_string).to eq("Merry Christmas! \u0000 🎄")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "loads libappsignal with FFI" do
|
|
20
|
+
expect(described_class.ffi_libraries.map(&:name).first).to include "libappsignal"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe ".lib_extension" do
|
|
24
|
+
subject { described_class.lib_extension }
|
|
25
|
+
|
|
26
|
+
context "when on a darwin system" do
|
|
27
|
+
before { expect(Appsignal::System).to receive(:agent_platform).and_return("darwin") }
|
|
28
|
+
|
|
29
|
+
it "returns the extension for darwin" do
|
|
30
|
+
is_expected.to eq "dylib"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context "when on a linux system" do
|
|
35
|
+
before { expect(Appsignal::System).to receive(:agent_platform).and_return("linux") }
|
|
36
|
+
|
|
37
|
+
it "returns the lib extension for linux" do
|
|
38
|
+
is_expected.to eq "so"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
describe Appsignal::Extension do
|
|
2
|
+
describe ".agent_config" do
|
|
3
|
+
subject { Appsignal::Extension.agent_config }
|
|
4
|
+
|
|
5
|
+
it { is_expected.to have_key("version") }
|
|
6
|
+
it { is_expected.to have_key("triples") }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe ".agent_version" do
|
|
10
|
+
subject { Appsignal::Extension.agent_version }
|
|
11
|
+
|
|
12
|
+
it { is_expected.to be_kind_of(String) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
context "when the extension library can be loaded" do
|
|
16
|
+
subject { Appsignal::Extension }
|
|
17
|
+
|
|
18
|
+
it "should indicate that the extension is loaded" do
|
|
19
|
+
expect(Appsignal.extension_loaded?).to be_truthy
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
context "without valid config" do
|
|
23
|
+
let(:out_stream) { std_stream }
|
|
24
|
+
let(:output) { out_stream.read }
|
|
25
|
+
|
|
26
|
+
describe ".start" do
|
|
27
|
+
it "outputs a warning about not starting the extension" do
|
|
28
|
+
capture_std_streams(out_stream, out_stream) do
|
|
29
|
+
subject.start
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
expect(output).to include \
|
|
33
|
+
"WARNING: Error when reading appsignal config, appsignal not starting"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe ".stop" do
|
|
38
|
+
it "does nothing" do
|
|
39
|
+
capture_std_streams(out_stream, out_stream) do
|
|
40
|
+
subject.stop
|
|
41
|
+
end
|
|
42
|
+
expect(output).to be_empty
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context "with a valid config" do
|
|
48
|
+
before do
|
|
49
|
+
project_fixture_config.write_to_environment
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "should have a start and stop method" do
|
|
53
|
+
subject.start
|
|
54
|
+
subject.stop
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
context "with a transaction" do
|
|
58
|
+
subject { Appsignal::Extension.start_transaction("request_id", "http_request", 0) }
|
|
59
|
+
|
|
60
|
+
it "should have a start_event method" do
|
|
61
|
+
subject.start_event(0)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "should have a finish_event method" do
|
|
65
|
+
subject.finish_event("name", "title", "body", 0, 0)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "should have a record_event method" do
|
|
69
|
+
subject.record_event("name", "title", "body", 0, 1000, 1000)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "should have a set_error method" do
|
|
73
|
+
subject.set_error("name", "message", Appsignal::Extension.data_map_new)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "should have a set_sample_data method" do
|
|
77
|
+
subject.set_sample_data("params", Appsignal::Extension.data_map_new)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "should have a set_action method" do
|
|
81
|
+
subject.set_action("value")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "should have a set_namespace method" do
|
|
85
|
+
subject.set_namespace("value")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it "should have a set_queue_start method" do
|
|
89
|
+
subject.set_queue_start(10)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "should have a set_metadata method" do
|
|
93
|
+
subject.set_metadata("key", "value")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "should have a finish method" do
|
|
97
|
+
subject.finish(0)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "should have a complete method" do
|
|
101
|
+
subject.complete
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "should have a set_gauge method" do
|
|
106
|
+
subject.set_gauge("key", 1.0)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "should have a increment_counter method" do
|
|
110
|
+
subject.increment_counter("key", 1)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "should have a add_distribution_value method" do
|
|
114
|
+
subject.add_distribution_value("key", 1.0)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
context "when the extension library cannot be loaded" do
|
|
120
|
+
subject { Appsignal::Extension }
|
|
121
|
+
|
|
122
|
+
before do
|
|
123
|
+
allow(Appsignal).to receive(:extension_loaded).and_return(false)
|
|
124
|
+
allow(Appsignal).to receive(:testing?).and_return(false)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "should indicate that the extension is not loaded" do
|
|
128
|
+
expect(Appsignal.extension_loaded?).to be_falsy
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "should not raise errors when methods are called" do
|
|
132
|
+
expect do
|
|
133
|
+
subject.something
|
|
134
|
+
end.not_to raise_error
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
describe Appsignal::GarbageCollectionProfiler do
|
|
2
|
+
let(:internal_profiler) { FakeGCProfiler.new }
|
|
3
|
+
let(:profiler) { described_class.new }
|
|
4
|
+
|
|
5
|
+
before do
|
|
6
|
+
allow_any_instance_of(described_class)
|
|
7
|
+
.to receive(:internal_profiler)
|
|
8
|
+
.and_return(internal_profiler)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
context "on initialization" do
|
|
12
|
+
it "has a total time of 0" do
|
|
13
|
+
expect(profiler.total_time).to eq(0)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
context "when the GC has run" do
|
|
18
|
+
before { internal_profiler.total_time = 0.12345 }
|
|
19
|
+
|
|
20
|
+
it "fetches the total time from Ruby's GC::Profiler" do
|
|
21
|
+
expect(profiler.total_time).to eq(123)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "clears Ruby's GC::Profiler afterward" do
|
|
25
|
+
expect(internal_profiler).to receive(:clear)
|
|
26
|
+
profiler.total_time
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context "when the total GC time becomes too high" do
|
|
31
|
+
it "resets the total time" do
|
|
32
|
+
internal_profiler.total_time = 2_147_483_647
|
|
33
|
+
expect(profiler.total_time).to eq(0)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context "when the GC has run multiple times" do
|
|
38
|
+
it "adds all times from Ruby's GC::Profiler together" do
|
|
39
|
+
2.times do
|
|
40
|
+
internal_profiler.total_time = 0.12345
|
|
41
|
+
profiler.total_time
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
expect(profiler.total_time).to eq(246)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context "when in multiple threads and with a slow GC::Profiler" do
|
|
49
|
+
it "does not count garbage collection times twice" do
|
|
50
|
+
threads = []
|
|
51
|
+
results = []
|
|
52
|
+
internal_profiler.clear_delay = 0.001
|
|
53
|
+
internal_profiler.total_time = 0.12345
|
|
54
|
+
|
|
55
|
+
2.times do
|
|
56
|
+
threads << Thread.new do
|
|
57
|
+
profiler = Appsignal::GarbageCollectionProfiler.new
|
|
58
|
+
results << profiler.total_time
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
threads.each(&:join)
|
|
63
|
+
expect(results).to eq([123, 0])
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
describe Appsignal::Hooks::ActionCableHook do
|
|
2
|
+
if DependencyHelper.action_cable_present?
|
|
3
|
+
context "with ActionCable" do
|
|
4
|
+
require "action_cable/engine"
|
|
5
|
+
|
|
6
|
+
describe ".dependencies_present?" do
|
|
7
|
+
subject { described_class.new.dependencies_present? }
|
|
8
|
+
|
|
9
|
+
it "returns true" do
|
|
10
|
+
is_expected.to be_truthy
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe ActionCable::Channel::Base do
|
|
15
|
+
let(:transaction) do
|
|
16
|
+
Appsignal::Transaction.new(
|
|
17
|
+
transaction_id,
|
|
18
|
+
Appsignal::Transaction::ACTION_CABLE,
|
|
19
|
+
ActionDispatch::Request.new(env)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
let(:channel) do
|
|
23
|
+
Class.new(ActionCable::Channel::Base) do
|
|
24
|
+
def speak(_data)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.to_s
|
|
28
|
+
"MyChannel"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
let(:log) { StringIO.new }
|
|
33
|
+
let(:server) do
|
|
34
|
+
ActionCable::Server::Base.new.tap do |s|
|
|
35
|
+
s.config.logger = ActiveSupport::Logger.new(log)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
let(:connection) { ActionCable::Connection::Base.new(server, env) }
|
|
39
|
+
let(:identifier) { { :channel => "MyChannel" }.to_json }
|
|
40
|
+
let(:params) { {} }
|
|
41
|
+
let(:request_id) { SecureRandom.uuid }
|
|
42
|
+
let(:transaction_id) { request_id }
|
|
43
|
+
let(:env) do
|
|
44
|
+
http_request_env_with_data("action_dispatch.request_id" => request_id, :params => params)
|
|
45
|
+
end
|
|
46
|
+
let(:instance) { channel.new(connection, identifier, params) }
|
|
47
|
+
subject { transaction.to_h }
|
|
48
|
+
before do
|
|
49
|
+
start_agent
|
|
50
|
+
expect(Appsignal.active?).to be_truthy
|
|
51
|
+
transaction
|
|
52
|
+
|
|
53
|
+
expect(Appsignal::Transaction).to receive(:create)
|
|
54
|
+
.with(transaction_id, Appsignal::Transaction::ACTION_CABLE, kind_of(ActionDispatch::Request))
|
|
55
|
+
.and_return(transaction)
|
|
56
|
+
allow(Appsignal::Transaction).to receive(:current).and_return(transaction)
|
|
57
|
+
# Make sure sample data is added
|
|
58
|
+
expect(transaction.ext).to receive(:finish).and_return(true)
|
|
59
|
+
# Stub complete call, stops it from being cleared in the extension
|
|
60
|
+
# And allows us to call `#to_h` on it after it's been completed.
|
|
61
|
+
expect(transaction.ext).to receive(:complete)
|
|
62
|
+
|
|
63
|
+
# Stub transmit call for subscribe/unsubscribe tests
|
|
64
|
+
allow(connection).to receive(:websocket)
|
|
65
|
+
.and_return(instance_double("ActionCable::Connection::WebSocket", :transmit => nil))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe "#perform_action" do
|
|
69
|
+
it "creates a transaction for an action" do
|
|
70
|
+
instance.perform_action("message" => "foo", "action" => "speak")
|
|
71
|
+
|
|
72
|
+
expect(subject).to include(
|
|
73
|
+
"action" => "MyChannel#speak",
|
|
74
|
+
"error" => nil,
|
|
75
|
+
"id" => transaction_id,
|
|
76
|
+
"namespace" => Appsignal::Transaction::ACTION_CABLE,
|
|
77
|
+
"metadata" => {
|
|
78
|
+
"method" => "websocket",
|
|
79
|
+
"path" => "/blog"
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
expect(subject["events"].first).to include(
|
|
83
|
+
"allocation_count" => kind_of(Integer),
|
|
84
|
+
"body" => "",
|
|
85
|
+
"body_format" => Appsignal::EventFormatter::DEFAULT,
|
|
86
|
+
"child_allocation_count" => kind_of(Integer),
|
|
87
|
+
"child_duration" => kind_of(Float),
|
|
88
|
+
"child_gc_duration" => kind_of(Float),
|
|
89
|
+
"count" => 1,
|
|
90
|
+
"gc_duration" => kind_of(Float),
|
|
91
|
+
"start" => kind_of(Float),
|
|
92
|
+
"duration" => kind_of(Float),
|
|
93
|
+
"name" => "perform_action.action_cable",
|
|
94
|
+
"title" => ""
|
|
95
|
+
)
|
|
96
|
+
expect(subject["sample_data"]).to include(
|
|
97
|
+
"params" => {
|
|
98
|
+
"action" => "speak",
|
|
99
|
+
"message" => "foo"
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
context "without request_id (standalone server)" do
|
|
105
|
+
let(:request_id) { nil }
|
|
106
|
+
let(:transaction_id) { SecureRandom.uuid }
|
|
107
|
+
let(:action_transaction) do
|
|
108
|
+
Appsignal::Transaction.new(
|
|
109
|
+
transaction_id,
|
|
110
|
+
Appsignal::Transaction::ACTION_CABLE,
|
|
111
|
+
ActionDispatch::Request.new(env)
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
before do
|
|
115
|
+
# Stub future (private AppSignal) transaction id generated by the hook.
|
|
116
|
+
expect(SecureRandom).to receive(:uuid).and_return(transaction_id)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "uses its own internal request_id set by the subscribed callback" do
|
|
120
|
+
# Subscribe action, sets the request_id
|
|
121
|
+
instance.subscribe_to_channel
|
|
122
|
+
expect(transaction.to_h["id"]).to eq(transaction_id)
|
|
123
|
+
|
|
124
|
+
# Expect another transaction for the action.
|
|
125
|
+
# This transaction will use the same request_id as the
|
|
126
|
+
# transaction id used to subscribe to the channel.
|
|
127
|
+
expect(Appsignal::Transaction).to receive(:create).with(
|
|
128
|
+
transaction_id,
|
|
129
|
+
Appsignal::Transaction::ACTION_CABLE,
|
|
130
|
+
kind_of(ActionDispatch::Request)
|
|
131
|
+
).and_return(action_transaction)
|
|
132
|
+
allow(Appsignal::Transaction).to receive(:current).and_return(action_transaction)
|
|
133
|
+
# Stub complete call, stops it from being cleared in the extension
|
|
134
|
+
# And allows us to call `#to_h` on it after it's been completed.
|
|
135
|
+
expect(action_transaction.ext).to receive(:complete)
|
|
136
|
+
|
|
137
|
+
instance.perform_action("message" => "foo", "action" => "speak")
|
|
138
|
+
expect(action_transaction.to_h["id"]).to eq(transaction_id)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
context "with an error in the action" do
|
|
143
|
+
let(:channel) do
|
|
144
|
+
Class.new(ActionCable::Channel::Base) do
|
|
145
|
+
def speak(_data)
|
|
146
|
+
raise ExampleException, "oh no!"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def self.to_s
|
|
150
|
+
"MyChannel"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
it "registers an error on the transaction" do
|
|
156
|
+
expect do
|
|
157
|
+
instance.perform_action("message" => "foo", "action" => "speak")
|
|
158
|
+
end.to raise_error(ExampleException)
|
|
159
|
+
|
|
160
|
+
expect(subject).to include(
|
|
161
|
+
"action" => "MyChannel#speak",
|
|
162
|
+
"id" => transaction_id,
|
|
163
|
+
"namespace" => Appsignal::Transaction::ACTION_CABLE,
|
|
164
|
+
"metadata" => {
|
|
165
|
+
"method" => "websocket",
|
|
166
|
+
"path" => "/blog"
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
expect(subject["error"]).to include(
|
|
170
|
+
"backtrace" => kind_of(String),
|
|
171
|
+
"name" => "ExampleException",
|
|
172
|
+
"message" => "oh no!"
|
|
173
|
+
)
|
|
174
|
+
expect(subject["sample_data"]).to include(
|
|
175
|
+
"params" => {
|
|
176
|
+
"action" => "speak",
|
|
177
|
+
"message" => "foo"
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe "subscribe callback" do
|
|
185
|
+
let(:params) { { "internal" => true } }
|
|
186
|
+
|
|
187
|
+
it "creates a transaction for a subscription" do
|
|
188
|
+
instance.subscribe_to_channel
|
|
189
|
+
|
|
190
|
+
expect(subject).to include(
|
|
191
|
+
"action" => "MyChannel#subscribed",
|
|
192
|
+
"error" => nil,
|
|
193
|
+
"id" => transaction_id,
|
|
194
|
+
"namespace" => Appsignal::Transaction::ACTION_CABLE,
|
|
195
|
+
"metadata" => {
|
|
196
|
+
"method" => "websocket",
|
|
197
|
+
"path" => "/blog"
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
expect(subject["events"].first).to include(
|
|
201
|
+
"allocation_count" => kind_of(Integer),
|
|
202
|
+
"body" => "",
|
|
203
|
+
"body_format" => Appsignal::EventFormatter::DEFAULT,
|
|
204
|
+
"child_allocation_count" => kind_of(Integer),
|
|
205
|
+
"child_duration" => kind_of(Float),
|
|
206
|
+
"child_gc_duration" => kind_of(Float),
|
|
207
|
+
"count" => 1,
|
|
208
|
+
"gc_duration" => kind_of(Float),
|
|
209
|
+
"start" => kind_of(Float),
|
|
210
|
+
"duration" => kind_of(Float),
|
|
211
|
+
"name" => "subscribed.action_cable",
|
|
212
|
+
"title" => ""
|
|
213
|
+
)
|
|
214
|
+
expect(subject["sample_data"]).to include(
|
|
215
|
+
"params" => { "internal" => "true" }
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
context "without request_id (standalone server)" do
|
|
220
|
+
let(:request_id) { nil }
|
|
221
|
+
let(:transaction_id) { SecureRandom.uuid }
|
|
222
|
+
before do
|
|
223
|
+
allow(SecureRandom).to receive(:uuid).and_return(transaction_id)
|
|
224
|
+
instance.subscribe_to_channel
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
it "uses its own internal request_id" do
|
|
228
|
+
expect(subject["id"]).to eq(transaction_id)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
context "with an error in the callback" do
|
|
233
|
+
let(:channel) do
|
|
234
|
+
Class.new(ActionCable::Channel::Base) do
|
|
235
|
+
def subscribed
|
|
236
|
+
raise ExampleException, "oh no!"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def self.to_s
|
|
240
|
+
"MyChannel"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
it "registers an error on the transaction" do
|
|
246
|
+
expect do
|
|
247
|
+
instance.subscribe_to_channel
|
|
248
|
+
end.to raise_error(ExampleException)
|
|
249
|
+
|
|
250
|
+
expect(subject).to include(
|
|
251
|
+
"action" => "MyChannel#subscribed",
|
|
252
|
+
"id" => transaction_id,
|
|
253
|
+
"namespace" => Appsignal::Transaction::ACTION_CABLE,
|
|
254
|
+
"metadata" => {
|
|
255
|
+
"method" => "websocket",
|
|
256
|
+
"path" => "/blog"
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
expect(subject["error"]).to include(
|
|
260
|
+
"backtrace" => kind_of(String),
|
|
261
|
+
"name" => "ExampleException",
|
|
262
|
+
"message" => "oh no!"
|
|
263
|
+
)
|
|
264
|
+
expect(subject["sample_data"]).to include(
|
|
265
|
+
"params" => { "internal" => "true" }
|
|
266
|
+
)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
describe "unsubscribe callback" do
|
|
272
|
+
let(:params) { { "internal" => true } }
|
|
273
|
+
|
|
274
|
+
it "creates a transaction for a subscription" do
|
|
275
|
+
instance.unsubscribe_from_channel
|
|
276
|
+
|
|
277
|
+
expect(subject).to include(
|
|
278
|
+
"action" => "MyChannel#unsubscribed",
|
|
279
|
+
"error" => nil,
|
|
280
|
+
"id" => transaction_id,
|
|
281
|
+
"namespace" => Appsignal::Transaction::ACTION_CABLE,
|
|
282
|
+
"metadata" => {
|
|
283
|
+
"method" => "websocket",
|
|
284
|
+
"path" => "/blog"
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
expect(subject["events"].first).to include(
|
|
288
|
+
"allocation_count" => kind_of(Integer),
|
|
289
|
+
"body" => "",
|
|
290
|
+
"body_format" => Appsignal::EventFormatter::DEFAULT,
|
|
291
|
+
"child_allocation_count" => kind_of(Integer),
|
|
292
|
+
"child_duration" => kind_of(Float),
|
|
293
|
+
"child_gc_duration" => kind_of(Float),
|
|
294
|
+
"count" => 1,
|
|
295
|
+
"gc_duration" => kind_of(Float),
|
|
296
|
+
"start" => kind_of(Float),
|
|
297
|
+
"duration" => kind_of(Float),
|
|
298
|
+
"name" => "unsubscribed.action_cable",
|
|
299
|
+
"title" => ""
|
|
300
|
+
)
|
|
301
|
+
expect(subject["sample_data"]).to include(
|
|
302
|
+
"params" => { "internal" => "true" }
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
context "without request_id (standalone server)" do
|
|
307
|
+
let(:request_id) { nil }
|
|
308
|
+
let(:transaction_id) { SecureRandom.uuid }
|
|
309
|
+
before do
|
|
310
|
+
allow(SecureRandom).to receive(:uuid).and_return(transaction_id)
|
|
311
|
+
instance.unsubscribe_from_channel
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it "uses its own internal request_id" do
|
|
315
|
+
expect(subject["id"]).to eq(transaction_id)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
context "with an error in the callback" do
|
|
320
|
+
let(:channel) do
|
|
321
|
+
Class.new(ActionCable::Channel::Base) do
|
|
322
|
+
def unsubscribed
|
|
323
|
+
raise ExampleException, "oh no!"
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def self.to_s
|
|
327
|
+
"MyChannel"
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
it "registers an error on the transaction" do
|
|
333
|
+
expect do
|
|
334
|
+
instance.unsubscribe_from_channel
|
|
335
|
+
end.to raise_error(ExampleException)
|
|
336
|
+
|
|
337
|
+
expect(subject).to include(
|
|
338
|
+
"action" => "MyChannel#unsubscribed",
|
|
339
|
+
"id" => transaction_id,
|
|
340
|
+
"namespace" => Appsignal::Transaction::ACTION_CABLE,
|
|
341
|
+
"metadata" => {
|
|
342
|
+
"method" => "websocket",
|
|
343
|
+
"path" => "/blog"
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
expect(subject["error"]).to include(
|
|
347
|
+
"backtrace" => kind_of(String),
|
|
348
|
+
"name" => "ExampleException",
|
|
349
|
+
"message" => "oh no!"
|
|
350
|
+
)
|
|
351
|
+
expect(subject["sample_data"]).to include(
|
|
352
|
+
"params" => { "internal" => "true" }
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
else
|
|
360
|
+
context "without ActionCable" do
|
|
361
|
+
describe ".dependencies_present?" do
|
|
362
|
+
subject { described_class.new.dependencies_present? }
|
|
363
|
+
|
|
364
|
+
it "returns false" do
|
|
365
|
+
is_expected.to be_falsy
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|