jmx4perl/lib/JMX/Jmx4Perl/Agent.pm
2017-10-31 14:38:28 +01:00

487 lines
15 KiB
Perl

#!/usr/bin/perl
package JMX::Jmx4Perl::Agent;
use JSON;
use URI::Escape qw(uri_escape_utf8);
use HTTP::Request;
use Carp;
use strict;
use vars qw($VERSION $DEBUG);
use base qw(JMX::Jmx4Perl);
use JMX::Jmx4Perl::Request;
use JMX::Jmx4Perl::Response;
use JMX::Jmx4Perl::Agent::UserAgent;
use Data::Dumper;
$VERSION = $JMX::Jmx4Perl::VERSION;
=head1 NAME
JMX::Jmx4Perl::Agent - JSON-HTTP based acess to a remote JMX agent
=head1 SYNOPSIS
my $agent = new JMX::Jmx4Perl(mode=>"agent", url => "http://jeeserver/j4p");
my $answer = $agent->get_attribute("java.lang:type=Memory","HeapMemoryUsage");
print Dumper($answer);
{
request => {
attribute => "HeapMemoryUsage",
name => "java.lang:type=Memory"
},
status => 200,
value => {
committed => 18292736,
init => 0,
max => 532742144,
used => 15348352
}
}
=head1 DESCRIPTION
This module is not used directly, but via L<JMX::Jmx4Perl>, which acts as a
proxy to this module. You can think of L<JMX::Jmx4Perl> as the interface which
is backed up by this module. Other implementations (e.g.
=head1 METHODS
=over 4
=item $jjagent = JMX::Jmx4Perl::Agent->new(url => $url, ....)
Creates a new local agent for a given url
=over
=item url => <url to JEE server>
The url where the agent is deployed. This is a mandatory parameter. The url
must include the context within the server, which is typically based on the
name of the war archive. Example: C<http://localhost:8080/j4p> for a drop
in deployment of the agent in a standard Tomcat's webapp directory.
=item timeout => <timeout>
Timeout in seconds after which a request should be stopped if it not suceeds
within this time. This parameter is given through directly to the underlying
L<LWP::UserAgent>
=item user => <user>, password => <password>
Credentials to use for the HTTP request
=item method => <method>
The HTTP method to use for contacting the agent. Must be either "GET" or
"POST". This method is used, if the request to send dosen't specify the method
and no other parameters forces a POST context.
=item proxy => { http => '<http_proxy>', https => '<https_proxy>', ... }
=item proxy => <http_proxy>
=item proxy => { url => <http_proxy> }
Optional proxy to use
=item proxy_user => <user>, proxy_password => <password>
Credentials to use for accessing the proxy
=item target
Add a target which is used for any request served by this object if not already
a target is present in the request. This way you can setup the default target
configuration if you are using the agent servlet as a proxy, e.g.
... target => { url => "service:jmx:...", user => "...", password => "..." }
=item legacy-escape
Before version 1.0 a quite strange escaping scheme is used, when the part of a
GET requests contains a slash (/). Starting with 1.0 this scheme has changed,
but in order to allow post 1.0 Jmx4perl clients acess pre 1.0 Jolokia agents,
this option can be set to true to switch to the old escape mechanism.
=back
=cut
# HTTP Parameters to be used for transmitting the request
my @PARAMS = ("maxDepth","maxCollectionSize","maxObjects","ignoreErrors");
# Regexp for detecting invalid chars which can not be used securily in pathinfos
my $INVALID_PATH_CHARS = qr/%(5C|3F|3B|2F)/i; # \ ? ; /
# Init called by parent package within 'new' for specific initialization. See
# above for the parameters recognized
sub init {
my $self = shift;
croak "No URL provided" unless $self->cfg('url');
my $ua = JMX::Jmx4Perl::Agent::UserAgent->new();
$ua->jjagent_config($self->{cfg});
#push @{ $ua->requests_redirectable }, 'POST';
$ua->timeout($self->cfg('timeout')) if $self->cfg('timeout');
#print "TO: ",$ua->timeout(),"\n";
$ua->agent("JMX::Jmx4Perl::Agent $VERSION");
# $ua->env_proxy;
my $proxy = $self->cfg('proxy');
if ($proxy) {
my $url = ref($proxy) eq "HASH" ? $proxy->{url} : $proxy;
if (ref($url) eq "HASH") {
for my $k (keys %$url) {
$ua->proxy($k,$url->{$k});
}
} else {
if ($self->cfg('url') =~ m|^(.*?)://|) {
# Set proxy for URL scheme used
$ua->proxy($1,$url);
} else {
$ua->proxy('http',$proxy);
}
}
}
$self->{ua} = $ua;
return $self;
}
=item $url = $agent->url()
Get the base URL for connecting to the agent. You cannot change the URL via this
method, it is immutable for a given agent.
=cut
sub url {
my $self = shift;
return $self->cfg('url');
}
=item $resp = $agent->request($request)
Implementation of the JMX request as specified in L<JMX::Jmx4Perl>. It uses a
L<HTTP::Request> sent via an L<LWP::UserAgent> for posting a JSON representation
of the request. This method shouldn't be called directly but via
L<JMX::Jmx4Perl>->request().
=cut
sub request {
my $self = shift;
my @jmx_requests = $self->cfg('target') ? $self->_update_targets(@_) : @_;
my $ua = $self->{ua};
my $http_req = $self->_to_http_request(@jmx_requests);
if ($self->{cfg}->{verbose}) {
print $http_req->as_string;
print "===========================================================\n";
}
#print Dumper($http_req);
my $http_resp = $ua->request($http_req);
my $json_resp = {};
if ($self->{cfg}->{verbose}) {
print $http_resp->as_string,"\n";
print "===========================================================\n";
}
eval {
$json_resp = from_json($http_resp->content());
};
my $json_error = $@;
if ($http_resp->is_error) {
return JMX::Jmx4Perl::Response->new
(
status => $http_resp->code,
value => $json_error ? $http_resp->content : $json_resp,
error => $json_error ? $self->_prepare_http_error_text($http_resp) :
ref($json_resp) eq "ARRAY" ? join "\n", map { $_->{error} } grep { $_->{error} } @$json_resp : $json_resp->{error},
stacktrace => ref($json_resp) eq "ARRAY" ? $self->_extract_stacktraces($json_resp) : $json_resp->{stacktrace},
request => @jmx_requests == 1 ? $jmx_requests[0] : \@jmx_requests
);
} elsif ($json_error) {
# If is not an HTTP-Error and deserialization fails, then we
# probably got a wrong URL and get delivered some server side
# document (with HTTP code 200)
my $e = $json_error;
$e =~ s/(.*)at .*?line.*$/$1/;
return JMX::Jmx4Perl::Response->new
(
status => 400,
error =>
"Error while deserializing JSON answer (Wrong URL ?)\n" . $e,
value => $http_resp->content
);
}
my @responses = ($self->_from_http_response($json_resp,@jmx_requests));
if (!wantarray && scalar(@responses) == 1) {
return shift @responses;
} else {
return @responses;
}
}
=item $encrypted = $agent->encrypt($plain)
Encrypt a password which can be used in configuration files in order to
obfuscate the clear text password.
=cut
sub encrypt {
return "[[" . &JMX::Jmx4Perl::Agent::UserAgent::encrypt(shift) . "]]";
}
# Create an HTTP-Request for calling the server
sub _to_http_request {
my $self = shift;
my @reqs = @_;
if ($self->_use_GET_request(\@reqs)) {
# Old, rest-style
my $url = $self->request_url($reqs[0]);
return HTTP::Request->new(GET => $url);
} else {
my $url = $self->cfg('url') || croak "No URL provided";
$url .= "/" unless $url =~ m|/$|;
my $request = HTTP::Request->new(POST => $url);
my $content = to_json(@reqs > 1 ? \@reqs : $reqs[0], { convert_blessed => 1 });
#print Dumper($reqs[0],$content);
$request->content($content);
return $request;
}
}
sub _use_GET_request {
my $self = shift;
my $reqs = shift;
if (@$reqs == 1) {
my $req = $reqs->[0];
# For proxy configs and explicite set POST request, get can not be
# used
return 0 if defined($req->get("target"));
#print Dumper($req);
for my $r ($req->method,$self->cfg('method')) {
return lc($r) eq "get" if defined($r);
}
# GET by default
return 1;
} else {
return 0;
}
}
# Create one or more response objects for a given request
sub _from_http_response {
my $self = shift;
my $json_resp = shift;
my @reqs = @_;
if (ref($json_resp) eq "HASH") {
return JMX::Jmx4Perl::Response->new(%{$json_resp},request => $reqs[0]);
} elsif (ref($json_resp) eq "ARRAY") {
die "Internal: Number of request and responses doesn't match (",scalar(@reqs)," vs. ",scalar(@$json_resp)
unless scalar(@reqs) == scalar(@$json_resp);
my @ret = ();
for (my $i=0;$i<@reqs;$i++) {
die "Internal: Not a hash --> ",$json_resp->[$i] unless ref($json_resp->[$i]) eq "HASH";
my $response = JMX::Jmx4Perl::Response->new(%{$json_resp->[$i]},request => $reqs[$i]);
push @ret,$response;
}
return @ret;
} else {
die "Internal: Not a hash nor an array but ",ref($json_resp) ? ref($json_resp) : $json_resp;
}
}
# Update targets if not set in request.
sub _update_targets {
my $self = shift;
my @requests = @_;
my $target = $self->_clone_target;
for my $req (@requests) {
$req->{target} = $target unless exists($req->{target});
# A request with existing but undefined target removes
# any default
delete $req->{target} unless defined($req->{target});
}
return @requests;
}
sub _clone_target {
my $self = shift;
die "Internal: No target set" unless $self->cfg('target');
my $target = { %{$self->cfg('target')} };
if ($target->{env}) {
$target->{env} = { %{$target->{env}}};
}
return $target;
}
=item $url = $agent->request_url($request)
Generate the URL for accessing the java agent based on a given request.
=cut
sub request_url {
my $self = shift;
my $request = shift;
my $url = $self->cfg('url') || croak "No base url given in configuration";
$url .= "/" unless $url =~ m|/$|;
my $type = $request->get("type");
my $req = $type . "/";
$req .= $self->_escape($request->get("mbean"));
if ($type eq READ) {
$req .= "/" . $self->_escape($request->get("attribute"));
$req .= $self->_extract_path($request->get("path"));
} elsif ($type eq WRITE) {
$req .= "/" . $self->_escape($request->get("attribute"));
$req .= "/" . $self->_escape($self->_null_escape($request->get("value")));
$req .= $self->_extract_path($request->get("path"));
} elsif ($type eq LIST) {
$req .= $self->_extract_path($request->get("path"));
} elsif ($type eq EXEC) {
$req .= "/" . $self->_escape($request->get("operation"));
for my $arg (@{$request->get("arguments")}) {
# Array refs are sticked together via ","
my $a = ref($arg) eq "ARRAY" ? join ",",@{$arg} : $arg;
$req .= "/" . $self->_escape($self->_null_escape($a));
}
} elsif ($type eq SEARCH) {
# Nothing further to append.
}
# Squeeze multiple slashes
$req =~ s|((?:!/)?/)/*|$1|g;
#print "R: $req\n";
if ($req =~ $INVALID_PATH_CHARS || $request->{use_query}) {
$req = "?p=$req";
}
my @params;
for my $k (@PARAMS) {
push @params, $k . "=" . $request->get($k)
if $request->get($k);
}
$req .= ($req =~ /\?/ ? "&" : "?") . join("&",@params) if @params;
return $url . $req;
}
# =============================================================================
# Return an (optional) path which must already be escaped
sub _extract_path {
my $self = shift;
my $path = shift;
return $path ? "/" . $path : "";
}
# Escaping is simple:
# ! --> !!
# / --> !/
# It is not done by backslashes '\' since often they get magically get
# translated into / when part of an URL
sub _escape {
my $self = shift;
my $input = shift;
if ($self->cfg('legacy-escape')) {
# Pre 1.0 escaping:
$input =~ s|(/+)|"/" . ('-' x length($1)) . "/"|eg;
$input =~ s|^/-|/^|; # The first slash needs to be escaped (first)
$input =~ s|-/$|+/|; # as well as last slash. They need a special
# escape, because two subsequent slashes get
# squeezed to one on the server side
} else {
# Simpler escaping since 1.0:
$input =~ s/!/!!/g;
$input =~ s/\//!\//g;
}
return URI::Escape::uri_escape_utf8($input,"^A-Za-z0-9\-_.!~*'()/"); # Added "/" to
# default
# set. See L<URI>
}
# Escape empty and undef values so that they can be detangled
# on the server side
sub _null_escape {
my $self = shift;
my $value = shift;
if (!defined($value)) {
return "[null]";
} elsif (! length($value)) {
return "\"\"";
} else {
return $value;
}
}
# Prepare some readable error text
sub _prepare_http_error_text {
my $self = shift;
my $http_resp = shift;
my $content = $http_resp->content;
my $error = "Error while fetching ".$http_resp->request->uri." :\n\n" . $http_resp->status_line . "\n";
chomp $content;
if ($content && $content ne $http_resp->status_line) {
my $error .= "=" x length($http_resp->status_line) . "\n\n";
my $short = substr($content,0,600);
$error .= $short . (length($short) < length($content) ? "\n\n... [truncated] ...\n\n" : "") . "\n"
}
return $error;
}
# Extract all stacktraces stored in the given array ref of json responses
sub _extract_stacktraces {
my $self = shift;
my $json_resp = shift;
my @ret = ();
for my $j (@$json_resp) {
push @ret,$j->{stacktrace} if $j->{stacktrace};
}
return @ret ? (scalar(@ret) == 1 ? $ret[0] : \@ret) : undef;
}
=back
=cut
# ===================================================================
# Specialized UserAgent for passing in credentials:
=head1 LICENSE
This file is part of jmx4perl.
Jmx4perl is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
jmx4perl is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with jmx4perl. If not, see <http://www.gnu.org/licenses/>.
A commercial license is available as well. Please contact roland@cpan.org for
further details.
=head1 AUTHOR
roland@cpan.org
=cut
1;