Everybody loves REST APIs. With such interfaces, developers can easily hook into external systems and exchange data, or trigger events. In this article, we will take a look into a mechanism that will allow anyone to both specify and rely on a strict API specification.
Prerequisites
Of course you can read this article without ever leaving the browser, but it’s a lot more fun when following along. Here’s the list of things you might want to have:
wget
or a similar tool to download things from the internet- a current Java runtime environment (the
java
command should be on your PATH) - a golang version of 1.12 on your PATH
- a text-editor of your choice
So what’s the problem with REST APIs?
Whenever we’re working with foreign REST APIs, we need to rely on the public documentation, that the vendor supplies. These documents need to be as accurate as possible. As time goes by, code will inevitably diverge from your documentation, as you will most probably enhance the featureset and fix conceptual issues.
With Instana, we have chosen to evolve our REST API over time, and without a big-bang. To be able to expose accurate documentation, we automatically publish machine-generated documentation on every release to our GitHub pages documentation.
In the document, there’s nothing fancy. An html documentation, with some code snippets and endpoint specifications – time to create code resembling our model into your consumer-code, right? – Not quite. There’s a quick and easy path to consume our API from within your system without wrestling too much with our data-formats.
Enter OpenAPI
OpenAPI (the artist formerly known as “Swagger Specification”) is a project that aims to provide a path to happiness for both API consumers and vendors. By designing the specification rather than the endpoints directly, they aim at having a stable and binding contract between vendors and consumers.
What does such specification look like? Here we have a shortened version of a specification for the infamous “PetShop” application:
openapi: "3.0.0" info: version: 1.0.0 title: Swagger Petstore license: name: MIT servers: - url: http://petstore.swagger.io/v1 paths: /pets: get: summary: List all pets operationId: listPets tags: - pets parameters: - name: limit in: query description: How many items to return at one time (max 100) required: false schema: type: integer format: int32 responses: '200': description: A paged array of pets headers: x-next: description: A link to the next page of responses schema: type: string content: application/json: schema: $ref: "#/components/schemas/Pets" components: schemas: Pet: type: object required: - id - name properties: id: type: integer format: int64 name: type: string tag: type: string Pets: type: array items: $ref: "#/components/schemas/Pet"
You might already have glimpsed at the possible consequences for the API in question. It says we have an endpoint /pets
with a possible GET
HTTP operation on it that returns a paginated list of Pets
. When we now look up the Pets
object in the components
section, we can quickly infer that it is an array of Pet
objects. The Pet
object ist specified as well and we can see that a Pet
has mandatory name
and id
properties with associate types.
If we are able to read such specification with our own eyes – why cant machines?
Entering OpenAPI Generator
In the OpenAPI ecosystem, there are multiple projects focussed on processing specification documents. These range from validating documents, to creating mock servers and testing utilities to generating client and even server code. The one we’ll focus about in this article, is the OpenAPI generator project.
The OpenAPI generator project focusses on providing a convenient way to generate code you can work with from a given specification document from a single command.
Instana OpenAPI specification
To spice things up a bit, we will be working with our API specification. You can retrieve it through the generated documentation. (YAML Version / JSON Version). This document is updated regularly within our release cycle and you should be able to work with it right out of the box.
Enter Golang
The Go language is great for creating small binaries for almost every occasion in a straight-forward fashion. We’ll use that and we require at least version 1.12
for proper modules support.
Implementation
First, we want to create our project directory, download the OpenAPI generator, and see what it can do for us:
# create and enter the project directory $ mkdir instana-golang-example && cd $_ # download the jar $ wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/5.0.0-beta/openapi-generator-cli-5.0.0-beta.jar \ -O openapi-generator-cli.jar # take peek at the different generators it offers us $ java -jar openapi-generator-cli.jar
Impressive list, right? Next we’ll use that jar
file to generate a go client for the Instana REST API:
# download the Instana openapi spec $ mkdir resources && wget https://instana.github.io/openapi/openapi.yaml -O resources/openapi.yaml # generated code is ugly, we'll use "gofmt" to format it in the same step $ GO_POST_PROCESS_FILE="gofmt -s -w" java -jar openapi-generator-cli.jar generate -i resources/openapi.yaml -g go \ -o pkg/instana \ --additional-properties packageName=instana \ --additional-properties isGoSubmodule=true \ --additional-properties packageVersion=1.185.824 \ --type-mappings=object=interface{} \ --enable-post-process-file \ --skip-validate-spec
Done. Don’t believe me? Let’s have a look into the generated documentation:
cat pkg/instana/README.md
We can now use it in our tiny new tool. Let’s create a go.mod
file in the current directory to include the library in our project:
module github.com/instana/instana-openapi-golang-example require github.com/instana/instana-openapi-golang-example/pkg/instana v1.185.824 replace github.com/instana/instana-openapi-golang-example/pkg/instana v1.185.824 => ./pkg/instana
Cool. We just linked our local api client into the go modules dependency management. Go modules are awesome!
To show you how to use the client, we can create a small program that will query all the available search fields you can use in instana.
Let’s create a main.go
file in our current directory:
package main import ( "context" "fmt" "os" "strings" "time" "github.com/antihax/optional" "github.com/instana/instana-openapi-golang-example/pkg/instana" ) func main() { // grab settings from environment apiKey := os.Getenv("INSTANA_KEY") tenant := os.Getenv("INSTANA_TENANT") unit := os.Getenv("INSTANA_UNIT") // create golang client specific configuration configuration := instana.NewConfiguration() host := fmt.Sprintf("%s-%s.instana.io", unit, tenant) configuration.Host = host configuration.BasePath = fmt.Sprintf("https://%s", host) client := instana.NewAPIClient(configuration) // Instana uses the `apiToken` prefix in the `Authorization` header auth := context.WithValue(context.Background(), instana.ContextAPIKey, instana.APIKey{ Key: apiKey, Prefix: "apiToken", }) searchFieldResults, _, err := client.InfrastructureCatalogApi.GetInfrastructureCatalogSearchFields(auth) if err != nil { fmt.Println(err) os.Exit(1) } fmt.Println("Available search fields supported by Instana:") for _, field := range searchFieldResults { fmt.Println(fmt.Sprintf("%s (%s) - %s", field.Keyword, field.Context, field.Description)) } }
Next, we need an API token. To get yourself a shiny new token, head to your Instana instance, and got to Settings -> API Tokens -> Add API Token
:
Side note: we need the tenant
and unit
from your environment. When using our SaaS offering, you can get these from yor environments’ URL. If you are accessing your environment through https://qa-acme.instana.io
, then acme
is the tenant and qa is the unit.
That’s it. Let’s run our code and see what search fields we can use with Instana:
$ INSTANA_TENANT=acme INSTANA_UNIT=qa INSTANA_KEY=$your_token$ go run main.go Available search fields supported by Instana: entity.kubernetes.pod.name (entities) - Kubernetes Pod Name event.specification.id (events) - Event specification ID entity.ping.target (entities) - Ping target entity.azure.service.sqlserver.name (entities) - The name of the SQL database server entity.lxc.ip (entities) - LXC container IP entity.jboss.serverName (entities) - JBoss Server Name ...
Making use of it
Listing the available tags is one thing, but nothing fancy, right? We want to live up to the hype and get metrics from a specific Kubernetes pod. Specifically the CPU requests over time.
To facilitate this, we need to find what we’re searching for. Instana organizes metrics by “plugins”. To see what plugins are available, we can query them:
// search for the kubernetes pod plugin // https://instana.github.io/openapi/#operation/getInfrastructureCatalogPlugins plugins, _, err := client.InfrastructureCatalogApi.GetInfrastructureCatalogPlugins(auth) if err != nil { println(fmt.Errorf("Error reading plugins: %s", err)) os.Exit(1) } println("Let's find ourselves some kubernetes pods!") // search for kubernetes plugins: for _, plugin := range plugins { if strings.Contains(plugin.Plugin, "kubernetes") { fmt.Printf("Found Kubernetes plugin %s with ID %s\n", plugin.Label, plugin.Plugin) } }
The output will be very similar to this:
Metric for plugin `kubernetesPod`: Containers (ID: container_count) Metric for plugin `kubernetesPod`: Memory Requests (ID: memoryRequests) Metric for plugin `kubernetesPod`: CPU Limits (ID: cpuLimits) Metric for plugin `kubernetesPod`: CPU Requests (ID: cpuRequests) Metric for plugin `kubernetesPod`: Restarts (ID: restartCount) Metric for plugin `kubernetesPod`: Memory Limits (ID: memoryLimits)
There you go. Our metric ID for getting CPU requests from the “kubernetesPod” plugin is “cpuRequests”.
Finding your snapshot
Next thing, knowing what metric from which plugin we want, we need to find Kubernetes pods in our infrastructure inventory. Instana organizes infrastructure changes over time in a concept called “snapshot”. We need to find a snapshot that contains “kubernetesPod” entities and then we can go query those metrics.
When you deploy our Instana Agent via one of the supported methods to your Kubernetes cluster, the agent pods themselves, will have names in the form of “instana-agent-ID”. Knowing this, we can search for all snapshots that contain entities with values of instana-agent-*
in the entity.kubernetes.pod.name
field.
Same as you would query via Dynamic Focus Query:
// let's find all the Snapshots that involve Kubernetes pods! // https://instana.github.io/openapi/#operation/getSnapshots snapshotsWithKubernetesPods, _, err := client.InfrastructureMetricsApi.GetSnapshots(auth, &instana.GetSnapshotsOpts{ Plugin: optional.NewString(kubernetesPodPluginID), // We know that clusters, when monitored with Instana usually have pods with a name of `instana-agent-*` Query: optional.NewString("entity.kubernetes.pod.name:instana-agent*"), // We can travel through time and query data from entities that are no more! Offline: optional.NewBool(true), // Instana uses Milliseconds accross the board WindowSize: optional.NewInt64(7 * 86400 * 1000), To: optional.NewInt64(time.Now().Unix() * int64(time.Millisecond)), }) if err != nil { println(fmt.Errorf("Error reading snapshots: %s", err)) os.Exit(1) } for _, snapshot := range snapshotsWithKubernetesPods.Items { fmt.Printf("Kubernetes Pod %s on Host %s with snapshot ID %s \n", snapshot.Label, snapshot.Host, snapshot.SnapshotId) }
The output will look like this:
Let's find a Kubernetes Pod that contains an Instana Agent Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) on Host with snapshot ID fkHp5kkCvpBonSAkyZD03GagAL4 Kubernetes Pod instana-agent/instana-agent-1-7jlwd (pod) on Host with snapshot ID 2sCSGsxJGUJB3mpaQ7SVOHJlMmY Kubernetes Pod instana-agent/instana-agent-1-7rqvm (pod) on Host with snapshot ID FU7AaxMkghV9vsoB05AEXdrZqpE
Retrieving the metrics
Now that we know the snapshot IDs, we can actually go and fetch the metrics from those snapshots:
println("Let's put everything together: Querying the cpuRequests pod metrics from a specific snapshot") metricID := "cpuRequests" metricsQuery := instana.GetCombinedMetrics{ Plugin: kubernetesPodPluginID, Metrics: []string{ metricID, }, SnapshotIds: []string{ snapshotsWithKubernetesPods.Items[0].SnapshotId, }, TimeFrame: instana.TimeFrame{ To: time.Now().Unix() * 1000, WindowSize: 300000, }, // 5 Minutes Rollup: 60, } metricsResult, _, err := client.InfrastructureMetricsApi.GetInfrastructureMetrics(auth, &instana.GetInfrastructureMetricsOpts{ GetCombinedMetrics: optional.NewInterface(metricsQuery), }) if err != nil { println(fmt.Errorf("Error reading metrics: %s", err.(instana.GenericOpenAPIError))) os.Exit(1) } for _, metric := range metricsResult.Items { for _, bracket := range metric.Metrics[metricID] { parsedTime := time.Unix(0, int64(bracket[0])*int64(time.Millisecond)) fmt.Printf("CPU requests of Kubernetes Pod %s at %s: %f\n", snapshotsWithKubernetesPods.Items[0].Label, parsedTime, bracket[1]) } }
The output will look like this:
Let's put everything together: Querying the cpuRequests pod metrics from a specific snapshot CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 10:56:06.656 +0000 UTC: 0.599900 CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 10:58:17.728 +0000 UTC: 0.599900 CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 10:58:17.728 +0000 UTC: 0.599900 CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 11:00:28.8 +0000 UTC: 0.599900 CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 11:00:28.8 +0000 UTC: 0.599900 CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 11:02:39.872 +0000 UTC: 0.599900
With having read until here, you learned how you can
- create or rather generate a API client in GoLang through an OpenAPI specification
- use that client to query the Instana REST API in a type-safe way
- resolve metric values from a specific item from Instanas vast, automatically created infrastructure store
Summary
In the article, we learned how to generate an API client for a specific programming environment based on an OpenAPI specification that is provided by a 3rd party. This is adaptable for many different projects and languages. An upside of this approach is, that the contract is basically also the code that is being executed to communicate with the remote API.
In case of the Instana REST API, the OpenAPI makes it easy for us, and our customers to version our API specification and separate our on-premise product and our SaaS offering, so you can always generate a client library for your specific environment.
The complete code is available on GitHub.