Showing posts with label tutorial. Show all posts
Showing posts with label tutorial. Show all posts

Build your own Mobicents Dashboard in 15 Minutes!

Monday, December 17, 2012 em 12:03

The Mobicents suite is growing everyday, from the early days of the initial JAIN SLEE container project, a lot of new fronts have been going on, such as SIP Servlets, Diameter Stack, Media Server, SS7 Stack, Restcomm, etc.

With such a vaste suite of projects, the need for common management and monitoring tools is a major concern we have been facing, as each project having it's own set of tools is not a optimal solution. At least a common framework/interface is desired.

JMX is a standard for monitoring and managing JVMs, but it's connector uses RMI, which is not the sexiest protocol these days. In that sense, we have searched for a JMX <=> HTTP bridge to use a REST-like protocol as a frontend to the JMX server. This brought us to Jolokia. With such bridge, we can easily extend our choices of management frameworks outside Java. Plus, it brings many other goodies, such as being firewall friendly (HTTP is allowed anywhere), security (filter what is accessible), support for bulk requests, etc.

Installing Jolokia

Jolokia is very simple to install in the JBoss AS container:
  1. Download Jolokia from here. Select the binary package.
  2. Extract the zip file.
  3. Copy the jolokia.war from the agents sub-directory to your JBoss AS deploy directory.
  4. If the JBoss AS is not running already, start it.

You should now be able to issue HTTP requests to Jolokia agent. Using curl (or pointing your browser to the URL), you can do:
$ curl http://localhost:8080/jolokia/read/java.lang:type=Memory/HeapMemoryUsage/used

{"timestamp":1355453720,"status":200,"request":{"mbean":"java.lang:type=Memory","path":"used",
"attribute":"HeapMemoryUsage","type":"read"},"value":310667560}

This is how we read a value, returned in a JSON format, using Jolokia REST API. The format used is the following: <base-url>/read/<mbean name>/<attribute name>/<inner path>. You can learn more reading the Jolokia Reference Manual.

So, now that we have this working, lets make some good use of it. Graphs are the best way to show this data over time. Looking for a good JavaScript graphs library, there are several options such as Google Chart Tools, HighCharts, jqPlot, YUI Charts, etc. For this demo we will be using HighCharts. It is not completely free, but it is a very good and feature rich chart library, plus there's this great article by Tomasz Nurkiewicz to help us getting started.

So, let's start to build our dashboard. Create an HTML file in your favorite editor and, here we go:

<html>
 <body>
  <div id="chart" />

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/jquery-ui.js"></script>
  <script src="http://jolokia.org/dist/1.0.6/js/jolokia.js"></script>
  <script src="http://jolokia.org/dist/1.0.6/js/jolokia-simple.js"></script>
  <script src="http://code.highcharts.com/highcharts.src.js"></script>

  <script id="source" language="javascript" type="text/javascript">
   $(document).ready(function() {
    var jolokia = new Jolokia("http://localhost:8080/jolokia");

    var chart = new Highcharts.Chart({
     chart: {
      renderTo: 'chart',
      defaultSeriesType: 'spline',
      events: {
       load: function() {
        var series = this.series[0];
        setInterval(function() {
         var x = (new Date()).getTime();
         var memoryUsed = jolokia.getAttribute("java.lang:type=Memory", "HeapMemoryUsage", "used");
         series.addPoint({
          x: new Date().getTime(),
          y: parseInt(memoryUsed)
         }, true, series.data.length >= 50);
        }, 1000);
       }
      }
     },
     title: {
      text: 'HeapMemoryUsage'
     },
     xAxis: {
      type: 'datetime'
     },
     yAxis: {
      title: { text: 'HeapMemoryUsage' }
     },
     series: [{
      data: [],
      name: 'Used Memory'
     }]
    });
   });
  </script>
 </body>
</html>

These 50 lines of HTML/JavaScript code already produce a very nice chart:

This is as easy as it gets! We may now add a lot of other extras, make it more generic to handle several charts, several data sources per chart, make use of jQuery UI Sortable to make a portlet-like dashboard, where it is possible to group and arrange those charts as desired, etc. Ready? Let's go!

<html>
 <head>
  <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/themes/black-tie/jquery-ui.css"/>
  <style>
   .column { width: 400px; float: left; padding-bottom: 10px; }
   .portlet { margin: 0 1em 1em 0; }
   .portlet-header { margin: 0.3em; padding-bottom: 4px; padding-left: 0.2em; }
   .portlet-header .ui-icon { float: right; }
   .portlet-content { padding: 0.4em; }
   .ui-sortable-placeholder { border: 1px dotted black; visibility: visible !important; height: 238px !important; }
   .ui-sortable-placeholder * { visibility: hidden; }
  </style>
 </head>

 <body style="background-color: #EEE; font-family: Verdana; font-size: small;">
  <!-- The template to be used for new portlets -->
  <div style="display: none;">
   <div class="portlet ui-widget-content ui-helper-clearfix ui-corner-all" id="portlet-template" style="">
    <div class="portlet-header ui-widget-header ui-corner-all">
     <span class='ui-icon ui-icon-minusthick'></span>
     <span class="title"> </span>
    </div>
    <div class="portlet-content"></div>
   </div>
  </div>

  <div><h2 style="text-align: center;">.:[ MOBICENTS DASHBOARD ]:.</h2></div>
  <hr />
  <div id="charts" class="column" /></div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/jquery-ui.js"></script>
  <script src="http://jolokia.org/dist/1.0.6/js/jolokia.js"></script>
  <script src="http://jolokia.org/dist/1.0.6/js/jolokia-simple.js"></script>
  <script src="http://code.highcharts.com/highcharts.src.js"></script>

  <script id="source" language="javascript" type="text/javascript">
  $(document).ready(function() {
   jolokia = new Jolokia({url: "http://localhost:8080/jolokia", fetchInterval: 1000});

   var factory = new JmxChartsFactory();
    factory.create([
     {
      type: 'read',
      name: 'org.mobicents.slee:name=EventRouterStatistics',
      attribute: 'AverageEventRoutingTime'
     }
    ]);
    factory.create([
     {
      type: 'read',
      name: 'org.mobicents.slee:name=EventRouterStatistics',
      attribute: 'ActivitiesMapped'
     }
    ]);
    executors = []
    numExecutors = jolokia.getAttribute("org.mobicents.slee:name=EventRouterConfiguration","EventRouterThreads");
    for (var i = 0; i < numExecutors; i++) {
     executors[i] = {
      type: 'exec',
      name: 'org.mobicents.slee:name=EventRouterStatistics',
      operation: 'getAverageEventRoutingTime(int)',
      args: [i]
     }
    }
    factory.create(executors);
    factory.create([
     {
      type: 'read',
      name: 'java.lang:type=Memory',
      attribute: 'HeapMemoryUsage',
      path: 'committed'
     },
     {
      type: 'read',
      name: 'java.lang:type=Memory',
      attribute: 'HeapMemoryUsage',
      path: 'used'
     }
    ]);
    factory.create(
     {
      type: 'read',
      name: 'java.lang:type=OperatingSystem',
      attribute: 'SystemLoadAverage'
     }
    );
    factory.create(
     {
      type:  'read',
      name:  'java.lang:type=Threading',
      attribute: 'ThreadCount'
     }
    );
   });

   function JmxChartsFactory(keepHistorySec, pollInterval, columnsCount) {
    var series = [];
    var monitoredMbeans = [];
    var chartsCount = 0;

    // if not given a value for number of columns, use what fits.
    columnsCount = columnsCount || Math.floor($(window).width()/$(".column").width());
    // poll interval, defaults to 1000ms
    pollInterval = pollInterval || 1000;
    // how many data points to show in the graphs, defaults to 30
    var keepPoints = (keepHistorySec || 30) / (pollInterval / 1000);

    setupPortletsContainer(columnsCount);

    setInterval(function() {
     pollAndUpdateCharts();
    }, pollInterval);

    this.create = function(mbeans) {
     mbeans = $.makeArray(mbeans);
     series = series.concat(createChart(mbeans).series);
     monitoredMbeans = monitoredMbeans.concat(mbeans);
    };

    function pollAndUpdateCharts() {
     var requests = prepareBatchRequest();
     var responses = jolokia.request(requests);
     updateCharts(responses);
    }

    function createNewPortlet(name) {
     return $('#portlet-template')
       .clone(true)
       .appendTo($('.column')[chartsCount++ % columnsCount])
       .removeAttr('id')
       .find('.title').text((name.length > 50 ? '...' : '') + name.substring(name.length - 50, name.length)).end()
       .find('.portlet-content')[0];
    }

    function setupPortletsContainer() {
     var column = $('.column');
     for(var i = 1; i < columnsCount; ++i){
      column.clone().appendTo(column.parent());
     }
     $(".column").sortable({
      connectWith: ".column"
     });

     $(".portlet-header .ui-icon").click(function() {
      $(this).toggleClass("ui-icon-minusthick").toggleClass("ui-icon-plusthick");
      $(this).parents(".portlet:first").find(".portlet-content").toggle();
     });
     $(".column").disableSelection();
    }

    function prepareBatchRequest() {
     return $.map(monitoredMbeans, function(mbean) {
      switch(mbean.type) {
       case 'read':
        return {
         type: mbean.type,
         opts: mbean.args,
         mbean: mbean.name,
         attribute: mbean.attribute,
         path: mbean.path
        };
        break;
       case 'exec':
        return {
         type: mbean.type,
         arguments: mbean.args,
         mbean: mbean.name,
         operation: mbean.operation,
         path: mbean.path
        };
        break;
      }
     });
    }

    function updateCharts(responses) {
     var curChart = 0;
     $.each(responses, function() {
      var point = {
       x: this.timestamp * 1000,
       y: parseFloat(this.value)
      };
      var curSeries = series[curChart++];
      curSeries.addPoint(point, true, curSeries.data.length >= keepPoints);
     });
    }

    function createChart(mbeans) {
     return new Highcharts.Chart({
      chart: {
       renderTo: createNewPortlet(mbeans[0].name),
       height: 200,
       defaultSeriesType: 'spline',
      },
      title: { text: null },
      xAxis: { type: 'datetime' },
      yAxis: { title: { text: mbeans[0].attribute || mbeans[0].operation } },
      legend: {
       enabled: true,
       borderWidth: 0
      },
      credits: {enabled: false},
      series: $.map(mbeans, function(mbean) {
       return {
        data: [],
        name: mbean.path || mbean.attribute || mbean.args
       }
      })
     })
    }
   }
  </script>
 </body>
</html>

A bit more of code than previously, but with this we added 6 different graphs, some with more than one data series, we added portlet behavior... in my opinion, it is still quite simple for the output we get of it:

Is it cool or what? And all of this in a single static html file. Personally, I love it.

And this is the way we are heading with Mobicents Monitoring and Management... adding some extras such as thresholds with some kind of alarms/notifications, more customization like easily adding/removing charts at runtime through web interface, add some persistence to remember history and preferences, and many more to make this a solid tool to keep your Mobicents suite under control!

Mobicents Diameter has a new home.. powered by git!

Wednesday, July 4, 2012 em 01:12
Due to the increasingly number and complexity of sub-projects, Mobicents has been splitting it's projects under independent project homes.

We are now at a transition stage, where each project lead will migrate his project at the appropriate time. For Diameter, after our major release (1.4.0.FINAL), it seemed the best time to do this migration.

The new home of the Mobicents Diameter project is located at http://code.google.com/p/jdiameter/. Feel free to visit us!

With this change, we have also taken the chance to move to a "better" and more powerful version control system, so we have changed from SVN to Git. We hope this will ease the contributions, which have been happening more often, thanks to out thriving community!

Despite being a allegedly better VCS, Git also has it's own shortcomings, such as not being able to checkout a single folder. The lack of this feature has impacted our structure, since it makes it impossible to independently release sub-components in the same Git repository.

While digging for a solution, I've came across the "sparse checkout" feature. While this may work for checking out only some folder(s), it is not supported by maven release plugin, thus not fixing the main problem. Another possible (and probably more correct) solution would be to use the "sub-repository" approach, where we'd split the components into different sub-repositories.

We actually did this on a first phase, but after realizing the big changes it would imply and being aware that google code messes with the automatic revision links (http://code.google.com/p/support/issues/detail?id=3245) we have decided to revert back to the single repository approach and abdicate from the release independency for each component. We have barely used it, anyway. The only exception is for the Jopr RHQ Plugin, so it got it's own sub-repository.

As a reference for someone going through the same process of moving from a [googlecode] SVN repository to a [googlecode] git repository, I'll leave a summary of the commands used for the move (please keep in mind that I'm totally new to git, so these may not be the optimal way, feel free to comment):

0. (optional) Since SVN only lists the username and git uses the email as well, the migration will cause weird authors such as "brainslog <brainslog@bf0df8d0-2c1f-0410-b170-bd30377b63dc>" it may be good to sanitize the authors. For that I've used the following script in the SVN repository:
svn log -q | awk -F '|' '/^r/ {sub("^ ", "", $2); sub(" $", "", $2); print $2" = "$2" <"$2">"}' | sort -u > authors.txt

And then I've fixed the outcome in authors.txt to the correct: username = Full Name .

1. Clone the new (and probably empty) repository
git clone https://@code.google.com/p/jdiameter/ git-jdiameter

2. Clone from SVN (with authors fixed) to the cloned git repository, with full history, tags and branches
git svn clone -A authors.txt -Ttrunk/servers/diameter -ttags/servers/diameter -bbranches/servers/diameter https://mobicents.googlecode.com/svn git-jdiameter

Where -A points to the authors file, -T to the trunk of the SVN repository, -t to the tags and -b to the branches. Use only what you need, I didn't needed the branches part as we don't have any.

3. Enter the local git repository
cd git-jdiameter

4. Push!
git push --all

5. Create the git tags from SVN tags (I know this can be automatized but I needed to make some customizations to existing tags, as not all were correct)
git tag "1.0.0.BETA1" "refs/remotes/tags/jdiameter-1.0.0.BETA1"
...
git tag "1.0.0.FINAL" "refs/remotes/tags/1.0.0.FINAL"

6. Push.. again!
git push --tags

These are the 7 magical steps! In case that you don't care about history, simply do a "svn export --force http://mobicents.googlecode.com/svn/trunk/servers/diameter/ git-jdiameter" and push it, nothing else. Also, as mentioned, I did not had branches but I suppose they'd make it in the #4 push.

Mobicents Diameter users working with trunk master, must now switch to this new repository as the SVN will not be updated anymore! Do a "git clone https://code.google.com/p/jdiameter/" and hack away!

Developing an Offline Charging Application with Mobicents Diameter

Monday, January 24, 2011 em 15:01

Introduction
A couple of months ago, the Mobicents Diameter Team decided to start a series of educational posts regarding Diameter development using Mobicents Diameter solution.

Bartosz Baranowski has kicked-off with the "Creating a "Hello World" Application with Mobicents Diameter" article where two things were addressed:
  • Explaining some basics of the Diameter protocol such as Diameter Nodes, Realms, Applications, Messages, AVPs, Sessions and all those fundamental concepts that hopefully helps to make sense out of this;
  • A step-by-step example on how to create server and client instances for a Diameter Application (a fictitious Application was used to exemplify) using Mobicents Diameter. It includes detailed code explanation and it's a good read as a preparation for this one if you're new on the subject.
In this second post, we will learn how easy it is to create an Offline Charging Application using Mobicents Diameter. Also, for those not familiar with what is Offline Charging (and Online Charging as well) a brief introduction will be provided.

Fasten your seatbelt, the ride is about to begin!

Offline and Online Charging. What's that about?
Online Charging is the name used by 3GPP for pre-paid charging in the IMS scope. It is the charging which occurs in real-time, where the service cost is deducted from the user balance (which has been previously loaded by the user) while the service is going on. In IMS this is the Ro interface, and is defined by 3GPP TS 32.299 (and extending RFC 4006 - Diameter Credit Control Application).

On the other hand, Offline Charging is the 3GPP name for post-paid charging, where the provided services are not paid at the time of their usage but rather in a periodic manner, such as at the end of each month. However, while the service is on course, it's usage is logged as a Call Detail Record (CDR) that will be processed later by a Billing system. This corresponds to the IMS interface Rf, defined also by 3GPP TS 32.299 (inheriting from Diameter Base Accounting in RFC 3588).

The CDR generation is the responsibility of an Offline Charging Server.

Please keep in mind that while we are using the Online/Offline terminology introduced by 3GPP for IMS, this post does not intend to focus on IMS details, and so, we will only use the Diameter Base Accounting Application, allowing to simplify the exchanged messages and provide a more straightforward tutorial.

The Messages and AVPs
The messages exchanged for offline charging, as defined in RFC 3588 - Section 9.7, are only two: Accounting-Request (ACR) and Accounting-Answer (ACA), with Command-Code 271.

While only a pair of Request/Answer is defined, it can be used with different purposes, depending on the value of the Accounting-Record-Type AVP (code 480). This AVP can have the following values:
  • EVENT_RECORD (1) - Defines that this is charging for a one-time event, such as a sent SMS;
  • START_RECORD (2) - In a service with a measurable length (eg: voice call) this value defines that such service has started;
  • INTERIM_RECORD (3) - In a service with the above characteristics, this ACR type provides cumulative accounting information;
  • STOP_RECORD (4) - This is used to inform that a service with measurable length has terminated and to provide cumulative accounting information regarding it.
Another meaningful AVP is the Acct-Interim-Interval AVP (code 85) where it is specified the interval at which intermediate records should be produced, by sending ACR messages with Accounting-Record-Type set to INTERIM_RECORD. The absence of it, or a value of 0, means that no intermedite records are needed.

An important note is that Base Accounting does not define any service-specific AVPs, as it is not intended to be a usable Application but rather the base for the ones being defined. As such, we will 'abuse' from User-Name AVP, and piggyback the Subscription and Service identifiers into it, in the form ".@mobicents.org".

Putting it all together!
Assuming you already know how to configure and set up the stack in your Java project (which has been explained by Bartosz on the first post of these educational series), let's start by defining the interface for our Charging Client enabler, to be used by the applications:
package org.mobicents.diameter.simulator.client;

public interface OfflineChargingClient {

  // Accounting-Record-Type Values --------------------------------------------
  static final int ACCOUNTING_RECORD_TYPE_EVENT    = 1;
  static final int ACCOUNTING_RECORD_TYPE_START    = 2;
  static final int ACCOUNTING_RECORD_TYPE_INTERIM  = 3;
  static final int ACCOUNTING_RECORD_TYPE_STOP     = 4;

  /**
   * Sends an Accounting-Request with Accounting-Record-Type set to "2" and the 
   * corresponding Subscription-Id and Service-Id AVPs filled accordingly.
   * 
   * @param subscriptionId the String value to be used for Subscription-Id AVP
   * @param serviceId the String value to be used for Service-Id AVP
   * @throws Exception
   */
  public abstract void startOfflineCharging(String subscriptionId, String serviceId)
    throws Exception;

  /**
   * Sends an Accounting-Request with Accounting-Record-Type set to "3" and the
   * corresponding Subscription-Id and Service-Id AVPs filled accordingly.
   * 
   * @param subscriptionId the String value to be used for Subscription-Id AVP
   * @param serviceId the String value to be used for Service-Id AVP
   * @throws Exception
   */
  public abstract void updateOfflineCharging(String subscriptionId, String serviceId)
    throws Exception;

  /**
   * Sends an Accounting-Request with Accounting-Record-Type set to "4" and the
   * corresponding Subscription-Id and Service-Id AVPs filled accordingly.
   * 
   * @param subscriptionId the String value to be used for Subscription-Id AVP
   * @param serviceId the String value to be used for Service-Id AVP
   * @throws Exception
   */
  public abstract void terminateOfflineCharging(String subscriptionId, String serviceId) 
    throws Exception;

  /**
   * Sends an Accounting-Request with Accounting-Record-Type set to "1" and the
   * corresponding Subscription-Id and Service-Id AVPs filled accordingly.
   * 
   * @param subscriptionId the String value to be used for Subscription-Id AVP
   * @param serviceId the String value to be used for Service-Id AVP
   * @throws Exception
   */
  public abstract void eventOfflineCharging(String subscriptionId, String serviceId)
    throws Exception;

  /**
   * Sets the listener to receive the callbacks from this charging client. 
   * @param listener an OfflineChargingClientListener implementor to be the listener
   */
  public abstract void setListener(OfflineChargingClientListener listener);

}
Also, we will need the applications to implement the Listener interface, in order to receive the callbacks from the Offline Charging Enabler. The interface to be implemented is quite simple, with only one method for the callback:
package org.mobicents.diameter.simulator.client;

/**
 * Listener interface to be implemented by applications 
 * wanting to have Offline Accounting
 */
public interface OfflineChargingClientListener {

  /**
   * Callback method invoked by Offline Charging Client to deliver answers
   * 
   * @param subscriptionId the String value identifying the user
   * @param serviceId the String value identifying the service
   * @param sessionId a String value identifying the accounting session
   * @param accountingRecordType the type of Accounting Record the answer refers to
   * @param accountingRecordNumber the Accounting Record number the answer refers to
   * @param resultCode the Result-Code value obtained from the answer
   * @param acctInterimInterval the interval in seconds to send updates
   */
  public void offlineChargingAnswerCallback(String subscriptionId, 
      String serviceId, String sessionId, int accountingRecordType, 
      long accountingRecordNumber, long resultCode, long acctInterimInterval);
  
}
The implementation for the OfflineChargingClient (not complete, just the basics to understanding) is the following:
public class OfflineChargingClientImpl implements OfflineChargingClient, 
    NetworkReqListener, EventListener<Request, Answer>, 
    StateChangeListener<AppSession>, ClientAccSessionListener {

  // Application Id -----------------------------------------------------------
  private static final ApplicationId ACCOUNTING_APPLICATION_ID = 
    ApplicationId.createByAccAppId(0, 3);

  // Configuration Values -----------------------------------------------------
  private static final String SERVER_HOST = "127.0.0.1";

  private static String REALM_NAME = "mobicents.org";

  private SessionFactory sessionFactory;
  private AccSessionFactoryImpl accountingSessionFactory;
  private OfflineChargingClientListener listener;

  private ConcurrentHashMap<String, ClientAccSession> acctSessions = 
    new ConcurrentHashMap<String, ClientAccSession>(); 
  private ConcurrentHashMap<String, Integer> acctRecNumberMap = 
    new ConcurrentHashMap<String, Integer>(); 
  
  public OfflineChargingClientImpl() throws Exception {
    // Initalize Stack
    ...
  }
At this point we just define some constants, configuration values and variables to be used later. We define two maps to allow us to keep sessions and Accounting-Record-Number values stored in the enabler. This could be moved client-side if desired.
We will next define some auxiliary methods to assit in common operations, such as creating Accounting-Requests, retrieving the appropriate Accounting-Record-Number, etc.
// Aux Methods --------------------------------------------------------------

  /**
   * Gets an accounting record number for the specified user+service id
   * @param identifier the user+service identifier
   * @param isStart indicates if it's an initial record, which should be set to 0
   * @return the accounting record number to be used in the AVP
   */
  private int getAccountingRecordNumber(String sessionId, boolean isStart) {
    // An easy way to produce unique numbers is to set the value to 0 for
    // records of type EVENT_RECORD and START_RECORD, and set the value to 1
    // for the first INTERIM_RECORD, 2 for the second, and so on until the 
    // value for STOP_RECORD is one more than for the last INTERIM_RECORD.
    int accRecNumber = 0;
    if(!isStart) {
      accRecNumber = acctRecNumberMap.get(sessionId);
      accRecNumber = accRecNumber++;
    }

    acctRecNumberMap.put(sessionId, accRecNumber);
    
    return accRecNumber;
  }
  
  /**
   * Creates an Accounting-Request with the specified data.
   * 
   * @param session the session to be used for creating the request 
   * @param accRecordType the type of Accounting Record (Event, Start, Interim, Stop)
   * @param username the value to be used in the User-Name AVP
   * @return an AccountRequest object with the needed AVPs filled
   * @throws InternalException
   */
  private AccountRequest createAccountingRequest(ClientAccSession session, 
      int accRecordType, int accRecNumber, String username) throws InternalException {
    AccountRequest acr = new AccountRequestImpl(session, accRecordType, accRecNumber,
        REALM_NAME, SERVER_HOST);

    // Let's 'abuse' from User-Name AVP and use it for identifying user@service
    AvpSet avps = acr.getMessage().getAvps();
    avps.addAvp(Avp.USER_NAME, username, false);

    return acr;
  }
  
  /**
   * Method for creating and sending an Accounting-Request
   * 
   * @param identifier the user+service identifier to be used in the User-Name AVP
   * @param accRecType the type of Accounting Record (Event, Start, Interim, Stop)
   * @throws Exception
   */
  private void sendAccountingRequest(ClientAccSession session, String identifier,
      int accRecType, int accRecNumber) throws Exception {
    // Create Accounting-Request
    AccountRequest acr = createAccountingRequest(session, accRecType, accRecNumber, 
      identifier);
    
    // Send it
    session.sendAccountRequest(acr);
  }

  /**
   * Aux method for generating a unique identifier from subscription and service ids
   * @param subscriptionId the subscription id to be used
   * @param serviceId the service id to be used
   * @return the generated unique identifier
   */
  private String getIdentifier(String subscriptionId, String serviceId) {
    return subscriptionId + "." + serviceId + "@" + REALM_NAME;
  }
And finally we can do the real implementation for the Offline Charging Client which, hopefully, became a bit simpler.
// Offline Charging Client Implementation -----------------------------------
  
  public void setListener(OfflineChargingClientListener listener) {
    this.listener = listener;
  }

  public void startOfflineCharging(String subscriptionId, String serviceId)
      throws Exception {
    // Create new session to send start record
    ClientAccSession session = (ClientAccSession) accountingSessionFactory.
      getNewSession(null, ClientAccSession.class, ACCOUNTING_APPLICATION_ID,
      new Object[]{});

    // Store it in the map
    acctSessions.put(session.getSessionId(), session);
    
    // Get the account record number
    int accRecNumber = getAccountingRecordNumber(session.getSessionId(), true);
    
    sendAccountingRequest(session, getIdentifier(subscriptionId, serviceId), 
        ACCOUNTING_RECORD_TYPE_START, accRecNumber);
  }

  public void interimOfflineCharging(String subscriptionId, String serviceId, 
      String sessionId) throws Exception {
    // Fetch existing session to send interim record
    ClientAccSession session = this.acctSessions.get(sessionId);

    // Get the account record number
    int accRecNumber = getAccountingRecordNumber(session.getSessionId(), false);
    
    sendAccountingRequest(session, getIdentifier(subscriptionId, serviceId), 
        ACCOUNTING_RECORD_TYPE_INTERIM, accRecNumber);
  }

  public void stopOfflineCharging(String subscriptionId, String serviceId,
      String sessionId) throws Exception {
    // Fetch existing session  (and remove it from map) to send stop record
    ClientAccSession session = this.acctSessions.remove(sessionId);
    
    // Get the account record number (and remove)
    int accRecNumber = getAccountingRecordNumber(session.getSessionId(), false);
    this.acctRecNumberMap.remove(session.getSessionId());

    sendAccountingRequest(session, getIdentifier(subscriptionId, serviceId), 
        ACCOUNTING_RECORD_TYPE_STOP, accRecNumber);
  }

  public void eventOfflineCharging(String subscriptionId, String serviceId) 
      throws Exception {
    // Create new session to send event record
    ClientAccSession session = (ClientAccSession) accountingSessionFactory.
      getNewSession(null, ClientAccSession.class, ACCOUNTING_APPLICATION_ID, 
      new Object[]{});

    // No need to store Session or Accounting-Record-Number as it's a one-shot.

    sendAccountingRequest(session, getIdentifier(subscriptionId, serviceId), 
        ACCOUNTING_RECORD_TYPE_EVENT, 0);
  }

  // Client Acc Session Listener Implementation -------------------------------
  
  public void doAccAnswerEvent(ClientAccSession appSession, AccountRequest request, 
      AccountAnswer answer) throws InternalException, IllegalDiameterStateException,
      RouteException, OverloadException {

    // Extract interesting AVPs
    AvpSet acaAvps = answer.getMessage().getAvps();
    
    String subscriptionId = null;
    String serviceId = null;
    try {
      String username = acaAvps.getAvp(Avp.USER_NAME).getUTF8String();
      // It's in form subscription.service@REALM_NAME
      String[] identifiers = username.replaceFirst("@" + REALM_NAME, "").split("\\.");
      subscriptionId = identifiers[0];
      serviceId = identifiers[1];
    }
    catch (Exception e) {
      e.printStackTrace();
    }

    // Get the session-id value
    String sessionId = appSession.getSessionId();

    // We must be able to get this, it's mandatory
    int accRecType = -1;
    try {
      accRecType = answer.getAccountingRecordType();
    }
    catch (Exception e) {
      e.printStackTrace();
    }

    // We must be able to get this, it's mandatory
    long accRecNumber = -1L;
    try {
      accRecNumber = answer.getAccountingRecordNumber();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
    
    // If we can't get it we'll fallback to DIAMETER_UNABLE_TO_COMPLY (5012)
    long resultCode = 5012L;
    try {
      resultCode = answer.getResultCodeAvp().getUnsigned32();
    }
    catch (AvpDataException e) {
      e.printStackTrace();
    }

    // Here we fallback to 0, it means the same as omitting 
    long acctInterimInterval = 0;
    try {
      acctInterimInterval = acaAvps.getAvp(Avp.ACCT_INTERIM_INTERVAL).getUnsigned32();
    }
    catch (AvpDataException e) {
      e.printStackTrace();
    }

    // Invoke the callback to deliver the answer
    listener.offlineChargingAnswerCallback(subscriptionId, serviceId, sessionId, 
        accRecType, accRecNumber, resultCode, acctInterimInterval);
  }

  public void doOtherEvent(AppSession appSession, AppRequestEvent request, 
      AppAnswerEvent answer) throws InternalException, IllegalDiameterStateException, 
      RouteException, OverloadException {
    // We can ignore this
  }
Given the above implementation of what can be seen as the enabler for offline charging, now our Example Application should implement the following state machine using the enabler:

So let's get our hands dirty with the example application implementation, which should implement the above specified listener interface:
package org.mobicents.diameter.simulator.client;

import static org.mobicents.diameter.simulator.client.OfflineChargingClient.*;

public class ExampleApplication implements OfflineChargingClientListener  {

  // Internal Client State Machine --------------------------------------------
  private static final int STATE_IDLE                  = 0;
  private static final int STATE_START_ACR_SENT        = 2;
  private static final int STATE_START_ACA_SUCCESS     = 4;
  private static final int STATE_INTERIM_ACR_SENT      = 6;
  private static final int STATE_INTERIM_ACA_SUCCESS   = 8;
  private static final int STATE_STOP_ACR_SENT         = 10;
  private static final int STATE_STOP_ACA_SUCCESS      = 12;
  private static final int STATE_EVENT_ACR_SENT        = 14;
  private static final int STATE_EVENT_ACA_SUCCESS     = 16;
  private static final int STATE_END                   = 18;
  private static final int STATE_ERROR                 = 99;

  private int currentState = STATE_IDLE;

  public static void main(String[] args) throws Exception {
    ExampleApplication app = new ExampleApplication(new OfflineChargingClientImpl());
    app.occ.startOfflineCharging("", "");
  }

  private OfflineChargingClient occ;

  public ExampleApplication(OfflineChargingClient occ) {
    this.occ = occ;
    occ.setListener(this);
  }

  public void offlineChargingAnswerCallback(String subscriptionId, String serviceId, 
      String sessionId, int accountingRecordType, long accountingRecordNumber, 
      long resultCode, long acctInterimInterval) {
    // Handle the EVENT situation
    if(accountingRecordType == ACCOUNTING_RECORD_TYPE_EVENT) {
      if(this.currentState == STATE_EVENT_ACR_SENT) {
        if(resultCode == 2001) {
          this.currentState = STATE_EVENT_ACA_SUCCESS;
          System.out.println("(((o))) Event Offline Charging for user '"+ subscriptionId +
              "' and service '" + serviceId + "' completed! (((o)))");
          // and now just to be correct...
          this.currentState = STATE_END;          
        }
      }
      else {
        this.currentState = STATE_ERROR;
        throw new RuntimeException("Unexpected message received.");
      }
    }
    // Handle START / INTERIM / STOP situation
    else {
      switch(this.currentState) {
      // Receiving an Answer at any of these states is an error
      case STATE_IDLE:
      case STATE_EVENT_ACA_SUCCESS:
      case STATE_START_ACA_SUCCESS:
      case STATE_INTERIM_ACA_SUCCESS:
      case STATE_STOP_ACA_SUCCESS:
        // At any of these states we don't expect to receive an ACA, move to error.
        this.currentState = STATE_ERROR;
        break;
        // We've sent ACR EVENT
      case STATE_START_ACR_SENT:
        if(accountingRecordType == ACCOUNTING_RECORD_TYPE_START) {
          if(resultCode >= 2000 && resultCode < 3000) {
            // Our event charging has completed successfully. We're done!
            System.err.println("(((o))) Offline Charging for user '" + subscriptionId +
                "' and service '" + serviceId + "' started... (((o)))");

            if(acctInterimInterval > 0) {
              try {
                // Let's sleep until next interim update...
                Thread.sleep(acctInterimInterval * 1000);

                // We send an update at the correct time
                occ.interimOfflineCharging(subscriptionId, serviceId, sessionId);
              }
              catch (Exception e) {
                this.currentState = STATE_ERROR;
                throw new RuntimeException("Unable to schedule/send interim update.", e);
              }
            }
          }
          else {
            // It failed
            System.err.println("(((x))) Offline Charging for user '" + subscriptionId + 
                "' and service '" + serviceId + "' failed with Result-Code="+ resultCode +
                "! (((x)))");
          }
        }
        else {
          this.currentState = STATE_ERROR;
          throw new RuntimeException("Unexpected message received.");
        }
        break;
        // We've sent ACR START
      case STATE_INTERIM_ACR_SENT:
        if(accountingRecordType == ACCOUNTING_RECORD_TYPE_INTERIM) {
          if(resultCode >= 2000 && resultCode < 3000) {
            // Our offline charging has started successfully...
            System.out.println("(((o))) Offline Charging for user '" + subscriptionId +
                "' and service '" + serviceId + "' updated... (((o)))");

            if(acctInterimInterval > 0) {
              try {
                // Let's sleep until next interim update...
                Thread.sleep(acctInterimInterval);

                // We send an update at the correct time
                occ.interimOfflineCharging(subscriptionId, serviceId, sessionId);
              }
              catch (Exception e) {
                this.currentState = STATE_ERROR;
                throw new RuntimeException("Unable to schedule/send interim update.", e);
              }
            }
          }
          else {
            // It failed, let's warn the application
            System.out.println("(((x))) Offline Charging for user '" + subscriptionId +
                "' and service '" + serviceId + "' failed to start with Result-Code=" +
                resultCode + "! (((x)))");
          }
        }
        else {
          this.currentState = STATE_ERROR;
          throw new RuntimeException("Unexpected message received.");
        }
        break;
      case STATE_STOP_ACR_SENT:
        if(accountingRecordType == ACCOUNTING_RECORD_TYPE_INTERIM) {
          if(resultCode >= 2000 && resultCode < 3000) {
            // Our offline charging has started successfully...
            System.out.println("(((o))) Offline Charging for user '" + subscriptionId +
                "' and service '" + serviceId + "' stopped! (((o)))");
          }
          else {
            // It failed, let's warn the application
            System.out.println("(((x))) Offline Charging for user '" + subscriptionId +
                "' and service '" + serviceId + "' failed to stop with Result-Code=" +
                resultCode + "! (((x)))");
          }
        }
        else {
          this.currentState = STATE_ERROR;
          throw new RuntimeException("Unexpected message received.");
        }
        break;
      default:
        this.currentState = STATE_ERROR;
        throw new RuntimeException("Unexpected message received.");
      }
    }
  }
}
As it can be seen it turns to be really simple to implement such Application using Offline Charging in this way. The above application also provides an enhancement, which is to automatically wait and send the interim ACR updates (lines 73-85 and 107-119). That obviously is a design choice, which can be changed if control over that behavior is intended.

Conclusion
As this article was meant to demonstrate it's simple to add Offline Accounting to your applications using Mobicents Diameter solution. Several options (and scenarios) were simplified in order to keep the example easier to understand and follow the steps, as well as to give a better understanding of the Mobicents Diameter solution, while still rendering a completely functional solution.

You can learn more about Mobicents Diameter at http://www.mobicents.org/diameter/.

Installing Jopr/RHQ and Mobicents JAIN SLEE (or Diameter) Plugin

Wednesday, July 21, 2010 em 02:05
As it seems that some users don't find the task of installing and configuring Mobicents JAIN SLEE / Diameter Management consoles as plugins for Jopr/RHQ easy, I've decided to make a blog post explaining how to do it.

But if an image worths more than 1.000 words, how many words can a video worth? :) Hopefully, a lot, because I've just recorded one that I hope it helps to better understand how it should be done.

Still, I will point the main actions to be executed and the time they occur at the video, for faster reference.

Here it is, right from the Mobicents Channel @ Vimeo:

Installing Jopr/RHQ and Mobicents JAIN SLEE 2.x Plugin (click to watch in HD)

  1. [00:00] Download RHQ Server from http://www.rhq-project.org/ and Unzip rhq-server-3.0.0.zip to any directory of your choice;
  2. [00:50] In a terminal, go to "rhq-server-3.0.0/bin" inside the extracted folder and run "rhq-server.[bat|sh] console", depending on your OS;
  3. [01:30] Go to http://127.0.0.1:7080/ and install the server (don't forget to select your database);
  4. [03:15] Login with default credentials (rhqadmin/rhqadmin), go to Downloads and Download the "Agent Installer";
  5. [04:00] In a new terminal, go to the directory where the Agent Installer was downloaded and issue the command "java -jar rhq-enterprise-agent-3.0.0.jar --install" to install it;
  6. [04:35] In the same terminal go to rhq-agent/bin (where it was installed) and run "rhq-agent.[bat|sh] --clean" (use --clean to make sure you will use a fresh configuration). Default values should be fine;
  7. [05:30] In the web interface, go to Administration > System Configuration > Plugins, click "Add" and browse to the Mobicents JAIN SLEE / Diameter Plugin location and click "Upload". When done, click "Scan for Updates";
  8. [07:45] If not yet running, start yout JAIN SLEE / Diameter Server;
  9. [09:30] In the RHQ Agent terminal, issue "plugins update" to update the agent plugins (download the JAIN SLEE / Diameter plugin to the agent);
  10. [11:10] Now that the plugins are updated, and with the JAIN SLEE / Diameter Server running, in the Agent terminal issue the "discovery" command to find new resources;
  11. [11:35] In the web interface, go to Overview > Auto Discovery Queue, locate the Server, check it and click "Import"
  12. [12:45] Still at the webpage, head to Resources > Servers. Click on the server entry to manage it. Enjoy!

Simple, isn't it ? Hope it is now.. It's time to have fun, managing and monitoring Mobicents Servers via Jopr/RHQ ;-)

For details on what you can do with these plugins, please refer to the documentation found in the binaries releases of the servers.