ballerina-guides/open-api-based-service

Name: open-api-based-service

Owner: ballerina-guides

Description: null

Created: 2018-03-01 09:16:24.0

Updated: 2018-04-30 13:28:44.0

Pushed: 2018-04-30 13:28:42.0

Homepage: null

Size: 344

Language: Ballerina

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

Build Status

Swagger / OpenAPI

OpenAPI Specification (formerly called the Swagger Specification) is a specification that creates RESTful contract for APIs, detailing all of its resources and operations in a human and machine-readable format for easy development, discovery, and integration. The Swagger to Ballerina Code Generator can take existing Swagger definition files and generate Ballerina services from them.

This guide walks you through building a RESTful Ballerina web service using Swagger / OpenAPI Specification.

The following are the sections available in this guide.

What you'll build

You'll build an RESTful web service using an OpenAPI / Swagger specification. This guide will walk you through building a pet store server from an OpenAPI / Swagger definition. The pet store service will have RESTful POST,PUT,GET and DELETE methods to handle pet data.

  alt text  

Prerequisites
Optional requirements
Implementation

If you want to skip the basics, you can download the git repo and directly move to the “Testing” section by skipping “Implementation” section.

Understand the Swagger / OpenAPI specification

The scenario that we use throughout this guide will base on a petstore.json swagger specification. The OpenAPI / Swagger specification contains all the required details about the pet store RESTful API. Additionally, You can use the Swagger view in Ballerina Composer to create and edit OpenAPI / Swagger files.

Basic structure of petstore Swagger/OpenAPI specification

wagger": "2.0",
nfo": {
"title": "Ballerina Petstore",
"description": "This is a sample Petstore server.",
"version": "1.0.0"

ost": "localhost:9090",
asePath": "/v1",
chemes": [
"http"

aths": {
"/pet": {
  "post": {
    "summary": "Add a new pet to the store",
    "description": "Optional extended description in Markdown.",
    "produces": [
      "application/json"
    ],
    "responses": {
      "200": {
        "description": "OK"
      }
    }
  }
}


NOTE : The above OpenAPI / Swagger definition is only the basic structure. You can find the complete OpenAPI / Swagger definition in petstore.json file.

Create the project structure

Ballerina is a complete programming language that can have any custom project structure that you wish. Although the language allows you to have any package structure, use the following project structure for this project to follow this guide.

-api-based-service
? guide
 ??? petstore.json  
Genarate the web service from the Swagger / OpenAPI definition

Ballerina language is capable of understanding the Swagger / OpenAPI specifications. You can easily generate the web service just by typing the following command structure in the terminal.

erina swagger mock <swaggerFile> [-o <output directory name>] [-p <package name>] 

For our pet store service you need to run the following command from the /guide in sample root directory(location where you have the petstore.json file) to generate the Ballerina service from the OpenAPI / Swagger definition

llerina swagger mock petstore.json -p petstore

The -p flag indicates the package name and -o flag indicates the file destination for the web service. These parameters are optional and can be used to have a customized package name and file location for the project.

Generated ballerina packages

After running the above command, the pet store web service will be auto-generated. You should now see a package structure similar to the following,

open-api-based-service
??? guide
    ??? petstore
    ?   ??? ballerina_petstore_impl.bal
    ?   ??? gen
    ?   ?   ??? ballerina_petstore.bal
    ?   ?   ??? schema.bal
    ?   ??? tests
    ?       ??? ballerina_petstore_test.bal
    ??? petstore.json

ballerina_petstore.bal is the generated Ballerina code of the pet store service and ballerina_petstore_impl.bal is the generated mock implementation for the pet store functions.

Generated ballerina_petstore.bal file
rt ballerina/log;
rt ballerina/http;
rt ballerina/swagger;

oint http:Listener ep0 { 
host: "localhost",
port: 9090


gger:ServiceInfo { 
title: "Ballerina Petstore",
description: "This is a sample Petstore server.",
serviceVersion: "1.0.0",
termsOfService: "http://ballerina.io/terms/",
contact: {name: "", email: "samples@ballerina.io", url: ""},
license: {name: "Apache 2.0", url: "http://www.apache.org/licenses/LICENSE-2.0.html"},
tags: [
    {name: "pet", description: "Everything about your Pets", externalDocs:
    { description: "Find out more", url: "http://ballerina.io" } }
],
externalDocs: { description: "Find out about Ballerina", url: "http://ballerina.io" },
security: [
]

p:ServiceConfig {
basePath: "/v1"

ice BallerinaPetstore bind ep0 {

@swagger:ResourceInfo {
    tags: ["pet"],
    summary: "Update an existing pet",
    description: "",
    externalDocs: {  },
    parameters: [
    ]
}
@http:ResourceConfig { 
    methods:["PUT"],
    path:"/pet"
}
updatePet (endpoint outboundEp, http:Request req) {
    http:Response res = updatePet(req);
    outboundEp->respond(res) but { error e => log:printError("Error while responding",
        err = e) };
}

@swagger:ResourceInfo {
    tags: ["pet"],
    summary: "Add a new pet to the store",
    description: "",
    externalDocs: {  },
    parameters: [
    ]
}
@http:ResourceConfig { 
    methods:["POST"],
    path:"/pet"
}
addPet (endpoint outboundEp, http:Request req) {
    http:Response res = addPet(req);
    outboundEp->respond(res) but { error e => log:printError("Error while responding",
        err = e) };
}

@swagger:ResourceInfo {
    tags: ["pet"],
    summary: "Find pet by ID",
    description: "Returns a single pet",
    externalDocs: {  },
    parameters: [
        {
            name: "petId",
            inInfo: "path",
            description: "ID of pet to return", 
            required: true, 
            allowEmptyValue: ""
        }
    ]
}
@http:ResourceConfig { 
    methods:["GET"],
    path:"/pet/{petId}"
}
getPetById (endpoint outboundEp, http:Request req, int petId) {
    http:Response res = getPetById(req, petId);
    outboundEp->respond(res) but { error e => log:printError("Error while responding",
        err = e) };
}

@swagger:ResourceInfo {
    tags: ["pet"],
    summary: "Deletes a pet",
    description: "",
    externalDocs: {  },
    parameters: [
        {
            name: "petId",
            inInfo: "path",
            description: "Pet id to delete", 
            required: true, 
            allowEmptyValue: ""
        }
    ]
}
@http:ResourceConfig { 
    methods:["DELETE"],
    path:"/pet/{petId}"
}
deletePet (endpoint outboundEp, http:Request req, int petId) {
    http:Response res = deletePet(req, petId);
    outboundEp->respond(res) but { error e => log:printError("Error while responding",
        err = e) };
}


Next we need to implement the business logic in the ballerina_petstore_impl.bal file.

Implement the business logic for petstore

Now you have the Ballerina web service for the give petstore.json Swagger file. Then you need to implement the business logic for functionality of each resource. The Ballerina Swagger generator has generated ballerina_petstore_impl.bal file inside the open-api-based-service/guide/petstore. You need to fill the ballerina_petstore_impl.bal as per your requirement. For simplicity, we will use an in-memory map to store the pet data. The following code is the completed pet store web service implementation.

rt ballerina/http;
rt ballerina/mime;

petData;

ic function addPet(http:Request req) returns http:Response {

// Initialize the http response message
http:Response resp;
// Retrieve the data about pets from the json payload of the request
var reqesetPayloadData = req.getJsonPayload();
// Match the json payload with json and errors
match reqesetPayloadData {
    // If the req.getJsonPayload() returns JSON
    json petDataJson => {
        // Transform into Pet data structure
        Pet petDetails = check <Pet>petDataJson;
        if (petDetails.id == "") {
            // Send bad request message if request doesn't contain valid pet id
            resp.setTextPayload("Error : Please provide the json payload with `id`,
            `catogery` and `name`");
            // set the response code as 400 to indicate a bad request
            resp.statusCode = 400;
        }
        else {
            // Add the pet details into the in memory map
            petData[petDetails.id] = petDetails;
            // Send back the status message back to the client
            string payload = "Pet added successfully : Pet ID = " + petDetails.id;
            resp.setTextPayload(payload);
        }
    }
    error => {
        // Send bad request message if request doesn't contain valid pet data
        resp.setTextPayload("Error : Please provide the json payload with `id`,
        `catogery` and `name`");
        // set the response code as 400 to indicate a bad request
        resp.statusCode = 400;
    }
}
return resp;


ic function updatePet(http:Request req) returns http:Response {

// Initialize the http response message
http:Response resp;
// Retrieve the data about pets from the json payload of the request
var reqesetPayloadData = req.getJsonPayload();
// Match the json payload with json and errors
match reqesetPayloadData {
    // If the req.getJsonPayload() returns JSON
    json petDataJson => {
        // Transform into Pet data structure
        Pet petDetails = check <Pet>petDataJson;
        if (petDetails.id == "" || !petData.hasKey(petDetails.id)) {
            // Send bad request message if request doesn't contain valid pet id
            resp.setTextPayload("Error : provide the json payload with valid `id``");
            // set the response code as 400 to indicate a bad request
            resp.statusCode = 400;
        }
        else {
            // Update the pet details in the map
            petData[petDetails.id] = petDetails;
            // Send back the status message back to the client
            string payload = "Pet updated successfully : Pet ID = " + petDetails.id;
            resp.setTextPayload(payload);
        }
    }

    error => {
        // Send bad request message if request doesn't contain valid pet data
        resp.setTextPayload("Error : Please provide the json payload with `id`,
        `catogery` and `name`");
        // set the response code as 400 to indicate a bad request
        resp.statusCode = 400;
    }
}
return resp;



ic function getPetById(http:Request req, int petId) returns http:Response {
// Initialize http response message to send back to the client
http:Response resp;
// Send bad request message to client if pet ID cannot found in petData map
if (!petData.hasKey(<string>petId)) {
    resp.setTextPayload("Error : Invalid Pet ID");
    // set the response code as 400 to indicate a bad request
    resp.statusCode = 400;
}
else {
    // Set the pet data as the payload and send back the response
    var payload = <string>petData[<string>petId];
    resp.setTextPayload(payload);
}
return resp;


ic function deletePet(http:Request req, int petId) returns http:Response {
// Initialize http response message
http:Response resp;
// Send bad request message to client if pet ID cannot found in petData map
if (!petData.hasKey(<string>petId)) {
    resp.setTextPayload("Error : Invalid Pet ID");
    // set the response code as 400 to indicate a bad request
    resp.statusCode = 400;
}
else {
    // Remove the pet data from the petData map
    _ = petData.remove(<string>petId);
    // Send the status back to the client
    string payload = "Deleted pet data successfully : Pet ID = " + petId;
    resp.setTextPayload(payload);
}
return resp;

With that, we have completed the implementation of the pet store web service.

Testing
Invoking the petstore service

You can run the RESTful service that you developed above, in your local environment. Open your terminal and navigate to open-api-based-service/guide, and execute the following command.

llerina run petstore

Add a new pet

 -X POST -d '{"id":1, "catogery":"dog", "name":"doggie"}' 
p://localhost:9090/v1/pet/" -H "Content-Type:application/json"

ut :  
added successfully : Pet ID = 1

Retrieve pet data

 "http://localhost:9090/v1/pet/1"

ut:
":"1","catogery":"dog","name":"Updated"}

Update pet data

 -X PUT -d '{"id":1, "catogery":"dog-updated", "name":"Updated-doggie"}' 
p://localhost:9090/v1/pet/" -H "Content-Type:application/json"

ut: 
details updated successfully : id = 1

Delete pet data

 -X DELETE  "http://localhost:9090/v1/pet/1"

ut:
ted pet data successfully: Pet ID = 1
Writing Unit Tests

In Ballerina, the unit test cases should be in the same package inside a folder named as 'tests'. When writing the test functions the below convention should be followed.

This guide contains unit test cases for each method available in the 'petstore service' implemented above.

To run the unit tests, open your terminal and navigate to open-api-based-service/guide, and run the following command.

llerina test

To check the implementation of the test file, refer to the ballerina_petstore_test.bal.

Deployment

Once you are done with the development, you can deploy the service using any of the methods that we listed below.

Deploying locally
Deploying on Docker

You can run the service that we developed above as a docker container. As Ballerina platform offers native support for running ballerina programs on containers, you just need to put the corresponding docker annotations on your service code.

BallerinaPetstore.bal
ther imports
rt ballerinax/docker;

ker:Config {
registry:"ballerina.guides.io",
name:"petstore",
tag:"v1.0"


ker:Expose{}
oint http:ServiceEndpoint ep0 {
host:"localhost",
port:9090


petData' Map definition

@swagger:ServiceInfo' annotation

p:ServiceConfig {
basePath:"/v1"

ice<http:Service> BallerinaPetstore bind ep0 {
docker run -d -p 9090:9090 ballerina.guides.io/petstore:v1.0
Here we run the docker image with flag`` -p <host_port>:<container_port>`` so that we  use  the host port 9090 and the container port 9090. Therefore you can access the service through the host port. 
curl -X POST -d '{"id":1, "catogery":"dog", "name":"doggie"}' \
"http://localhost:9090/v1/pet/" -H "Content-Type:application/json"  
Deploying on Kubernetes
BallerinaPetstore.bal
ther imports
rt ballerinax/kubernetes;

ernetes:Ingress {
stname:"ballerina.guides.io",
me:"ballerina-guides-petstore",
th:"/"


ernetes:Service {
rviceType:"NodePort",
me:"ballerina-guides-petstore"


ernetes:Deployment {
age:"ballerina.guides.io/petstore:v1.0",
me:"ballerina-guides-petstore"


oint http:ServiceEndpoint ep0 {
host:"localhost",
port:9090


petData' Map definition

@swagger:ServiceInfo' annotation

p:ServiceConfig {
basePath:"/v1"

ice<http:Service> BallerinaPetstore bind ep0 {
allerina build petstore

n following command to deploy kubernetes artifacts:  
bectl apply -f ./target/petstore/kubernetes
ubectl apply -f ./target/petstore/kubernetes 
eployment.extensions "ballerina-guides-petstore" created
ngress.extensions "ballerina-guides-petstore" created
ervice "ballerina-guides-petstore" created
 everything is successfully deployed, you can invoke the service either via Node port or ingress. 

 Port:

curl -X POST -d '{“id”:1, “catogery”:“dog”, “name”:“doggie”}' \ “http://:/v1/pet/” -H “Content-Type:application/json”

ess:

`/etc/hosts` entry to match hostname. 

127.0.0.1 ballerina.guides.io

ss the service 

curl -X POST -d '{“id”:1, “catogery”:“dog”, “name”:“doggie”}' \ “http://ballerina.guides.io/v1/pet/” -H “Content-Type:application/json”

bservability 
erina is by default observable. Meaning you can easily observe your services, resources, etc.
ver, observability is disabled by default via configuration. Observability can be enabled by adding following configurations to `ballerina.conf` file in `open-api-based-service/guide/`.

[b7a.observability]

[b7a.observability.metrics]

Flag to enable Metrics

enabled=true

[b7a.observability.tracing]

Flag to enable Tracing

enabled=true

: The above configuration is the minimum configuration needed to enable tracing and metrics. With these configurations default values are load as the other configuration parameters of metrics and tracing.

Tracing 

can monitor ballerina services using in built tracing capabilities of Ballerina. We'll use [Jaeger](https://github.com/jaegertracing/jaeger) as the distributed tracing system.
ow the following steps to use tracing with Ballerina.

u can add the following configurations for tracing. Note that these configurations are optional if you already have the basic configuration in `ballerina.conf` as described above.

[b7a.observability]

[b7a.observability.tracing] enabled=true name=“jaeger”

[b7a.observability.tracing.jaeger] reporter.hostname=“localhost” reporter.port=5775 sampler.param=1.0 sampler.type=“const” reporter.flush.interval.ms=2000 reporter.log.spans=true reporter.max.buffer.spans=1000

n Jaeger docker image using the following command

$ docker run -d -p5775:5775/udp -p6831:6831/udp -p6832:6832/udp -p5778:5778 -p16686:16686 \ -p14268:14268 jaegertracing/all-in-one:latest

vigate to `open-api-based-service/guide` and run the restful-service using following command 

$ ballerina run petstore

serve the tracing using Jaeger UI using following URL

http://localhost:16686

Metrics
ics and alarts are built-in with ballerina. We will use Prometheus as the monitoring tool.
ow the below steps to set up Prometheus and view metrics for Ballerina restful service.

u can add the following configurations for metrics. Note that these configurations are optional if you already have the basic configuration in `ballerina.conf` as described under `Observability` section.

[b7a.observability.metrics] enabled=true provider=“micrometer”

[b7a.observability.metrics.micrometer] registry.name=“prometheus”

[b7a.observability.metrics.prometheus] port=9700 hostname=“0.0.0.0” descriptions=false step=“PT1M”

eate a file `prometheus.yml` inside `/tmp/` location. Add the below configurations to the `prometheus.yml` file.

global:

 scrape_interval:     15s
 evaluation_interval: 15s

scrape_configs:

 - job_name: prometheus
   static_configs:
     - targets: ['172.17.0.1:9797']
OTE : Replace `172.17.0.1` if your local docker IP differs from `172.17.0.1`

n the Prometheus docker image using the following command

$ docker run -p 19090:9090 -v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml \ prom/prometheus

u can access Prometheus at the following URL

http://localhost:19090/

:  Ballerina will by default have following metrics for HTTP server connector. You can enter following expression in Prometheus UI
ttp_requests_total
ttp_response_time


Logging

erina has a log package for logging to the console. You can import ballerina/log package and start logging. The following section will describe how to search, analyze, and visualize logs in real time using Elastic Stack.

art the Ballerina Service with the following command from `open-api-based-service/guide`

$ nohup ballerina run petstore &» ballerina.log&

OTE: This will write the console log to the `ballerina.log` file in the `open-api-based-service/guide` directory

art Elasticsearch using the following command

art Elasticsearch using the following command

$ docker run -p 9200:9200 -p 9300:9300 -it -h elasticsearch –name \ elasticsearch docker.elastic.co/elasticsearch/elasticsearch:6.2.2

OTE: Linux users might need to run `sudo sysctl -w vm.max_map_count=262144` to increase `vm.max_map_count` 

art Kibana plugin for data visualization with Elasticsearch

$ docker run -p 5601:5601 -h kibana –name kibana –link \ elasticsearch:elasticsearch docker.elastic.co/kibana/kibana:6.2.2

nfigure logstash to format the ballerina logs

reate a file named `logstash.conf` with the following content

input {
beats{

 port => 5044 

}
}

filter {
grok{

 match => { 
 "message" => "%{TIMESTAMP_ISO8601:date}%{SPACE}%{WORD:logLevel}%{SPACE}
 \[%{GREEDYDATA:package}\]%{SPACE}\-%{SPACE}%{GREEDYDATA:logMessage}"
 }  

}
}

output {
elasticsearch{

 hosts => "elasticsearch:9200"  
 index => "store"  
 document_type => "store_logs"  

}
}

Save the above `logstash.conf` inside a directory named as `{SAMPLE_ROOT}\pipeline`

 Start the logstash container, replace the `{SAMPLE_ROOT}` with your directory name

$ docker run -h logstash –name logstash –link elasticsearch:elasticsearch \ -it –rm -v ~/{SAMPLE_ROOT}/pipeline:/usr/share/logstash/pipeline/ \ -p 5044:5044 docker.elastic.co/logstash/logstash:6.2.2

onfigure filebeat to ship the ballerina logs

reate a file named `filebeat.yml` with the following content

filebeat.prospectors:

ii) Save the above filebeat.yml inside a directory named as {SAMPLE_ROOT}\filebeat

iii) Start the logstash container, replace the {SAMPLE_ROOT} with your directory name

cker run -v {SAMPLE_ROOT}/filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml \
SAMPLE_ROOT}/guide.restful_service/restful_service/ballerina.log:/usr/share\
ebeat/ballerina.log --link logstash:logstash docker.elastic.co/beats/filebeat:6.2.2

This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.