Making an Asterisk Manager Interface monitor using PHP, PAMI, and Ding
NOTE: If you are just looking for an introduction to ami and PAMI, take a look at: this.
Here, I'll write about AsterTrace (https://github.com/marcelog/AsterTrace), a
simple project that will help you get started using the asterisk manager interface in extremely few lines of code :)
The goal for AsterTrace is to log into ami and capture every event sent by asterisk, process some (or not) and maybe log every one of them to some database.
AsterTrace uses Ding and PAMI. The first one as the container, and latter to communicate with AMI, so I'll talk about how AsterTrace uses them both, and how this will help you here and in other applications to get a more decoupled code, very easy to mantain and scale.
A similar article with Ding and Doctrine2 as ORM is available here.
You can get AsterTrace directly from github:
- SVN: http://svn.github.com/marcelog/AsterTrace.git
- GIT: git://github.com/marcelog/AsterTrace.git
That's it :) Now read on to see how it works.
What is what and how it works
To sum up, this is the general idea:
- Use Ding as Inversion of Control and Dependency Injection container. All we're going to do are just beans, plain old php objects.
- Use PAMI as the framework to handle the connection to the Asterisk Manager Interface.
- Use Ding's PamiHelper so we dont need to deal with pami use details. Our main program will be just a bean.
- Use Ding's PropertiesHolder so we can configure mysql, ami, php, etc, from properties file (aka php INI files).
- Use Ding's TCPServerHelper to open a tcp server where you can connect via telnet or via the language of your choice, to listen for events serialized using json. You can also send commands and get the responses in json format.
- Our main program (called from PamiHelper) will use Ding's events to dispatch events coming in from AMI, to our "event listeners".
- A REST interface is also provided, so this software can be used from a web environment.
- Every event listener will be just a bean, that will get called by the container whenever an event is dispatched from our main program.
- We are going to log events to a database. I chose to use mysql, via pdo. This PDO object is a bean.
- To actually write to the database, we'll use prepared statements.
- Each prepared statement will be a "bean", and will get injected to our event listeners. These beans are instantiated by ding using the factory-method and factory-bean options.
- We need to log to files the normal application stuff, like debug info, errors, etc. We'll use ding's ILoggerAware interface in our beans, giving them direct access to the logger of the container (this needs log4php, so may want to install it before proceeding).
- Handle errors through Ding error handler helper
- Handle signals through Ding signal handler helper
- Handle shutdown through Ding shutdown handler helper
Application tree layout overview. What's where.
src/mg/AsterTrace ==> Main source directory ServerHandlers ==> Server Handlers reside here. These will listen for events triggered from the tcp server. ServerHandler.php ==> A base class for all tcp command listeners. ServerCommandDTO.php ==> DTO to be used when dispatching events from the TCP server. The handlers will get this as the argument of the event. CoreShowChannels.php ==> Event handler when a tcp client issues CoreShowChannels EventHandlers ==> Event Handlers reside here. These are our listening beans. DialListener.php DtmfListener.php EventListener.php PDOListener.php VarSetListener.php NewChannelListener.php NewStateListener.php NewExtenListener.php Handlers ErrorHandler.php ==> Application error handler. PamiHandler.php ==> Our pami handler (will receive all events issued by the PamiHelper). ShutdownHandler.php ==> Application shutdown handler. SignalHandler.php ==> Application signal handler (so you may shutdown everything with kill or ^C). TCPServerHandler.php ==> The TCP Server. This will get called from the TCPServerHelper from ding. In turn, this will dispatch events according to what each client sent as a command. bin astertrace.php ==> Main entry point for the application (see below). bootstrap.php ==> Bootstrap code (see below). rest.php ==> REST interface. Use it as a webservice (see below). conf/ astertrace.properties.example ==> AMI connection information. Rename to astertrace.properties and edit. log4php.properties.example ==> log4php configuration to log errors, debug, etc. Rename to log4php.properties (and log4php-rest.properties to use it with the rest interface)and edit. mysql.properties.example ==> mysql configuration (host, tables, database, etc). Rename to mysql.properties and edit. php.properties.example ==> php properties, like display_errors, error_reporting, etc. Rename to php.properties and edit. server.properties.example ==> TCP Server properties. Rename to server.properties and edit. support ==> Bean definitions are here. pdo-mysql.xml ==> PDO related beans. cli.xml ==> Main bean definitions for the CLI interface. handlers.xml ==> Beans for error handler, signal handler, shutdown handler, pami, etc. server-handlers.xml ==> Each bean defined here will handle a command (action) from a TCP Client. rest.xml ==> Main bean definitions for the REST interface. event-handlers.xml ==> Where to include the actual listeners to be run (CLI). event-handlers-rest.xml ==> Where to include the actual listeners to be run (REST). event-handlers dial-pdo-mysql-statements.xml ==> Prepared statements for dial listener. dial.xml ==> dial listener. dtmf-pdo-mysql-statements.xml ==> Prepared statements for dtmf listener. dtmf.xml ==> dtmf listener. event-pdo-mysql-statements.xml ==> Prepared statements for generic events listener. event.xml ==> Generic events listener. rest-event.xml ==> Generic events listener (REST). varset-pdo-mysql-statements.xml ==> Prepared statements for varset listener. varset.xml ==> Varset listener. newchannel-pdo-mysql-statements.xml ==> Prepared statements for newchannel listener. newchannel.xml ==> Newchannel listener. newstate-pdo-mysql-statements.xml ==> Prepared statements for newstate listener. newstate.xml ==> Newstate listener. newexten-pdo-mysql-statements.xml ==> Prepared statements for newexten listener. newexten.xml ==> Newexten listener. server-handlers ==> Beans that handle TCPClient actions. coreShowChannels.xml ==> Handler for a TCP Client issuing a CoreShowChannels. handlers ==> Several "handlers" beans (error, signal, etc). error-handler.xml pami-handler.xml shutdown-handler.xml signal-handler.xml server.xml
The key here, are the xml files, which are the container configuration. This is where all the beans are defined and related to each other. Because of this configuration, our beans will be injected with everything they need to work (even a logger :))
Main entry point
The main entry point for AsterTrace is astertrace.php. So you can start it by running it:
php src/mg/AsterTrace/bin/astertrace.php ./conf
This script will call the bootstrap.php file that resides in the same directory. Boostrap.php will:
- Setup the include path needed
- Check the invoking arguments (remember, if this is a command line application, you need one argument which is the config directory where all configuration files reside, typically, ./conf)
- If on a web environment, check the environment variable "CONFIG_DIR" to get the application's config directory.
- Setup the ding's container configuration
After this, astertrace.php will continue execution:
- The ding container will be instantiated using the configuration that bootstrap.php prepared.
- The container will use the cli.xml file as its configuration (if cli environment), or the rest.xml (if on web environment).
- Either beans file uses the PropertiesHolder to make ding load properties from php.properties, mysql.properties, astertrace.properties. So the correct values are injected in any beans that need them.
- Either beans file includes other bean configurations, for PDO, event listeners, error handler, signal handler, and shutdown handler, so all of them are available in the application.
- The bean pamiHelper is requested to the container. This bean is actually the PamiHelper that comes from ding. The container will also create the bean pami which is our own event handler.
- An infinite loop is started, calling the process() method of the PamiHelper. This method will read messages incoming from AMI.
And that is pretty much all we have to do :) the container takes care of the rest:
- Whenever an event is received by PAMI, it will call the PamiHelper.
- The PamiHelper will call our own handler.
- Our handler will dispatch an event through the container, named "anyEvent", so any beans listening for this event will execute.
- Based on the name of the event, the handler dispatches another event through the container. For example, if the event was Dial, then the "dial" event is dispatched, and the method onDial() of every listening bean (for that event) is executed.
- Make a telnet connection to the address:port configured in server.properties. You should see the events coming in from ami in json format :)
- Setup your web server so you can access rest.php. Remember to setup the environment variable CONFIG_DIR to point to the application environment. When requesting rest.php, you should start seeing events in json format coming in from ami.
Currently available listeners
- Code: src/mg/AsterTrace/EventHandlers/EventListener.php
- Bean: eventListener
- Listens-on: anyEvent
- Bean definition: conf/support/event-listeners/event.xml
- Statements: conf/support/event-listeners/event-pdo-mysql-statements.xml
- Behaviour: Log all events to the mysql. The events are saved by serializing the event object that pami delivers, which is a subclass of EventMessage. This will serialize everything about the incoming event.
- Event handler method: onAnyEvent
- Code: src/mg/AsterTrace/EventHandlers/VarSetListener.php
- Bean: varSetListener
- Listens-on: varSet
- Bean definition: conf/support/event-listeners/varset.xml
- Statements: conf/support/varset-listeners/varset-pdo-mysql-statements.xml
- Behaviour: Log all variables that are set to database. Each row will contain the uniqueid, the variable name, and the value set.
- Event handler method: onVarSet
- Code: src/mg/AsterTrace/EventHandlers/DialListener.php
- Bean: dialListener
- Listens-on: dial
- Bean definition: conf/support/event-listeners/dial.xml
- Statements: conf/support/event-listeners/dial-pdo-mysql-statements.xml
- Behaviour: Will capture Dial (SubEvent Begin), Dial (SubEvent End), Hangup, and VarSet (for the variables ANSWEREDTIME and DIALEDTIME). This will save the new call on DialBegin, and save the status of the call on Hangup. The VarSets then will update the answered and dialed times. This will effectively give you a working CDR :)
- Event handler method: onDial, onHangup, onVarSet
- Code: src/mg/AsterTrace/EventHandlers/DtmfListener.php
- Bean: dtmfListener
- Listens-on: dTMF
- Bean definition: conf/support/event-listeners/dtmf.xml
- Statements: conf/support/event-listeners/dtmf-pdo-mysql-statements.xml
- Behaviour: Will capture all dtmf events and execute an insert or update operation in its table in the following way: If the uniqueid does not exist, the uniqueid and the dtmf digit will be saved. Otherwise, the uniqueid row is updated, concatenating the new dtmf digit after the last one. This will effectively give you all the dtmf's digits pressed by every uniqueid channel.
- Event handler method: onDTMF
- Code: src/mg/AsterTrace/EventHandlers/NewChannelListener.php
- Bean: newChannelListener
- Listens-on: newchannel
- Bean definition: conf/support/event-listeners/newchannel.xml
- Statements: conf/support/event-listeners/newchannel-pdo-mysql-statements.xml
- Behaviour: Will capture all newchannel events. You can see when a channel is ringing, or reserved, etc.
- Event handler method: onNewchannel
- Code: src/mg/AsterTrace/EventHandlers/NewStateListener.php
- Bean: newStateListener
- Listens-on: newstate
- Bean definition: conf/support/event-listeners/newstate.xml
- Statements: conf/support/event-listeners/newstate-pdo-mysql-statements.xml
- Behaviour: Will capture all newstate events. You can see when a channel is ringing, up, down, etc.
- Event handler method: onNewstate
- Code: src/mg/AsterTrace/EventHandlers/NewExtenListener.php
- Bean: newExtenListener
- Listens-on: newexten
- Bean definition: conf/support/event-listeners/newexten.xml
- Statements: conf/support/event-listeners/newexten-pdo-mysql-statements.xml
- Behaviour: Will capture all newexten events, so you can see how channels go through the dialplan, step by step.
- Event handler method: onNewexten
Extending the application
We can add more beans to extend this functionality. Adding beans is trivial, we could use annotations, yaml, or xml configurations.
Whenever we want to get a specific event, we just need to define a listens-on directive in the bean configuration, and the container will automagically call the method onEventName() with the event received :) So we can propagate events via radius, or activemq, or rabbitmq, etc.
As you see, having Ding and PAMI work together is a pretty smooth task, and will benefit your code by making it cleaner and less coupled. You will be able to focus on the task you really want to do. Ding will take care of the inversion of control and dependeny injection, while PAMI will take care of the managing the AMI protocol. In this way, your application is truly event driven, and everything will get called without you having to worry about how or when. Even configuring the application is trivial due to the use of the PropertiesHolder.
Everything is a bean. The PDO object, the PDO statements, and error/shutdown/signal handlers, our own event handlers, etc. This allows the application to be easily extended (just write new beans that "listens-on" different events, or event the same). Everything can be done within extremely few lines of code :)