Technology Blog Posts by Members
cancel
Showing results for 
Search instead for 
Did you mean: 
Maral_Oliveras
Active Contributor
1,747

Introduction

In my previous blog, I described how to enable the Cloud ALM Inbound Metrics API service and test it with Postman.

In this blog I explain the details on how to send a custom metric from an ABAP on-prem (or private cloud) system. For S/4 HANA Cloud or a BTP ABAP Environment you can implement a metric provider as described in the below links:

The on-prem S/4, at least until S4 2023, is missing the Fiori apps to schedule a metric provider and to setup a communication scenario. Other than that, I might explore the possibility to use the new ABAP "metric provider" object together with the program rs_gsm_co_collect to partially reuse the same setup of the S4Cloud, but this is not covered in this blog. 

Call the Cloud ALM API from ABAP

My ABAP system is an S/4 HANA 2023 FPS 01, this means I can use relatively new features like the xco_cp_json library. However, it should be possible to achieve the same results in almost any ABAP system using alternative libraries and code techniques. For example the class /ui2/cl_json provides similar functionality to the XCO one.

Disclaimer: the code in this blog doesn't include any error handling and has all the parameters hardcoded. I might upload runnable version to GitHub if I have something a cleaner and generic. I wanted to build a mini RAP framework around it and make it open source, but I'll leave it to someone else with more time.

Get the API token

  • The first step is to save the API service key authentication details as an SM59 HTTP (Type G) destination. Use the service key client ID and secret in the logon tab "Basic Authentication" fields.

Maral_Oliveras_0-1730801515309.png

  • Fetch the token string using the SM59 destination and parse it using the xco_cp_json library or any other option available in your system. I used this one because is the only released option in my S/4 2023 system.

 

method get_token.
    data: begin of ls_token,
            access_token type string,
            token_type type string,
            expires_in type i,
            scope type string,
            jti type string,
          end of ls_token.

    data(lv_calm_dest) = 'CLOUD_ALM_INBOUND_METRICS_API'.
    cl_http_client=>create_by_destination(
      exporting
        destination              = lv_calm_dest
      importing
        client                   = data(lr_http_client)
      exceptions
        others                   = 1 ).

    lr_http_client->request->set_method( method = 'POST' ).
    cl_http_utility=>set_query( request = lr_http_client->request query = 'grant_type=client_credentials' ).

    lr_http_client->send( exceptions others = 1 ).
    lr_http_client->receive( exceptions others = 1 ).
    lr_http_client->response->get_status(
      importing
        code   = data(lv_code)
        reason = data(lv_reason) ).
    data(lv_bin) = lr_http_client->response->get_data( ).
    data(lr_conv) = cl_abap_conv_in_ce=>create( input = lv_bin ).
    data lv_response type string.
    lr_conv->read( importing data = lv_response ).
    lr_http_client->close( ).
    xco_cp_json=>data->from_string( iv_string = lv_response )->write_to( REF #( ls_token ) ).
    rv_token = ls_token-access_token.
    ##TODO "It could be possible to save the token expiration to keep it in cache until it's valid
  endmethod.

 

Get the Unix timestamp nano in ABAP

The Unix timestamp is required for the gauge metric to specify the point in time.

 

  method get_timestamp_nano.
    data(lv_ts_seconds) = xco_cp=>sy->unix_timestamp( )->value.
    rv_ts_nano = lv_ts_seconds * 1000000000.
  endmethod.

 

Build parameters and headers key/value tables

Use the tihttpnvp table type of the http client to populate the parameters and headers.

 

  method get_raw_metrics_api_params.
    rt_params = value #( ( name = 'useCase'   value = 'hm' )
                         ( name = 'version'   value = 'v1' )
                         ( name = 'serviceId' value = '<YOUR system ID>' )
                         ( name = 'dev'    value = 'true' ) "set to false for prod!
                         ( name = 'format'    value = 'protobuf-json' )
                          ).
  endmethod.

  method get_calm_api_headers.
    rt_headers = value #( ( name = '~request_method'   value = if_http_request=>co_request_method_post )
                          ( name = 'Content-Type'   value = 'application/json' )
                          ( name = 'Accept' value = '*/*' )
                          ( name = 'Authorization'    value = |Bearer { get_token( ) }| ) ).
  endmethod.

 

Build the body JSON object ABAP structure

Here I used the XCO_CP_JSON released library. Since the body schema is quite complex, I asked our friend ChatGPT to define the necessary ABAP structures for me from the Postman example. And guess what, it was done correctly at the first attempt! I only had to ask it to replace CamelCase by Underscore in the fields names as a 2nd step, of course you can keep the JSON notation if you prefer.

 

TYPES: BEGIN OF ty_value,
         string_value TYPE string,
       END OF ty_value.

TYPES: BEGIN OF ty_attributes,
         key   TYPE string,
         value TYPE ty_value,
       END OF ty_attributes.

TYPES: ty_attributes_tab TYPE STANDARD TABLE OF ty_attributes WITH EMPTY KEY.

TYPES: BEGIN OF ty_data_points,
         time_unix_nano TYPE string,
         as_double     TYPE f,
         attributes   TYPE ty_attributes_tab,
       END OF ty_data_points.

TYPES: ty_data_points_tab TYPE STANDARD TABLE OF ty_data_points WITH EMPTY KEY.

TYPES: BEGIN OF ty_gauge,
         data_points TYPE ty_data_points_tab,
       END OF ty_gauge.

TYPES: BEGIN OF ty_metrics,
         name        TYPE string,
         description TYPE string,
         gauge       TYPE ty_gauge,
       END OF ty_metrics.

TYPES: ty_metrics_tab TYPE STANDARD TABLE OF ty_metrics WITH EMPTY KEY.

TYPES: BEGIN OF ty_scope,
         name    TYPE string,
         version TYPE string,
       END OF ty_scope.

TYPES: BEGIN OF ty_scope_metrics,
         scope   TYPE ty_scope,
         metrics TYPE ty_metrics_tab,
       END OF ty_scope_metrics.

TYPES: ty_scope_metrics_tab TYPE STANDARD TABLE OF ty_scope_metrics WITH EMPTY KEY.

TYPES: BEGIN OF ty_resource,
         attributes TYPE ty_attributes_tab,
       END OF ty_resource.

TYPES: BEGIN OF ty_resource_metrics,
         resource      TYPE ty_resource,
         scope_metrics  TYPE ty_scope_metrics_tab,
       END OF ty_resource_metrics.

TYPES: ty_resource_metrics_tab TYPE STANDARD TABLE OF ty_resource_metrics WITH EMPTY KEY.

TYPES: BEGIN OF ty_calm_in_metrics_body,
         resource_metrics TYPE ty_resource_metrics_tab,
       END OF ty_calm_in_metrics_body.

 

Fill the metrics body structure

You just need to declare the body structure and fill the necessary fields. Normally you would have multiple "metrics" records and multiple "data points" for the metrics that require it. But in this example I only write one metric (SM13 errors) and 1 data point (client 300):

 

data lv_body_abap TYPE ty_calm_in_metrics_body.
data lv_sm13_errors_300 type int8 value '8.0'.

data(lv_resource_metrics) = VALUE ty_resource_metrics(
  resource = VALUE ty_resource(
    attributes = VALUE ty_attributes_tab( (
      key = 'service.name'
      value = VALUE ty_value( string_value = 'S4D300' )
    ) )
  )
  scope_metrics = VALUE ty_scope_metrics_tab( ( 
    scope = VALUE ty_scope(
      name = 'HM_ANALYTICS_DP'
      version = 'V1'
    )
    metrics = VALUE ty_metrics_tab( (
      name        = 'Custom.SM13.Errors'
      description = 'SM13 Update Requests with Errors'
      gauge = VALUE ty_gauge(
        data_points = VALUE ty_data_points_tab( (
          time_unix_nano = lv_ts_nano_str
          as_double     = lv_sm13_errors_300
          attributes   = VALUE ty_attributes_tab( (
            key   = 'Client'
            value = VALUE ty_value( string_value = '300' )
          ) ( 
            key   = 'Instance'
            value = VALUE ty_value( string_value = 's4d-pas-00' )
          ) (
            key   = 'Sid'
            value = VALUE ty_value( string_value = 'S4D' )
          ) )
        ) )
      )
    ) )
  ) )
).

lv_body_abap-resource_metrics = VALUE ty_resource_metrics_tab( ( lv_resource_metrics ) ).

 

Build the JSON body

Now you only need to transform the ABAP structure into a JSON object. Since I used underscores for the ABAP field names, now I have to transform to camelCase. I recommend comparing the JSON string from Postman with the one generated with the ABAP code to validate the result.

 

data(lv_body_json) = xco_cp_json=>data->from_abap( ia_abap = lv_resource_metrics )->apply( 
  VALUE #( ( xco_cp_json=>transformation->underscore_to_camel_case ) ) )->to_string( ).

 

Call the API

Finally you only need to call the API with the HTTP client and read the response. If you set the "dev" parameter to "true" you can get more details in case of issues.

 

  method if_oo_adt_classrun~main.
    cl_http_client=>create_by_url(
      exporting
        url                = |https://eu10.alm.cloud.sap/api/calm-metrics/v1/metrics|
      importing
        client             = data(lo_http_client)
      exceptions
        others             = 1 ).
    if sy-subrc <> 0.
      "error handling
    endif.

    "setting request method, parameters and headers
    lo_http_client->request->set_method( if_http_request=>co_request_method_post ).
    lo_http_client->request->set_form_fields( fields = get_raw_metrics_api_params( ) ).
    lo_http_client->request->set_header_fields( fields = get_calm_api_headers( ) ).

    lo_http_client->request->set_cdata( exporting data = lv_body ).
    lo_http_client->send( exceptions others = 1 ).
    if sy-subrc <> 0.
      "error handling
    else.
      lo_http_client->receive(
        exceptions
          others                     = 1 ).
    endif.
    data(lv_response) = lo_http_client->response->get_cdata( ).
    lo_http_client->close( ).
    out->write( |response: { lv_response }| ).
  endmethod.

 

Run the test class and validate the result in Cloud ALM

Just build a class implementing the if_oo_dat_classrun interface and test the code.

Maral_Oliveras_0-1731604847293.png