OML Client Programming
General Documentation
You can add support for OML reporting into your C and C++ applications by doing the following.
- Include the oml2/omlc.h header file in your source file: #include <oml2/omlc.h>
- Call omlc_init(), passing in the command line arguments from main(), before doing any option processing in the rest of the code (the command line will be cleaned-up).
- Register measurement points
- Call omlc_start() to start the measurement collection process. At this point, interval-based measurement streams begin sampling.
- Call injector functions whenever you want to record a measurement sample.
- Call omlc_close() when your program has finished all measurement collection activities.
- Compile and link your program against the liboml2 library.
The measurement points registration, as well as the injector functions are generated automatically by oml2-scaffold. We still describe the use of omlc_add_mp() and omlc_inject() towards the end of this documentation, as they are still used under the hood, but it is not recommended to use them directly.
The oml2-scaffold automates the writing of repetitive code, provides better type-checking, is less error-prone and is more robust to API-changes. As an added bonus, it can also provide the skeleton for a new application (--main), its Makefile (--make) and option-parsing code (--opts).
For a simple working example of the process and its results, see the sine generator in the source (source:example/liboml2).
Initializing OML: omlc_init()¶
The first step in using OML is to initialize the client library by calling omlc_init(). This function takes four arguments:
int omlc_init (const char* name, int* argc_ptr, const char** argv, o_log_fn logger)
The last argument is for custom logging and can safely be set to NULL for most applications (OML uses its own internal logging functions in that case, reporting messages to stderr).
The first argument is the name of the application, for example "MyOmlClient". The second and third arguments should come from the argc and argv arguments to the C main() function. OML recognizes a set of command line arguments for configuring its internal operation, and the omlc_init() function process the command line, reading options that are relevant to OML, which all start with an "--oml" prefix. omlc_init() also removes its own arguments from the command line argument vector, and sets argc equal to the number of remaining arguments. This means that you can then go on to process command line arguments that are relevant to your own application, without having OML's arguments polluting your argv array. Here is a typical call to omlc_init():
#include <oml2/omlc.h> int main(int argc, const char** argv) { int result = omlc_init ("MyOmlClient", &argc, argv, NULL); if (result == -1) { fprintf (stderr, "Could not initialize OML\n"); exit (1); } /* Do application stuff... */ return 0; }
The omlc_init() function returns 0 on success and -1 on failure. Reasons for a failure will appear in the OML log file (this is true of all functions in the OML API).
Registering measurement points: oml2-scaffold(1)'s oml_register_mps()
Measurement points obviously vary depending on the application. While OML has low-level functions to declare and register measurement points, it is easier to use a higher-level language to declare them in a meaningful human-readable way. For historic reasons (compatibility with OMF), we chose to use a DSL based on Ruby, but it is simple enough that no prior knowledge of the language is required. This file is manipulated with oml2-scaffold.
This description file consists of three parts:
- Application metadata (name, version, description; defApplication);
- (Optional) application command-line options (defProperty);
- Measurement points definition (defMeasurement).
A template file can be generated with
$ oml2-scaffold -a APPNAME # creates APPNAME.rb
Here, we only focus on the defMeasurement commands. The full DSL is documented in oml2-scaffold.
a.defMeasurement(MPNAME) do |m| m.defMetric(FIELDNAME, 'FIELDTYPE') end
This snippet defines a Measurement Point (MP) called MPNAME, with one field named FIELDNAME of type FIELDTYPE). You can have as many MP as you want, and each MP can contain a maximum of 64 FIELDS. Field types are documented in the OML generic interface, but the most common are
- (u)int32 and (u)int64 for integers;
- double for double-precision numbers;
- string for nul-terminated character strings;
- blob for arbitrary binary data.
Once the APPNAME.rb has been edited to accurately list the desired measurement points, the C code can be generated. It takes the form of a header.
oml2-scaffold --oml APPNAME.rb Created 'APPNAME_oml.h'
This header declares several functions, including the MP-registration code. You can then include the new header in the application code, and register all the MPs with the oml_register_mps() function. Internally, this function uses omlc_add_mp(), but you're much better off using this wrapper. For this function to be defined, it is required to #define OML_FROM_MAIN in one and only one source file of your application prior to #inclue'ing <oml2/omlc.h>. This instructs the headers to actually define the oml_register_mps() helper, as well as some necessary storage variables, rather than just declaring them as extern.
#include <oml2/omlc.h> #define OML_FROM_MAIN /* IMPORTANT: instruct APPNAME_oml.h to enable the generated code, rather than just declaring prototypes; do this in only one source file */ #include <APPNAME_oml.h> int main(int argc, const char** argv) { int result = omlc_init ("MyOmlClient", &argc, argv, NULL); if (result == -1) { fprintf (stderr, "Could not initialize OML\n"); exit (1); } oml_register_mps(); /* Do application stuff. From this point on we can safely call omlc_inject() */ /* NOTE: if you used oml2-scaffold to generate the main program, it will contain a function "run" which does an omlc_inject_YourData() */ return 0; }
Not declaring OML_FROM_MAIN would result in errors such as
SRC.c:LINE:COL: error: implicit declaration of function âÂÂoml_register_mpsâ [-Werror=implicit-function-declaration]
or
SRC.c:(.text+0x43a): undefined reference to `oml_register_mps' SRC.c:(.text+0x446): undefined reference to `g_oml_mps_generator'
while declaring it in more than one file would lead to the linker complaining that
SRC2.o:(.data+0x0): multiple definition of `g_opts' SRC.o:(.data+0x0): first defined here SRC2.o:(.data+0x20): multiple definition of `options' SRC.o:(.data+0x20): first defined here SRC2.o:(.data+0x1c0): multiple definition of `g_oml_mps_generator' SRC.o:(.data+0x1c0): first defined here
Note: It is a good idea to put the creation of the APPNAME_oml.h file as part of the software-building process (e.g. in a Makefile) rather than keep the file static. This will provide more adpatability to future versions of OML. The APPNAME.rb file, however, can be safely checked in.
Starting OML measurement collection: omlc_start()
Once you have defined all of your MP's, you should call omlc_start(). This sets up the filter configuration for each measurement point and for each data destination, which is specified by the user of your application at runtime, either using the command line options or using the XML configuration file (for details about this, see Configuring OML Client Applications). For example:
#include <oml2/omlc.h> #define OML_FROM_MAIN #include <APPNAME_oml.h> int main(int argc, const char** argv) { int result = omlc_init ("MyOmlClient", &argc, argv, NULL); if (result == -1) { fprintf (stderr, "Could not initialize OML\n"); exit (1); } oml_register_mps(); result = omlc_start(); if (result == -1) { fprintf (stderr, "Error starting up OML measurement streams\n"); exit (1); } /* Do application stuff. From this point on we can safely call omlc_inject() */ return 0; }
If there was some problem with the configuration, omlc_start() will return -1, indicating a failure. If omlc_start() fails, no measurement can be performed, because the necessary filters and data destinations will not have been set up correctly. Any subsequent calls to omlc_inject() will be ignored by the OML client library in that case, however your application will continue to run. If omlc_start() returns 0, then no error occurred and all subsequent calls to omlc_inject() will successfully record measurements.
Recording measurements: oml2-scaffold(1)'s oml_inject_MPNAME()
As part of the generated code, oml2-scaffold, has also written injection helpers for each MP you defined in your APPNAME.rb. They are named oml_inject_MPNAME, and have as many arguments as the number of fields in the MP, plus one. An exception is for blobs, where two arguments (a pointer to the blob data, and its size) are added.
You can simply call these functions whenever there is data you want to report in a specific MP. The generic prototype of the injection helper is
void oml_inject_MPNAME(OmlMP* mp, FIELDCTYPE FIELDNAME, ... )
where mp is a variable, declared in APPNAME_oml.h and initialised by oml_register_mps() which relates to a specific measurement point. Their naming is regular: g_oml_mps_APPNAME->MPNAME. Then each field follow, in order, with the C-type matching their OML type. To inject in the previously discussed example MPNAME, the call would therefore be
oml_inject_MPNAME(g_oml_mps_APPNAME->MPNAME, (int32_t) v);
which assumes that the FIELDNAME mentionned above had a FIELDTYPE which is expressed as an int32_t in C. Please note again, blobs injection requires two fields: void* BLOBMPNAME, ssize_t BLOBMPNAME_len.
Closing OML: omlc_close()
Once you have finished all measurement activities you can call omlc_close() to shut down the measurement system. You should do this before exiting your application. It ensures that all measurement streams are flushed and all measurements are recorded (or at least sent to the file or server). If omlc_close() is not called before exit then some measurements might be lost.
The OML client library installs a signal handler to handle a user quit signal (SIGINT, sent when you press Control-C on the command line) and to detect server disconnection and handle it gracefully. In the case of a SIGINT signal, the signal handler calls omlc_close() before exiting the application.
Compiling your OML application¶
Once you have written your OML application code, it must be compiled and linked against the OML client library, liboml2. On unix systems with gcc, you can do this as follows:
$ gcc -c my-oml-client.c $ gcc -o my-oml-client my-oml-client.o -loml2
Adding measurement points: omlc_add_mp() (DEPRECATED)
This still mostly works, but is no longer guaranteed, as it is much more brittle and error-prone than using oml2-scaffold
Once your application has initialized the OML client library, you should create some measurement points. A measurement point (MP) is an application-defined input port for recording measurements. An application "injects" measurements into the MP, and the client library registers them, applies filters to them, then sends them to an output stream, either a file or a network socket connected to an oml2-server.
An MP accepts n-tuples as inputs. That is, each measurement is not just one number, but can be a group of related measurement items. For instance, a measurement might represent all the available information about a received packet, such as its source and destination, its length, and what protocol it carries. Integers, floating point numbers, and short strings can be represented (and we have plans to add new data types in the near future).
The description of an MP looks like this in C code:
OmlMPDef mp_def [] = { { "source", OML_LONG_VALUE }, { "destination", OML_LONG_VALUE }, { "length", OML_LONG_VALUE }, { "protocol", OML_STRING_VALUE }, { NULL, (OmlValueT)0 } };
As you can see, the MP definition is represented as an array of OmlMPDef structures. The first member of each struct is the name, as a string. The second is the type of that element of the n-tuple. Not shown in this example is the OML_DOUBLE_VALUE type, for floating point values.
The last element of the array is a sentinel that marks the end of the MP definition. It must always be included as shown.
To register this MP with OML, you must call the omlc_add_mp() function, like this:
OmlMP* mp = omlc_add_mp ("packet_info", mp_def); if (mp == NULL) { fprintf (stderr, "Error: could not register Measurement Point \"packet_info\""); exit (1); }
Here we have given the MP the name "packet_info". This is used in the schema for the measurement output, and in database table column names when the measurements are sent to an OML server. Again, if omlc_add_mp() fails for some reason, it signals an error by returning NULL, and your application should check for this case.
If omlc_add_mp() succeeds, then it returns a pointer to an OmlMP object, which you should treat as an opaque handle representing your particular MP, which is used in subsequent calls to omlc_inject() (see below). You can define multiple MP's in an application by calling omlc_add_mp() multiple times. Each time it is called, it will return a pointer to a different, unique OmlMP object.
Recording measurements: omlc_inject() (DEPRECATED)
This still mostly works, but is no longer guaranteed, as it is much more brittle and error-prone than using oml2-scaffold
If omlc_start() is successful, you can now call omlc_inject() to record your application's measurements. The protoype for omlc_inject() is:
void omlc_inject (OmlMP *mp, OmlValueU *values);
The function accepts an OmlMP* as its first argument -- this is the MP into which the measurement will be injected. It should be obtained from a call to omlc_add_mp(), see above. The second argument is an array of OmlValueU objects. An OmlValueU is an untyped representation of a value. Each element of the array should have the same type as the corresponding element of the OmlMPDef used to create the MP.
OmlValueU is implemented as a union, but it should not be manipulated directly. Instead, OML provides the following macros to set up the OmlValueU array:
omlc_set_long(var, value); omlc_set_double(var, value); omlc_set_string(var, value); omlc_set_const_string(var, value);
Here is an example of how to inject a measurement value into an MP. The MP in the example uses the same definition as the example shown for the omlc_add_mp() function above.
long source_id; long destination_id; long packet_length; char *protocol; ... /* Some application-specific code to obtain values for the variables above */ ... OmlValueU values[4]; omlc_zero_array(values, 4); /* Make sure all internal pointers are initially NULL to avoid segmentation faults and weird errors, see OmlValueU(3) */ omlc_set_long (values[0], source_id); omlc_set_long (values[1], destination_id); omlc_set_long (values[2], packet_length); omlc_set_string (values[3], protocol); omlc_inject (mp, values); omlc_reset_string(values[3]); /* Free allocated memory (see also omlc_reset_blob(3)) */
The function omlc_inject() does not return a value. If OML is not configured properly then it fails silently without recording any measurements. This allows your application to continue working even if there is a problem with OML.
If you call omlc_inject() before calling omlc_start() or after calling omlc_close(), then the call will also be ignored and no measurements will be recorded.