openapi: 3.0.3

info:
  title: Metrics Capture as a Service (MCaaS)
  description: |
    Pearson Service Insight's Metrics Capture as a Service rest API allows users to push JSON 
    formatted metrics to be uploaded to AWS Timestream databases and tables.

    These AWS Timestream databases are pre-integrated with Grafana, allowing users to quickly
    cycle from pushing metrics over HTTP to charting those metrics in Grafana.

    The normal use case for Grafana includes low velocity, low volume time series data that is
    ready to be visualized.

    ### Data Retention in AWS Timestream

    All AWS Timestream tables that MCaaS writes to have a data retention policy, including
    transferring data between storage tiers.
    There are two storage tiers in AWS Timestream:
      1. Memory Store
          - Fast, point in time queries
          - Typically used to store the most recent data
          - Data is moved to magnetic store after it expires from memory store
      2. Magnetic Store
          - Stores historical data
          - Slower
          - Data is deleted from the table after it expires from magnetic store

    Storage time is cumulative across both tiers, so a table with a 7 day memory store and a 30 day magentic store will retain data for a total of 37 days.  
    For more information, see the [AWS Documentation](https://docs.aws.amazon.com/timestream/latest/developerguide/storage.html)  on Timestream storage.

    ### Authentication

    Users authenticate with this rest API using the combination of two headers:
     - `x-mcaas-user` : The username for the user
     - `x-api-key` : The corresponding API key for the user
    
    If a user sends invalid credentials they will receive a 403 error.
    Valid credentials that pass authentication may still receive a 403 error if they request to
    access a resource they aren't authorized to access. Please refer to the "Authorization" section.

    ### Authorization

    While authenticated users are permitted to POST to the /metrics endpoint, they may not be
    authorized to write to the requested AWS Timestream database or table.
    
    Users will receive a 403 error if they authenticate successfully, but attempt to write to an AWS
    Timestream table they don't have access to.

  version: "0.2.0"

servers:
  - url: "http://api.pearsonserviceinsights.com/mcaas"
    description: Production Metrics Capture as a Service rest API
  - url: "http://api-uat.pearsonserviceinsights.com/mcaas"
    description: User Acceptance Testing (UAT) Metrics Capture as a Service rest API

tags:
  - name: Metrics
    description: |
      Endpoints to facilitate pushing JSON formatted metrics to AWS Timestream databases and tables.
  - name: API Documentation
    description: |
      API Documentation at the base path for this rest API.
  - name: Health Check
    description: |
      Endpoint for checking the health of a MCaaS.

# Rules for API Gateway's built in request validation
x-amazon-apigateway-request-validators:
  all:
    validateRequestBody: true
    validateRequestParameters: true
  params-only:
    validateRequestBody: false
    validateRequestParameters: true

paths:
  /:
    options:
      tags:
       - API Documentation
      summary: CORS support for interactive API documentation
      operationId: baseCorsOptions
      description: Enable CORS by returning correct headers
      responses:
        200:
          description: Default response for CORS method
          headers:
            Access-Control-Allow-Origin:
              schema:
                type: string
            Access-Control-Allow-Methods:
              schema:
                type: string
            Access-Control-Allow-Headers:
              schema:
                type: string
          content: {}
      x-amazon-apigateway-integration:
        type: mock
        requestTemplates:
          application/json: |
            {
              "statusCode" : 200
            }
        responses:
          default:
            statusCode: "200"
            responseParameters:
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Methods: "'*'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
            responseTemplates:
              application/json: |
                {}

    get:
      summary: API landing page
      tags:
       - API Documentation
      operationId: ApiLandingPage
      responses:
        200:
          description: OK
          headers:
            Content-Type:
              schema:
                type: string
            Access-Control-Allow-Origin:
              schema:
                type: string
            Access-Control-Allow-Methods:
              schema:
                type: string
            Access-Control-Allow-Headers:
              schema:
                type: string
      x-amazon-apigateway-integration:
        responses:
          default:
            statusCode: "200"
            responseParameters:
              method.response.header.Content-Type: "'text/html'"
              # Support for CORS as API DOC is served from another origin server, not the API Gateway itself
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"
              method.response.header.Access-Control-Allow-Methods: "'*'"
              method.response.header.Access-Control-Allow-Origin: "'*'"
            responseTemplates:
              text/html: >
                <!DOCTYPE html>
                <html lang="en">
                  <head>
                    <meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 charecters -->
                    <script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
                  </head>
                  <body>
                    <rapi-doc spec-url="https://downloads.pearsoncloudmanagement.com/mcaas/$context.stage/openapi.yaml"> </rapi-doc>
                  </body>
                </html>
        requestTemplates:
          application/json: "{\"statusCode\": 200}"
        passthroughBehavior: "when_no_match"
        type: "mock"

  /metrics:
    post:
      tags:
        - Metrics
      summary: Push metrics to AWS Timestream
      description: |
        Push JSON formatted metrics to the specified AWS Timestream database and table.

        ### Accepted Metric Formats
        JSON formatted metrics must be flat (scalar values only). Metrics must not have nested JSON
        objects or arrays.
        All non-numeric values of a metric are converted into strings for use as dimensions in the
        corresponding Timestream records.
        All numeric values of a metric are converted into floats for use as measure values in the
        corresponding Timestream records.

        ### Enforced Limitations
        Max Concurrent Requests = `50`

        Max Dimensions per Record = `100`
        
        Max Records per Request = `100`

        ### Push settings

        #### Upsert
        When upsert is set to `true`, the measure value will be updated for any metric that has the
        same dimension name/values as a record in the database. The version for an updated record is
        then set to the current Unix Epoch Time to ensure a version number greater than the previous
        is always provided. For this reason, you may not provide your own `version` for a metric.

        #### Preview
        When preview is set to `true`, the endpoint will run in dry-run mode, allowing users to
        inspect the results of their metrics push before actually writing to AWS Timestream.

        #### Min Common Dimensions
        As part of the metrics push, this endpoint will automatically batch your records for writing
        to AWS Timestream as efficiently as possible.

        The batching algorithm behind this endpoint attempts to batch metrics by common dimensions.
        The `min_common_dimensions` parameter defines the minimum number of common dimensions that
        records must share in order to be placed in the same batch.
        The `min_common_dimensions` parameter accepts values of 0 to 5 (inclusive), allowing users
        to adjust the behavior of this batching algorithm. By default, this parameter is set to 3.
        When upsert is set to `true`, the measure value will be updated for any metric that has the same dimension name/values as a record in the database. The version for an updated record is then set to the current Unix Epoch Time to ensure a version number greater than the previous is always provided. For this reason, you may not provide your own `version` for a metric. Doing so will return response code 500.
      operationId: postMetrics
      security:
        - ApiGatewayAuth: []
          ApiKeyAuth: []
      requestBody:
        description: |
          JSON object specifying an AWS Timestream database and table to upload metrics to, JSON
          formatted metric data, and other settings.
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                database:
                  type: string
                  example: timestream-database
                  description: AWS Timestream database to upload metrics to.
                table:
                  type: string
                  example: timestream-table
                  description: AWS Timestream table to upload metrics to. Must be a table in the specified database.
                metrics:
                  type: array
                  items:
                    type: object
                  description: JSON formatted metrics to upload.
                preview:
                  type: boolean
                  nullable: true
                  example: true
                  description: |
                    Perform a preview/dry run of the Timestream load.
                    No records get written to Timestream, but a preview of the result is returned.
                upsert:
                  type: boolean
                  nullable: true
                  example: false
                  description: |
                    Load metrics into Timestream as an upsert (update existing records on conflict, otherwise insert).
                    All JSON objects must specify a "time" field to perform an upsert
                min_common_dimensions:
                  type: integer
                  nullable: true
                  example: 4
                  description: |
                    The minimum number of shared dimensions a metric must have to be batch loaded. If omitted, MCaaS will default to 3.  
                    Valid values: 0-5
              required:
                - database
                - table
                - metrics
      responses:
        '201':
          description: Metrics successfully written to Timestream.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    nullable: true
                    example: null
                    description: Error message in the event of a failure. Null if the upload was successful.
                  invalid_items:
                    type: array
                    items:
                      type: object
                    example: []
                    description: Items that could not be parsed into Timestream records. Empty if the upload was successful.
                  accepted_records:
                    type: array
                    items:
                      type: object
                    example: [{"measure_values": [{"Name": "example_metric", "Value": "10", "Type": "DOUBLE"}], "dimension1": "dimension value", "time": "2021-09-01 11:12:13.456000000"}]
                    description: Records successfully written to Timestream. If "preview" is set to true on the request, this contains records successfully parsed from the request's metrics but not yet written to Timestream.
                  rejected_records:
                    type: array
                    items:
                      type: object
                    example: []
                    description: Records successfully parsed from the request's metrics but rejected by Timestream.
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    nullable: true
                    example: Some records were rejected by Timestream
                    description: Error message in the event of a failure. Null if the upload was successful.
                  invalid_items:
                    type: array
                    items:
                      type: object
                    example: []
                    description: Items that could not be parsed into Timestream records. Empty if the upload was successful.
                  accepted_records:
                    type: array
                    items:
                      type: object
                    example: []
                    description: Records successfully written to Timestream. If "preview" is set to true on the request, records successfully parsed from the request's metrics.
                  rejected_records:
                    type: array
                    items:
                      type: object
                    example: [{"record": {"measure_name": "example_metric", "measure_value::double": 10.0, "dimension1": "dimension value", "time": "2021-09-01 11:12:13.456000000", "version": 1}, "error": "A record already exists with the same time, dimensions, measure name, and version 1. A higher version is required to update the measure value."}]
                    description: Records successfully parsed from the request's metrics but rejected by Timestream.
        '401':
          description: Unauthorized
        '403':
          description: Forbidden. Occurs when the request fails either authentication or authorization.
        '500':
          description: Server error
      x-amazon-apigateway-integration:
        type: aws_proxy
        uri: 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:mcaas-request-handler/invocations'
        httpMethod: POST
        passthroughBehavior: when_no_match
        contentHandling: CONVERT_TO_TEXT

  /health:
    get:
      tags:
        - Health
      summary: Check availability of MCaaS
      description: |
        Test the API to ensure the backend is responding
      operationId: getHealth
      responses:
        '200':
          description: All systems functional
          content:
            text/plain:
              schema:
                type: string
                example: "pong!"
        '401':
          description: Unauthorized
        '500':
          description: Server error
      x-amazon-apigateway-integration:
        type: aws_proxy
        uri: 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:mcaas-request-handler/invocations'
        httpMethod: POST
        passthroughBehavior: when_no_match
        contentHandling: CONVERT_TO_TEXT

# https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-authorizer.html
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
    ApiGatewayAuth:
      type: apiKey
      in: header
      name: Unused
      x-amazon-apigateway-authorizer:
        type: request
        identitySource: 'method.request.header.x-mcaas-user,method.request.header.x-api-key'
        authorizerUri: 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:mcaas-custom-auth/invocations'
        authorizerResultTtlInSeconds: 300
        enableSimpleResponses: false
      x-amazon-apigateway-authtype: custom

x-amazon-apigateway-importexport-version: '1.0'

