public
Description: a JMX library for JRuby
Homepage: http://github.com/jmesnil/jmx4r/
Clone URL: git://github.com/jmesnil/jmx4r.git
jmx4r / lib / jmx4r.rb
100644 262 lines (238 sloc) 9.508 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# Copyright 2007 Jeff Mesnil (http://jmesnil.net)
require 'java'
 
class String
  # Transform a CamelCase String to a snake_case String.
  #--
  # Code has been taken from ActiveRecord
  def snake_case
    self.to_s.gsub(/::/, '/').
    gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
    gsub(/([a-z\d])([A-Z])/,'\1_\2').
    tr("-", "_").
    downcase
  end
end
 
module JMX
  require 'dynamic_mbean'
  require 'open_data_helper'
  require 'objectname_helper'
  require 'jruby'
 
  class MBeanServerConnectionProxy
    attr_reader :connector
 
    # Adds a connector attribute to Java's native MBeanServerConnection class.
    #
    # The connector attribute can be used to manage the connection (e.g, to close it).
    # Why this isn't included in the native MBeanServerConnection class is beyond me.
    #
    # connector:: JMXConnector instance as returned by JMXConnectorFactory.connect.
    def initialize(connector)
      @connector = connector
      @connection = connector.getMBeanServerConnection
    end
 
    # Close the connection (an unfortunate omission from the MBeanServerConnection class, imho)
    def close
      @connector.close
    end
 
    # Forward all other method messages to the underlying MBeanServerConnection instance.
    def method_missing(method, *args, &block)
      @connection.send method, *args, &block
    end
  end
 
  class MBean
    include_class 'java.util.HashMap'
    include_class 'javax.naming.Context'
    include_class 'javax.management.Attribute'
    include_class 'javax.management.ObjectName'
    include_class 'javax.management.remote.JMXConnector'
    include_class 'javax.management.remote.JMXConnectorFactory'
    include_class 'javax.management.remote.JMXServiceURL'
    JThread = java.lang.Thread
 
    attr_reader :object_name, :operations, :attributes, :connection
 
    # Creates a new MBean.
    #
    # object_name:: a string corresponding to a valid ObjectName
    # connection:: a connection to a MBean server. If none is passed,
    # use the global connection created by
    # MBean.establish_connection
    def initialize(object_name, connection=nil)
      @connection = connection || @@connection
      @object_name = object_name
      info = @connection.getMBeanInfo @object_name
      @attributes = Hash.new
      info.attributes.each do | mbean_attr |
        @attributes[mbean_attr.name.snake_case] = mbean_attr.name
        self.class.instance_eval do
          define_method mbean_attr.name.snake_case do
            @connection.getAttribute @object_name, "#{mbean_attr.name}"
          end
        end
        if mbean_attr.isWritable
          self.class.instance_eval do
            define_method "#{mbean_attr.name.snake_case}=" do |value|
              attr = Attribute.new mbean_attr.name, value
              @connection.setAttribute @object_name, attr
            end
          end
        end
      end
      @operations = Hash.new
      info.operations.each do |mbean_op|
        param_types = mbean_op.signature.map {|param| param.type}
        @operations[mbean_op.name.snake_case] = [mbean_op.name, param_types]
      end
    end
 
    def method_missing(method, *args, &block) #:nodoc:
      if @operations.keys.include?(method.to_s)
        op_name, param_types = @operations[method.to_s]
        @connection.invoke @object_name,
        op_name,
        args.to_java(:Object),
        param_types.to_java(:String)
      else
        super
      end
    end
 
    @@connection = nil
 
    # establish a connection to a remote MBean server which will
    # be used by all subsequent MBeans.
    #
    # See MBean.create_connection for a list of the keys that are
    # accepted in arguments.
    #
    # Examples
    #
    # JMX::MBean.establish_connection :port => "node23", :port => 1090
    # JMX::MBean.establish_connection :port => "node23", :username => "jeff", :password => "secret"
    def self.establish_connection(args={})
      @@connection ||= create_connection args
    end
 
    def self.remove_connection(args={})
      if @@connection
        @@connection.close rescue nil
      end
      @@connection = nil
    end
 
    def self.connection(args={})
      if args.has_key? :host or args.has_key? :port
        return create_connection(args)
      else
        @@connection ||= MBean.establish_connection(args)
      end
    end
 
    # Create a connection to a remote MBean server.
    #
    # The args accepts the following keys:
    #
    # [:host] the host of the MBean server (defaults to "localhost")
    #
    # [:port] the port of the MBean server (defaults to 3000)
    #
    # [:url] the url of the MBean server.
    # No default.
    # if the url is specified, the host & port parameters are
    # not taken into account
    #
    # [:username] the name of the user (if the MBean server requires authentication).
    # No default
    #
    # [:password] the password of the user (if the MBean server requires authentication).
    # No default
    #
    # [:credentials] custom credentials (if the MBean server requires authentication).
    # No default. It has precedence over :username and :password (i.e. if
    # :credentials is specified, :username & :password are ignored)
    #
    # [:provider_package] use to fill the JMXConnectorFactory::PROTOCOL_PROVIDER_PACKAGES.
    # No default
    #
    def self.create_connection(args={})
      host= args[:host] || "localhost"
      port = args[:port] || 3000
      username = args[:username]
      password = args[:password]
      credentials = args[:credentials]
      provider_package = args[:provider_package]
      
      # host & port are not taken into account if url is set (see issue #7)
      standard_url = "service:jmx:rmi:///jndi/rmi://#{host}:#{port}/jmxrmi"
      url = args[:url] || standard_url
      
      unless credentials
        if !username.nil? and username.length > 0
          user_password_credentials = [username, password]
          credentials = user_password_credentials.to_java(:String)
        end
      end
      
      env = HashMap.new
      env.put(JMXConnector::CREDENTIALS, credentials) if credentials
      # only fill the Context and JMXConnectorFactory properties if provider_package is set
      if provider_package
        env.put(Context::SECURITY_PRINCIPAL, username) if username
        env.put(Context::SECURITY_CREDENTIALS, password) if password
        env.put(JMXConnectorFactory::PROTOCOL_PROVIDER_PACKAGES, provider_package)
      end
 
      # the context class loader is set to JRuby's classloader when
      # creating the JMX Connection so that classes loaded using
      # JRuby "require" (and not from its classpath) can also be
      # accessed (see issue #6)
      begin
        context_class_loader = JThread.current_thread.context_class_loader
        JThread.current_thread.context_class_loader = JRuby.runtime.getJRubyClassLoader
        
        connector = JMXConnectorFactory::connect JMXServiceURL.new(url), env
        MBeanServerConnectionProxy.new connector
      ensure
        # ... and we reset the previous context class loader
        JThread.current_thread.context_class_loader = context_class_loader
      end
    end
 
    # Returns an array of MBeans corresponding to all the MBeans
    # registered for the ObjectName passed in parameter (which may be
    # a pattern).
    #
    # The args accepts the same keys than #create_connection and an
    # additional one:
    #
    # [:connection] a MBean server connection (as returned by #create_connection)
    # No default. It has precedence over :host and :port (i.e if
    # :connection is specified, :host and :port are ignored)
    #
    def self.find_all_by_name(name, args={})
      object_name = ObjectName.new(name)
      connection = args[:connection] || MBean.connection(args)
      object_names = connection.queryNames(object_name, nil)
      object_names.map { |on| MBean.new(on, connection) }
    end
 
    # Same as #find_all_by_name but the ObjectName passed in parameter
    # can not be a pattern.
    # Only one single MBean is returned.
    def self.find_by_name(name, args={})
      connection = args[:connection] || MBean.connection(args)
      MBean.new ObjectName.new(name), connection
    end
 
    def self.pretty_print (object_name, args={})
      connection = args[:connection] || MBean.connection(args)
      info = connection.getMBeanInfo ObjectName.new(object_name)
      puts "object_name: #{object_name}"
      puts "class: #{info.class_name}"
      puts "description: #{info.description}"
      puts "operations:"
      info.operations.each do | op |
        puts " #{op.name}"
        op.signature.each do | param |
          puts " #{param.name} (#{param.type} #{param.description})"
        end
        puts " ----"
        puts " description: #{op.description}"
        puts " return_type: #{op.return_type}"
        puts " impact: #{op.impact}"
      end
      puts "attributes:"
      info.attributes.each do | attr |
        puts " #{attr.name}"
        puts " description: #{attr.description}"
        puts " type: #{attr.type}"
        puts " readable: #{attr.readable}"
        puts " writable: #{attr.writable}"
        puts " is: #{attr.is}"
      end
    end
  end
end