Add log api functionality

Change-Id: Ie7025de1bafaf788c8b5950c4a1ee6921b264d8b
This commit is contained in:
Lukasz Zajaczkowski 2015-08-18 14:00:10 +00:00 committed by Witold Bedyk
parent 257c20fa59
commit c798c2cc54
48 changed files with 4750 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
*.py[co]
*~
doc/build/*
dist
build
cover
.coverage
*.egg
*.egg-info
.testrepository
.tox
ChangeLog
MANIFEST
monasca.log
*.swp
*.iml
.DS_Store
.cache
.classpath
.idea
.project
.target/
java/debs/*
.settings/
target
test-output/
logs/
*config*.yml
db/config.yml

175
LICENSE Normal file
View File

@ -0,0 +1,175 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

78
README.md Normal file
View File

@ -0,0 +1,78 @@
# Forked from https://github.com/stackforge/monasca-api
This repository is forked from [monasca-api](https://github.com/stackforge/monasca-api).
# Overview
`monasca-log-api` is a RESTful API server that is designed with a layered architecture [layered architecture](http://en.wikipedia.org/wiki/Multilayered_architecture).
The full API Specification can be found in [docs/monasca-log-api-spec.md](docs/monasca-log-api-spec.md)
## Java Build
Requires monasca-common from https://github.com/stackforge/monasca-common. Download and do mvn install. Then:
```
cd java
mvn clean package
```
# StackForge Java Build
There is a pom.xml in the base directory that should only be used for the StackForge build. The StackForge build is a rather strange build because of the limitations of the current StackForge java jobs and infrastructure. We have found that the API runs faster if built with maven 3 but the StackForge nodes only have maven 2. This build checks the version of maven and if not maven 3, it downloads a version of maven 3 and uses it. This build depends on jars that are from monasca-common. That StrackForge build uploads the completed jars to http://tarballs.openstack.org/ci/monasca-common, but they are just regular jars, and not in a maven repository and sometimes zuul takes a long time to do the upload. Hence, the first thing the maven build from the base project does is invoke build_common.sh in the common directory. This script clones monasca-common and then invokes maven 3 to build monasca-common in the common directory and install the jars in the local maven repository.
Since this is all rather complex, that part of the build only works on StackForge so follow the simple instruction above if you are building your own monasca-log-api.
Currently this build is executed on the bare-precise nodes in StackForge and they only have maven 2. So, this build must be kept compatible with Maven 2. If another monasca-common jar is added as a dependency to java/pom.xml, it must also be added to download/download.sh.
## Usage
```
java -jar target/monasca-log-api.jar server config-file.yml
```
## Keystone Configuration
For secure operation of the Monasca API, the API must be configured to use Keystone in the configuration file under the middleware section. Monasca only works with a Keystone v3 server. The important parts of the configuration are explained below:
* serverVIP - This is the hostname or IP Address of the Keystone server
* serverPort - The port for the Keystone server
* useHttps - Whether to use https when making requests of the Keystone API
* truststore - If useHttps is true and the Keystone server is not using a certificate signed by a public CA recognized by Java, the CA certificate can be placed in a truststore so the Monasca API will trust it, otherwise it will reject the https connection. This must be a JKS truststore
* truststorePassword - The password for the above truststore
* connSSLClientAuth - If the Keystone server requires the SSL client used by the Monasca server to have a specific client certificate, this should be true, false otherwise
* keystore - The keystore holding the SSL Client certificate if connSSLClientAuth is true
* keystorePassword - The password for the keystore
* defaultAuthorizedRoles - An array of roles that authorize a user to access the complete Monasca API. User must have at least one of these roles. See below
* agentAuthorizedRoles - An array of roles that authorize only the posting of logs. See Keystone Roles below
* adminAuthMethod - "password" if the Monasca API should adminUser and adminPassword to login to the Keystone server to check the user's token, "token" if the Monasca API should use adminToken
* adminUser - Admin user name
* adminPassword - Admin user password
* adminProjectId - Specify the project ID the api should use to request an admin token. Defaults to the admin user's default project. The adminProjectId option takes precedence over adminProjectName.
* adminProjectName - Specify the project name the api should use to request an admin token. Defaults to the admin user's default project. The adminProjectId option takes precedence over adminProjectName.
* adminToken - A valid admin user token if adminAuthMethod is token
* timeToCacheToken - How long the Monasca API should cache the user's token before checking it again
### Keystone Roles
See [Monasca API documentation](https://github.com/stackforge/monasca-api/blob/master/README.md#keystone-roles) for the levels of access description.
## Design Overview
### Architectural layers
Requests flow through the following architectural layers from top to bottom:
* Resource
* Serves as the entrypoint into the service.
* Responsible for handling web service requests, and performing structural request validation.
* Application
* Responsible for providing application level implementations for specific use cases.
* Domain
* Contains the technology agnostic core domain model and domain service definitions.
* Responsible for upholding invariants and defining state transitions.
* Infrastructure
* Contains technology specific implementations of domain services.
## Documentation
* API Specification: [/docs/monasca-log-api-spec.md](/docs/monasca-log-api-spec.md).

29
common/build_common.sh Executable file
View File

@ -0,0 +1,29 @@
#!/bin/sh
set -x
ME=`whoami`
echo "Running as user: $ME"
MVN=$1
VERSION=$2
check_user() {
ME=$1
if [ "${ME}" != "jenkins" ]; then
echo "\nERROR: Download monasca-common and do a mvn install to install the monasca-commom jars\n" 1>&2
exit 1
fi
}
BUILD_COMMON=false
POM_FILE=~/.m2/repository/monasca-common/monasca-common/${VERSION}/monasca-common-${VERSION}.pom
if [ ! -r "${POM_FILE}" ]; then
check_user ${ME}
BUILD_COMMON=true
fi
# This should only be done on the stack forge system
if [ "${BUILD_COMMON}" = "true" ]; then
git clone https://github.com/stackforge/monasca-common
cd monasca-common
${MVN} clean
${MVN} install
fi

View File

@ -0,0 +1,86 @@
# Monasca Log API
Date: August 19, 2015
Document Version: v2.0
# Log
The log resource allows logs to be created.
## Create Log
Create log.
### POST /v2.0/log/single
#### Headers
* X-Auth-Token (string, required) - Keystone auth token
* Content-Type (string, required) - application/json; text/plain
* X-Application-Type (string(255), optional) - Type of application
* X-Dimensions ({string(255):string(255)}, optional) - A dictionary consisting of (key, value) pairs used to structure logs.
#### Path Parameters
None.
#### Query Parameters
* tenant_id (string, optional, restricted) - Tenant ID to create log on behalf of. Usage of this query parameter requires the `monitoring-delegate` role.
#### Request Body
Consists of a single plain text message or a JSON object which can have a maximum length of 1048576 characters.
#### Request Examples
##### Plain text log - single line
POST a single line of plain text log.
```
POST /v2.0/log/single HTTP/1.1
Host: 192.168.10.4:8080
Content-Type: text/plain
X-Auth-Token: 27feed73a0ce4138934e30d619b415b0
X-Application-Type: apache
X-Dimensions: applicationname:WebServer01,environment:production
Cache-Control: no-cache
Hello World
```
##### Plain text log - multi lines
POST a multiple lines of plain text log.
```
POST /v2.0/log/single HTTP/1.1
Host: 192.168.10.4:8080
Content-Type: text/plain
X-Auth-Token: 27feed73a0ce4138934e30d619b415b0
X-Application-Type: apache
X-Dimensions: applicationname:WebServer01,environment:production
Cache-Control: no-cache
Hello\nWorld
```
##### JSON log
POST a JSON log
```
POST /v2.0/log/single HTTP/1.1
Host: 192.168.10.4:8080
Content-Type: application/json
X-Auth-Token: 27feed73a0ce4138934e30d619b415b0
X-Application-Type: apache
X-Dimensions: applicationname:WebServer01,environment:production
Cache-Control: no-cache
{
"message":"Hello World!",
"from":"hoover"
}
```
### Response
#### Status Code
* 204 - No content
#### Response Body
This request does not return a response body.

439
java/pom.xml Normal file
View File

@ -0,0 +1,439 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>monasca-log-api</groupId>
<artifactId>monasca-log-api</artifactId>
<version>1.1.0-SNAPSHOT</version>
<url>http://github.com/stackforge/monasca-log-api</url>
<packaging>jar</packaging>
<prerequisites>
<maven>3.0</maven>
</prerequisites>
<properties>
<gitRevision></gitRevision>
<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyy-MM-dd'T'HH:mm:ss</maven.build.timestamp.format>
<computedVersion>${project.version}-${timestamp}-${gitRevision}</computedVersion>
<computedName>${project.artifactId}-${computedVersion}</computedName>
<mon.common.version>1.1.0-SNAPSHOT</mon.common.version>
<dropwizard.version>0.7.0</dropwizard.version>
<skipITs>false</skipITs>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<shadedJarName>${project.artifactId}-${project.version}-shaded
</shadedJarName>
</properties>
<scm>
<connection>scm:git:git@github.com:stackforge/monasca-log-api</connection>
<developerConnection>scm:git:git@github.com:stackforge/monasca-log-api</developerConnection>
</scm>
<profiles>
<profile>
<id>release-deploy-url-override</id>
<activation>
<property>
<name>BUILD_NUM</name>
</property>
</activation>
<properties>
<computedVersion>${versionNumber}.${BUILD_NUM}</computedVersion>
</properties>
</profile>
</profiles>
<dependencies>
<dependency>
<groupId>monasca-common</groupId>
<artifactId>monasca-common-model</artifactId>
<version>${mon.common.version}</version>
</dependency>
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>monasca-common</groupId>
<artifactId>monasca-common-kafka</artifactId>
<version>${mon.common.version}</version>
</dependency>
<dependency>
<groupId>monasca-common</groupId>
<artifactId>monasca-common-middleware</artifactId>
<version>${mon.common.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-core</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-assets</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-jersey</artifactId>
<version>${dropwizard.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.2.0-incubating</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.9.2</artifactId>
<version>0.8.0</version>
<exclusions>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>monasca-common</groupId>
<artifactId>monasca-common-testing</artifactId>
<version>${mon.common.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>monasca-common</groupId>
<artifactId>monasca-common-dropwizard</artifactId>
<version>${mon.common.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-testing</artifactId>
<version>${dropwizard.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.docker-java</groupId>
<artifactId>docker-java</artifactId>
<version>0.9.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.restassured</groupId>
<artifactId>rest-assured</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>6.8.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>2.5</version>
<configuration>
<filesets>
<fileset>
<directory>${project.basedir}/debs</directory>
</fileset>
</filesets>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>buildnumber-maven-plugin</artifactId>
<version>1.1</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>create</goal>
</goals>
</execution>
</executions>
<configuration>
<doCheck>false</doCheck>
<shortRevisionLength>6</shortRevisionLength>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.17</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-testng</artifactId>
<version>2.17</version>
</dependency>
</dependencies>
<configuration>
<excludedGroups>
</excludedGroups>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.17</version>
<configuration>
<groups></groups>
<skipTests>${skipITs}</skipTests>
<parallel>methods</parallel>
<threadCount>4</threadCount>
</configuration>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.2</version>
<configuration>
<finalName>${computedName}</finalName>
<createDependencyReducedPom>true</createDependencyReducedPom>
<filters>
<filter>
<!-- *:* can't be used for artifact because we are using an older shade plugin -->
<artifact>org.eclipse.jetty.orbit:javax.servlet</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<artifactSet>
<excludes>
<exclude>org.hamcrest:hamcrest-core</exclude>
<exclude>org.hamcrest:hamcrest-library</exclude>
</excludes>
</artifactSet>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>monasca.api.MonApiApplication</mainClass>
</transformer>
</transformers>
<shadedArtifactAttached>true</shadedArtifactAttached>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<packageName>monasca.api</packageName>
</manifest>
<manifestEntries>
<Implementation-Version>${project.artifactId}-${computedVersion}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4.1</version>
<configuration>
<descriptors>
<descriptor>src/assembly/tar.xml</descriptor>
</descriptors>
<finalName>${artifactNamedVersion}</finalName>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>jdeb</artifactId>
<groupId>org.vafer</groupId>
<version>1.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jdeb</goal>
</goals>
<configuration>
<deb>${project.basedir}/debs/binaries/${computedName}.deb</deb>
<dataSet>
<data>
<type>file</type>
<src>${project.build.directory}/${shadedJarName}.jar</src>
<dst>/opt/monasca/monasca-api.jar</dst>
</data>
<data>
<type>file</type>
<src>${project.basedir}/src/deb/etc/log-api-config.yml-sample
</src>
<dst>/etc/monasca/log-api-config.yml-sample</dst>
</data>
</dataSet>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.3</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-scm-plugin</artifactId>
<version>1.9.2</version>
<configuration>
<tag>${project.version}</tag>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.5.201505241946</version>
<configuration>
<destFile>${basedir}/target/coverage-reports/jacoco-unit.exec</destFile>
<dataFile>${basedir}/target/coverage-reports/jacoco-unit.exec</dataFile>
</configuration>
<executions>
<execution>
<id>jacoco-initialize</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-site</id>
<phase>package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.5.201505241946</version>
<configuration>
<destFile>${basedir}/target/coverage-reports/jacoco-unit.exec</destFile>
<dataFile>${basedir}/target/coverage-reports/jacoco-unit.exec</dataFile>
</configuration>
<executions>
<execution>
<id>jacoco-initialize</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-site</id>
<phase>package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

29
java/src/assembly/tar.xml Normal file
View File

@ -0,0 +1,29 @@
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>tar</id>
<formats>
<format>tar.gz</format>
</formats>
<fileSets>
<fileSet>
<directory>${project.basedir}</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>README*</include>
<include>LICENSE*</include>
</includes>
</fileSet>
</fileSets>
<files>
<file>
<source>${project.build.directory}/${shadedJarName}.jar</source>
<outputDirectory>/</outputDirectory>
<destName>monasca-log-api.jar</destName>
</file>
<file>
<source>${project.basedir}/src/deb/etc/log-api-config.yml-sample</source>
<outputDirectory>examples</outputDirectory>
</file>
</files>
</assembly>

View File

@ -0,0 +1,9 @@
Package: [[name]]
Section: misc
Priority: optional
Architecture: all
Depends: openjdk-7-jre-headless | openjdk-7-jre
Version: [[version]]-[[timestamp]]-[[buildNumber]]
Maintainer: Monasca Team <monasca@lists.launchpad.net>
Description: Monasca-LOG-API
RESTful API for Monasca logs.

View File

@ -0,0 +1,9 @@
#!/bin/sh
case "$1" in
remove)
stop monasca-log-api
;;
esac
exit 0

View File

@ -0,0 +1,72 @@
# The region for which all metrics passing through this server will be persisted
region: useast
kafka:
brokerUris:
- 192.168.10.4:9092
zookeeperUris:
- 192.168.10.4:2181
healthCheckTopic: healthcheck
middleware:
enabled: true
serverVIP: 192.168.10.5
serverPort: 5000
connTimeout: 500
connSSLClientAuth: false
connPoolMaxActive: 3
connPoolMaxIdle: 3
connPoolEvictPeriod: 600000
connPoolMinIdleTime: 600000
connRetryTimes: 2
connRetryInterval: 50
defaultAuthorizedRoles: [user, domainuser, domainadmin, monasca-user]
agentAuthorizedRoles: [monasca-agent]
adminAuthMethod: password
adminUser: admin
adminPassword: admin
adminProjectId:
adminProjectName:
adminToken:
timeToCacheToken: 600
maxTokenCacheSize: 1048576
server:
applicationConnectors:
- type: http
maxRequestHeaderSize: 16KiB # Allow large headers used by keystone tokens
# Logging settings.
logging:
# The default level of all loggers. Can be OFF, ERROR, WARN, INFO, DEBUG, TRACE, or ALL.
level: debug
# Logger-specific levels.
loggers:
# Sets the level for 'com.example.app' to DEBUG.
com.example.app: DEBUG
appenders:
- type: console
threshold: ALL
timeZone: UTC
target: stdout
logFormat: # TODO
- type: file
currentLogFilename: /var/log/monasca/monasca-api.log
threshold: ALL
archive: true
archivedLogFilenamePattern: /var/log/monasca/monasca-api-%d.log.gz
archivedFileCount: 5
timeZone: UTC
logFormat: # TODO
- type: syslog
host: 192.168.10.4
port: 514
facility: local0
threshold: ALL
logFormat: # TODO

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api;
import io.dropwizard.Configuration;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.NotEmpty;
import monasca.common.messaging.kafka.KafkaConfiguration;
import monasca.log.api.infrastructure.middleware.MiddlewareConfiguration;
public class ApiConfig extends Configuration {
@NotEmpty
public String region;
@NotEmpty
public String logTopic = "log";
@Valid
@NotNull
public KafkaConfiguration kafka;
@Valid
@NotNull
public MiddlewareConfiguration middleware;
}

View File

@ -0,0 +1,221 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api;
import io.dropwizard.Application;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.servlet.FilterRegistration.Dynamic;
import javax.ws.rs.ext.ExceptionMapper;
import org.eclipse.jetty.servlets.CrossOriginFilter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import monasca.common.middleware.AuthConstants;
import monasca.common.middleware.TokenAuth;
import monasca.common.util.Injector;
import monasca.log.api.infrastructure.servlet.MockAuthenticationFilter;
import monasca.log.api.infrastructure.servlet.PostAuthenticationFilter;
import monasca.log.api.infrastructure.servlet.PreAuthenticationFilter;
import monasca.log.api.infrastructure.servlet.RoleAuthorizationFilter;
import monasca.log.api.resource.LogResource;
import monasca.log.api.resource.exception.ConstraintViolationExceptionMapper;
import monasca.log.api.resource.exception.IllegalArgumentExceptionMapper;
import monasca.log.api.resource.exception.JsonMappingExceptionManager;
import monasca.log.api.resource.exception.JsonProcessingExceptionMapper;
import monasca.log.api.resource.exception.ThrowableExceptionMapper;
/**
* Monitoring API application.
*/
public class MonApiApplication extends Application<ApiConfig> {
public static void main(String[] args) throws Exception {
/*
* This should allow command line options to show the current version
* java -jar monasca-log-api.jar --version
* java -jar monasca-log-api.jar -version
* java -jar monasca-log-api.jar version
* Really anything with the word version in it will show the
* version as long as there is only one argument
* */
if (args.length == 1 && args[0].toLowerCase().contains("version")) {
showVersion();
System.exit(0);
}
new MonApiApplication().run(args);
}
private static void showVersion() {
Package pkg;
pkg = Package.getPackage("monasca.log.api");
System.out.println("-------- Version Information --------");
System.out.println(pkg.getImplementationVersion());
}
@Override
public void initialize(Bootstrap<ApiConfig> bootstrap) {
}
@Override
public String getName() {
return "Fujitsu LOG service";
}
@Override
@SuppressWarnings("unchecked")
public void run(ApiConfig config, Environment environment) throws Exception {
/** Wire services */
Injector.registerModules(new MonApiModule(config));
/** Configure resources */
environment.jersey().register(Injector.getInstance(LogResource.class));
/** Configure providers */
removeExceptionMappers(environment.jersey().getResourceConfig().getSingletons());
environment.jersey().register(new IllegalArgumentExceptionMapper());
environment.jersey().register(new JsonProcessingExceptionMapper());
environment.jersey().register(new JsonMappingExceptionManager());
environment.jersey().register(new ConstraintViolationExceptionMapper());
environment.jersey().register(new ThrowableExceptionMapper<Throwable>() {});
/** Configure Jackson */
environment.getObjectMapper().setPropertyNamingStrategy(
PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
environment.getObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
environment.getObjectMapper().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
SimpleModule module = new SimpleModule("SerializationModule");
environment.getObjectMapper().registerModule(module);
/** Configure CORS filter */
Dynamic corsFilter = environment.servlets().addFilter("cors", CrossOriginFilter.class);
corsFilter.addMappingForUrlPatterns(null, true, "/*");
corsFilter.setInitParameter("allowedOrigins", "*");
corsFilter.setInitParameter("allowedHeaders",
"X-Requested-With,Content-Type,Accept,Origin,X-Auth-Token");
corsFilter.setInitParameter("allowedMethods", "OPTIONS,GET,HEAD");
if (config.middleware.enabled) {
ensureHasValue(config.middleware.serverVIP, "serverVIP", "enabled", "true");
ensureHasValue(config.middleware.serverPort, "serverPort", "enabled", "true");
ensureHasValue(config.middleware.adminAuthMethod, "adminAuthMethod", "enabled", "true");
if ("password".equalsIgnoreCase(config.middleware.adminAuthMethod)) {
ensureHasValue(config.middleware.adminUser, "adminUser", "adminAuthMethod", "password");
ensureHasValue(config.middleware.adminPassword, "adminPassword", "adminAuthMethod", "password");
} else if ("token".equalsIgnoreCase(config.middleware.adminAuthMethod)) {
ensureHasValue(config.middleware.adminToken, "adminToken", "adminAuthMethod", "token");
} else {
throw new Exception(String.format(
"Invalid value '%s' for adminAuthMethod. Must be either password or token",
config.middleware.adminAuthMethod));
}
if (config.middleware.defaultAuthorizedRoles == null || config.middleware.defaultAuthorizedRoles.isEmpty()) {
ensureHasValue(null, "defaultAuthorizedRoles", "enabled", "true");
}
if (config.middleware.connSSLClientAuth) {
ensureHasValue(config.middleware.keystore, "keystore", "connSSLClientAuth", "true");
ensureHasValue(config.middleware.keystorePassword, "keystorePassword", "connSSLClientAuth", "true");
}
Map<String, String> authInitParams = new HashMap<String, String>();
authInitParams.put("ServerVIP", config.middleware.serverVIP);
authInitParams.put("ServerPort", config.middleware.serverPort);
authInitParams.put(AuthConstants.USE_HTTPS, String.valueOf(config.middleware.useHttps));
authInitParams.put("ConnTimeout", config.middleware.connTimeout);
authInitParams.put("ConnSSLClientAuth", String.valueOf(config.middleware.connSSLClientAuth));
authInitParams.put("ConnPoolMaxActive", config.middleware.connPoolMaxActive);
authInitParams.put("ConnPoolMaxIdle", config.middleware.connPoolMaxActive);
authInitParams.put("ConnPoolEvictPeriod", config.middleware.connPoolEvictPeriod);
authInitParams.put("ConnPoolMinIdleTime", config.middleware.connPoolMinIdleTime);
authInitParams.put("ConnRetryTimes", config.middleware.connRetryTimes);
authInitParams.put("ConnRetryInterval", config.middleware.connRetryInterval);
authInitParams.put("AdminToken", config.middleware.adminToken);
authInitParams.put("TimeToCacheToken", config.middleware.timeToCacheToken);
authInitParams.put("AdminAuthMethod", config.middleware.adminAuthMethod);
authInitParams.put("AdminUser", config.middleware.adminUser);
authInitParams.put("AdminPassword", config.middleware.adminPassword);
authInitParams.put(AuthConstants.ADMIN_PROJECT_ID, config.middleware.adminProjectId);
authInitParams.put(AuthConstants.ADMIN_PROJECT_NAME, config.middleware.adminProjectName);
authInitParams.put("MaxTokenCacheSize", config.middleware.maxTokenCacheSize);
setIfNotNull(authInitParams, AuthConstants.TRUSTSTORE, config.middleware.truststore);
setIfNotNull(authInitParams, AuthConstants.TRUSTSTORE_PASS, config.middleware.truststorePassword);
setIfNotNull(authInitParams, AuthConstants.KEYSTORE, config.middleware.keystore);
setIfNotNull(authInitParams, AuthConstants.KEYSTORE_PASS, config.middleware.keystorePassword);
/** Configure auth filters */
Dynamic preAuthenticationFilter =
environment.servlets().addFilter("pre-auth", new PreAuthenticationFilter());
preAuthenticationFilter.addMappingForUrlPatterns(null, true, "/");
preAuthenticationFilter.addMappingForUrlPatterns(null, true, "/v2.0/*");
Dynamic tokenAuthFilter = environment.servlets().addFilter("token-auth", new TokenAuth());
tokenAuthFilter.addMappingForUrlPatterns(null, true, "/");
tokenAuthFilter.addMappingForUrlPatterns(null, true, "/v2.0/*");
tokenAuthFilter.setInitParameters(authInitParams);
Dynamic postAuthenticationFilter =
environment.servlets().addFilter(
"post-auth",
new PostAuthenticationFilter(config.middleware.defaultAuthorizedRoles,
config.middleware.agentAuthorizedRoles));
postAuthenticationFilter.addMappingForUrlPatterns(null, true, "/");
postAuthenticationFilter.addMappingForUrlPatterns(null, true, "/v2.0/*");
environment.jersey().getResourceConfig().getContainerRequestFilters()
.add(new RoleAuthorizationFilter());
} else {
Dynamic mockAuthenticationFilter =
environment.servlets().addFilter("mock-auth", new MockAuthenticationFilter());
mockAuthenticationFilter.addMappingForUrlPatterns(null, true, "/");
mockAuthenticationFilter.addMappingForUrlPatterns(null, true, "/v2.0/*");
}
}
private void ensureHasValue(final String value, final String what, final String control,
final String controlValue) throws Exception {
if (value == null || value.isEmpty()) {
final String message =
String
.format(
"Since %s in middleware section of configuration file is set to %s, %s must have a value",
control, controlValue, what);
throw new Exception(message);
}
}
private void setIfNotNull(Map<String, String> authInitParams, String name, String value) {
if (value != null) {
authInitParams.put(name, value);
}
}
private void removeExceptionMappers(Set<Object> items) {
for (Iterator<Object> i = items.iterator(); i.hasNext();) {
Object o = i.next();
if (o instanceof ExceptionMapper)
i.remove();
}
}
}

View File

@ -0,0 +1,55 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api;
import java.util.Properties;
import javax.inject.Singleton;
import kafka.javaapi.producer.Producer;
import kafka.producer.ProducerConfig;
import com.google.common.base.Joiner;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import monasca.log.api.app.ApplicationModule;
/**
* Monitoring API server bindings.
*/
public class MonApiModule extends AbstractModule {
private final ApiConfig config;
public MonApiModule(ApiConfig config) {
this.config = config;
}
@Override
protected void configure() {
bind(ApiConfig.class).toInstance(config);
install(new ApplicationModule());
}
@Provides
@Singleton
public Producer<String, String> getProducer() {
Properties props = new Properties();
props.put("metadata.broker.list", Joiner.on(',').join(config.kafka.brokerUris));
props.put("serializer.class", "kafka.serializer.StringEncoder");
props.put("request.required.acks", "1");
ProducerConfig config = new ProducerConfig(props);
return new Producer<String, String>(config);
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.app;
import javax.inject.Singleton;
import com.google.inject.AbstractModule;
/**
* Application layer bindings.
*/
public class ApplicationModule extends AbstractModule {
@Override
protected void configure() {
bind(LogService.class).in(Singleton.class);
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright 2015 Fujitsu Limited
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.app;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.inject.Inject;
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import monasca.log.api.ApiConfig;
import monasca.log.api.model.Log;
import monasca.log.api.model.LogEnvelope;
import monasca.log.api.model.LogEnvelopes;
/**
* Log service implementation.
*/
public class LogService {
private static final Comparator<Map.Entry<String, String>> comparator;
private final ApiConfig config;
private final Producer<String, String> producer;
static {
comparator = new Comparator<Map.Entry<String, String>>() {
@Override
public int compare(Entry<String, String> o1, Entry<String, String> o2) {
int nameCmp = o1.getKey().compareTo(o2.getKey());
return (nameCmp != 0 ? nameCmp : o1.getValue().compareTo(o2.getValue()));
}
};
}
@Inject
public LogService(ApiConfig config, Producer<String, String> producer) {
this.config = config;
this.producer = producer;
}
public void create(Log log, String tenantId) {
Builder<String, Object> metaBuilder = new ImmutableMap.Builder<String, Object>().put("tenantId", tenantId).put("region", this.config.region);
ImmutableMap<String, Object> meta = metaBuilder.build();
LogEnvelope envelope = new LogEnvelope(log, meta);
String key = buildKey(tenantId, log);
String json = LogEnvelopes.toJson(envelope);
String topic = this.config.logTopic;
this.producer.send(new KeyedMessage<>(topic, key, json));
}
private String buildKey(String tenantId, Log log) {
final StringBuilder key = new StringBuilder();
// appends tenantId
key.append(tenantId);
// appends applicationType
if (log.applicationType != null && !log.applicationType.isEmpty()) {
key.append(log.applicationType);
}
// appends dimensions
if (log.dimensions != null && !log.dimensions.isEmpty()) {
for (final Map.Entry<String, String> dim : buildSortedDimSet(log.dimensions)) {
key.append(dim.getKey());
key.append(dim.getValue());
}
}
return key.toString();
}
// Key must be the same for the same log so sort the dimensions so they will
// be
// in a known order
private List<Map.Entry<String, String>> buildSortedDimSet(final Map<String, String> dimMap) {
final List<Map.Entry<String, String>> dims = new ArrayList<>(dimMap.entrySet());
Collections.sort(dims, comparator);
return dims;
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright 2015 Fujitsu Limited
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.app.command;
import java.util.Map;
import javax.annotation.Nullable;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
import monasca.log.api.app.validation.DimensionValidation;
import monasca.log.api.app.validation.LogApplicationTypeValidator;
import monasca.log.api.model.Log;
import monasca.log.api.resource.exception.Exceptions;
public class CreateLogCommand {
public static final int MAX_NAME_LENGTH = 255;
public static final int MAX_LOG_LENGTH = 1024 * 1024;
@NotEmpty
@Size(min = 1, max = MAX_NAME_LENGTH)
public String applicationType;
public Map<String, String> dimensions;
public String message;
public CreateLogCommand() {}
public CreateLogCommand(@Nullable String applicationType, @Nullable Map<String, String> dimensions, String message) {
setApplicationType(applicationType);
setDimensions(dimensions);
this.message = message;
}
public void setDimensions(Map<String, String> dimensions) {
// White spaces have been already trimmed, but normalize just in case.
this.dimensions = DimensionValidation.normalize(dimensions);
}
public void setApplicationType(String applicationType) {
this.applicationType = LogApplicationTypeValidator.normalize(applicationType);
}
public Log toLog() {
return new Log(applicationType, dimensions, message);
}
public void validate() {
// Validate applicationType
if (applicationType != null && !applicationType.isEmpty()) {
LogApplicationTypeValidator.validate(applicationType);
}
// Validate dimensions
if (dimensions != null) {
DimensionValidation.validate(dimensions, null);
}
// Validate log message
if (message.length() > MAX_LOG_LENGTH)
throw Exceptions.unprocessableEntity("Log must be %d characters or less", MAX_LOG_LENGTH);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
CreateLogCommand other = (CreateLogCommand) obj;
if (dimensions == null) {
if (other.dimensions != null)
return false;
} else if (!dimensions.equals(other.dimensions))
return false;
if (applicationType == null) {
if (other.applicationType != null)
return false;
} else if (!applicationType.equals(other.applicationType))
return false;
if (!message.equals(other.message))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((dimensions == null) ? 0 : dimensions.hashCode());
result = prime * result + ((applicationType == null) ? 0 : applicationType.hashCode());
result = prime * result * message.hashCode();
return result;
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
/**
* Houses the application/service layer.
*
* @see http://martinfowler.com/eaaCatalog/serviceLayer.html
*/
package monasca.log.api.app;

View File

@ -0,0 +1,182 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.app.validation;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.ws.rs.WebApplicationException;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import com.google.common.primitives.Ints;
import monasca.common.model.Services;
import monasca.log.api.resource.exception.Exceptions;
/**
* Utilities for validating dimensions.
*/
public final class DimensionValidation {
private static final Map<String, DimensionValidator> VALIDATORS;
private static final Pattern UUID_PATTERN = Pattern
.compile("\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}");
private static final Pattern VALID_DIMENSION_NAME = Pattern.compile("[^><={}(), '\";&]+$");
private static final String INVALID_CHAR_STRING = "> < = { } ( ) ' \" , ; &";
private DimensionValidation() {}
interface DimensionValidator {
boolean isValidDimension(String name, String value);
}
static {
VALIDATORS = new HashMap<String, DimensionValidator>();
// Compute validator
VALIDATORS.put(Services.COMPUTE_SERVICE, new DimensionValidator() {
@Override
public boolean isValidDimension(String name, String value) {
if ("instance_id".equals(name))
return value.length() != 36 || UUID_PATTERN.matcher(value).matches();
if ("az".equals(name))
return Ints.tryParse(value) != null;
return true;
}
});
// Objectstore validator
VALIDATORS.put(Services.OBJECT_STORE_SERVICE, new DimensionValidator() {
@Override
public boolean isValidDimension(String name, String value) {
if ("container".equals(name))
return value.length() < 256 || !value.contains("/");
return true;
}
});
// Volume validator
VALIDATORS.put(Services.VOLUME_SERVICE, new DimensionValidator() {
@Override
public boolean isValidDimension(String name, String value) {
if ("instance_id".equals(name))
return value.length() != 36 || UUID_PATTERN.matcher(value).matches();
if ("az".equals(name))
return Ints.tryParse(value) != null;
return true;
}
});
}
/**
* Normalizes dimensions by stripping whitespace.
*/
public static Map<String, String> normalize(Map<String, String> dimensions) {
if (dimensions == null)
return null;
Map<String, String> result = new HashMap<>();
for (Map.Entry<String, String> dimension : dimensions.entrySet()) {
String dimensionKey = null;
if (dimension.getKey() != null) {
dimensionKey = CharMatcher.WHITESPACE.trimFrom(dimension.getKey());
if (dimensionKey.isEmpty())
dimensionKey = null;
}
String dimensionValue = null;
if (dimension.getValue() != null) {
dimensionValue = CharMatcher.WHITESPACE.trimFrom(dimension.getValue());
if (dimensionValue.isEmpty())
dimensionValue = null;
}
result.put(dimensionKey, dimensionValue);
}
return result;
}
/**
* Validates that the given {@code dimensions} are valid.
*
* @throws WebApplicationException if validation fails
*/
public static void validate(Map<String, String> dimensions, @Nullable String service) {
// Validate dimension names and values
for (Map.Entry<String, String> dimension : dimensions.entrySet()) {
String name = dimension.getKey();
String value = dimension.getValue();
// General validations
if (Strings.isNullOrEmpty(name))
throw Exceptions.unprocessableEntity("Dimension name cannot be empty");
if (Strings.isNullOrEmpty(value))
throw Exceptions.unprocessableEntity("Dimension %s cannot have an empty value", name);
if (name.length() > 255)
throw Exceptions.unprocessableEntity("Dimension name %s must be 255 characters or less",
name);
if (value.length() > 255)
throw Exceptions.unprocessableEntity("Dimension value %s must be 255 characters or less",
value);
// Dimension names that start with underscores are reserved for internal use only.
if (name.startsWith("_")) {
throw Exceptions.unprocessableEntity("Dimension name cannot start with underscore (_)",
name);
}
if (!VALID_DIMENSION_NAME.matcher(name).matches())
throw Exceptions.unprocessableEntity(
"Dimension name %s may not contain: %s", name, INVALID_CHAR_STRING);
// Service specific validations
if (service != null) {
if (!name.equals(Services.SERVICE_DIMENSION)
&& !Services.isValidDimensionName(service, name))
throw Exceptions.unprocessableEntity("%s is not a valid dimension name for service %s",
name, service);
DimensionValidator validator = VALIDATORS.get(service);
if (validator != null && !validator.isValidDimension(name, value))
throw Exceptions.unprocessableEntity("%s is not a valid dimension value for service %s",
value, service);
}
}
}
/**
* Validates a list of dimension names
* @param names
*/
public static void validateNames(List<String> names) {
if(names != null) {
for (String name : names) {
if (Strings.isNullOrEmpty(name)) {
throw Exceptions.unprocessableEntity("Dimension name cannot be empty");
}
if (name.length() > 255) {
throw Exceptions.unprocessableEntity("Dimension name '%s' must be 255 characters or less",
name);
}
// Dimension names that start with underscores are reserved for internal use only.
if (name.startsWith("_")) {
throw Exceptions.unprocessableEntity("Dimension name '%s' cannot start with underscore (_)",
name);
}
if (!VALID_DIMENSION_NAME.matcher(name).matches())
throw Exceptions.unprocessableEntity(
"Dimension name '%s' may not contain: %s", name, INVALID_CHAR_STRING);
}
}
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2015 Fujitsu Limited
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.app.validation;
import java.util.regex.Pattern;
import javax.ws.rs.WebApplicationException;
import com.google.common.base.CharMatcher;
import monasca.log.api.app.command.CreateLogCommand;
import monasca.log.api.resource.exception.Exceptions;
/**
* Utilities for validating log application types.
*/
public class LogApplicationTypeValidator {
private static final Pattern VALID_APPLICATION_TYPE = Pattern.compile("^[a-zA-Z0-9_\\.\\-]+$");
private LogApplicationTypeValidator() {}
/**
* Normalizes the {@code applicationType} by removing whitespace.
*/
public static String normalize(String applicationType) {
return applicationType == null ? null : CharMatcher.WHITESPACE.trimFrom(applicationType);
}
/**
* Validates the {@code applicationType} for the character constraints.
*
* @throws WebApplicationException if validation fails
*/
public static void validate(String applicationType) {
if (applicationType.length() > CreateLogCommand.MAX_NAME_LENGTH)
throw Exceptions.unprocessableEntity("Application type %s must be %d characters or less", applicationType, CreateLogCommand.MAX_NAME_LENGTH);
if (!VALID_APPLICATION_TYPE.matcher(applicationType).matches())
throw Exceptions.unprocessableEntity("Application type %s may only contain: a-z A-Z 0-9 _ - .", applicationType);
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.app.validation;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.WebApplicationException;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import monasca.log.api.resource.exception.Exceptions;
/**
* Validation related utilities.
*/
public final class Validation {
private static final Splitter COMMA_SPLITTER_FOR_LOG = Splitter.on(',').trimResults();
private Validation() {}
/**
* @throws WebApplicationException if the {@code value} is null or empty.
*/
public static Map<String, String> parseLogDimensions(String dimensionsStr) {
Validation.validateNotNullOrEmpty(dimensionsStr, "dimensions");
Map<String, String> dimensions = new HashMap<String, String>();
for (String dimensionStr : COMMA_SPLITTER_FOR_LOG.split(dimensionsStr)) {
if (dimensionStr.isEmpty())
throw Exceptions.unprocessableEntity("Dimension cannot be empty");
int index = dimensionStr.indexOf(':');
if (index == -1)
throw Exceptions.unprocessableEntity("%s is not a valid dimension", dimensionStr);
String dimensionKey = dimensionStr.substring(0, index);
String dimensionValue = dimensionStr.substring(index + 1);
dimensions.put(dimensionKey, dimensionValue);
}
return dimensions;
}
/**
* @throws WebApplicationException if the {@code value} is null or empty.
*/
public static void validateNotNullOrEmpty(String value, String parameterName) {
if (Strings.isNullOrEmpty(value))
throw Exceptions.unprocessableEntity("%s is required", parameterName);
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.app.validation;
import com.google.common.base.CharMatcher;
import com.google.common.base.Strings;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.WebApplicationException;
import monasca.log.api.resource.exception.Exceptions;
/**
* Utilities for validating valueMeta.
*/
public final class ValueMetaValidation {
private static final int VALUE_META_MAX_NUMBER = 16;
private static final int VALUE_META_VALUE_MAX_LENGTH = 2048;
private static final int VALUE_META_NAME_MAX_LENGTH = 255;
private static final Map<String, String> EMPTY_VALUE_META = Collections
.unmodifiableMap(new HashMap<String, String>());
private ValueMetaValidation() {}
/**
* Normalizes valueMeta by stripping whitespace from name. validate() must
* already have been called on the valueMeta
*/
public static Map<String, String> normalize(Map<String, String> valueMeta) {
if (valueMeta == null || valueMeta.isEmpty()) {
return EMPTY_VALUE_META;
}
final Map<String, String> result = new HashMap<>();
for (Map.Entry<String, String> entry : valueMeta.entrySet()) {
final String key = CharMatcher.WHITESPACE.trimFrom(entry.getKey());
result.put(key, entry.getValue());
}
return result;
}
/**
* Validates that the given {@code valueMetas} are valid.
*
* @throws WebApplicationException if validation fails
*/
public static void validate(Map<String, String> valueMetas) {
if (valueMetas.size() > VALUE_META_MAX_NUMBER) {
throw Exceptions.unprocessableEntity("Maximum number of valueMeta key/value parirs is %d",
VALUE_META_MAX_NUMBER);
}
// Validate valueMeta names and values
for (Map.Entry<String, String> valueMeta : valueMetas.entrySet()) {
// Have to check for null first because later check is for trimmed name
if (valueMeta.getKey() == null) {
throw Exceptions.unprocessableEntity("valueMeta name cannot be empty");
}
final String name = CharMatcher.WHITESPACE.trimFrom(valueMeta.getKey());
String value = valueMeta.getValue();
if (value == null) {
// Store nulls as empty strings
value = "";
}
// General validations
if (Strings.isNullOrEmpty(name)) {
throw Exceptions.unprocessableEntity("valueMeta name cannot be empty");
}
if (name.length() > VALUE_META_NAME_MAX_LENGTH) {
throw Exceptions.unprocessableEntity("valueMeta name %s must be %d characters or less",
name, VALUE_META_NAME_MAX_LENGTH);
}
if (value.length() > VALUE_META_VALUE_MAX_LENGTH) {
throw Exceptions.unprocessableEntity("valueMeta value %s must be %d characters or less",
value, VALUE_META_VALUE_MAX_LENGTH);
}
}
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.infrastructure.middleware;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* CS Middleware configuration.
*/
public class MiddlewareConfiguration {
public Boolean enabled = false;
@JsonProperty
public String serverVIP;
@JsonProperty
public String serverPort;
@JsonProperty
public Boolean useHttps = Boolean.FALSE;
@JsonProperty
public String connTimeout = "500";
@JsonProperty
public Boolean connSSLClientAuth = Boolean.FALSE;
@JsonProperty
public String connPoolMaxActive = "3";
@JsonProperty
public String connPoolMaxIdle = "3";
@JsonProperty
public String connPoolEvictPeriod = "600000";
@JsonProperty
public String connPoolMinIdleTime = "600000";
@JsonProperty
public String connRetryTimes = "2";
@JsonProperty
public String connRetryInterval = "50";
@JsonProperty
public List<String> defaultAuthorizedRoles;
@JsonProperty
public List<String> agentAuthorizedRoles;
@JsonProperty
public String delegateAuthorizedRole;
@JsonProperty
public String timeToCacheToken = "600";
@JsonProperty
public String adminAuthMethod;
@JsonProperty
public String adminUser;
@JsonProperty
public String adminToken;
@JsonProperty
public String adminPassword;
@JsonProperty
public String adminProjectId = "";
@JsonProperty
public String adminProjectName = "";
@JsonProperty
public String maxTokenCacheSize = "1048576";
@JsonProperty
public String truststore;
@JsonProperty
public String truststorePassword;
@JsonProperty
public String keystore;
@JsonProperty
public String keystorePassword;
}

View File

@ -0,0 +1,93 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.infrastructure.servlet;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
* Mocks authentication by converting X-Auth-Token headers to X-Tenant-Ids.
*/
public class MockAuthenticationFilter implements Filter {
private static final String X_AUTH_TOKEN_HEADER = "X-Auth-Token";
@Override
public void destroy() {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
final HttpServletRequest req = (HttpServletRequest) request;
HttpServletRequestWrapper wrapper = requestWrapperFor(req);
chain.doFilter(wrapper, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
/**
* Returns an HttpServletRequestWrapper that serves tenant id headers from request attributes.
*/
private HttpServletRequestWrapper requestWrapperFor(final HttpServletRequest request) {
return new HttpServletRequestWrapper(request) {
@Override
public Object getAttribute(String name) {
if (name.equalsIgnoreCase(PostAuthenticationFilter.X_TENANT_ID_HEADER)) {
String tenantId = request.getHeader(PostAuthenticationFilter.X_TENANT_ID_HEADER);
return tenantId == null ? request.getHeader(X_AUTH_TOKEN_HEADER) : tenantId;
}
if (name.equalsIgnoreCase(PostAuthenticationFilter.X_IDENTITY_STATUS_ATTRIBUTE))
return PostAuthenticationFilter.CONFIRMED_STATUS;
if (name.equalsIgnoreCase(PostAuthenticationFilter.X_ROLES_ATTRIBUTE))
return "user";
return super.getAttribute(name);
}
@Override
public String getHeader(String name) {
if (name.equalsIgnoreCase(PostAuthenticationFilter.X_TENANT_ID_HEADER))
return request.getHeader(X_AUTH_TOKEN_HEADER);
return super.getHeader(name);
}
@Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
names.add(PostAuthenticationFilter.X_TENANT_ID_HEADER);
return Collections.enumeration(names);
}
@Override
public Enumeration<String> getHeaders(String name) {
if (name.equalsIgnoreCase(PostAuthenticationFilter.X_TENANT_ID_HEADER)) {
String authToken = request.getHeader(X_AUTH_TOKEN_HEADER);
return authToken == null ? Collections.<String>emptyEnumeration() : Collections
.enumeration(Collections.singleton(authToken));
}
return super.getHeaders(name);
}
};
}
}

View File

@ -0,0 +1,192 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.infrastructure.servlet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import javax.annotation.Nullable;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;
import monasca.log.api.infrastructure.servlet.PreAuthenticationFilter.ErrorCapturingServletResponseWrapper;
/**
* Authenticates requests using header information from the CsMiddleware. Provides the X-TENANT-ID
* servlet attribute as a request header. Intended to be added to a servlet filter chain after the
* CsMiddleware TokenAuth filter.
*/
public class PostAuthenticationFilter implements Filter {
static final String CONFIRMED_STATUS = "CONFIRMED";
static final String X_ROLES_ATTRIBUTE = "X-ROLES";
static final String X_MONASCA_AGENT = "X-MONASCA_AGENT";
static final String X_IDENTITY_STATUS_ATTRIBUTE = "X-IDENTITY-STATUS";
private static final String X_TENANT_ID_ATTRIBUTE = "X-PROJECT-ID";
static final String X_TENANT_ID_HEADER = "X-Tenant-Id";
static final String X_ROLES_HEADER = "X-Roles";
private final List<String> defaultAuthorizedRoles = new ArrayList<String>();
private final List<String> agentAuthorizedRoles = new ArrayList<String>();
public PostAuthenticationFilter(List<String> defaultAuthorizedRoles,
List<String> agentAuthorizedRoles) {
for (String defaultRole : defaultAuthorizedRoles) {
this.defaultAuthorizedRoles.add(defaultRole.toLowerCase());
}
for (String agentRole : agentAuthorizedRoles) {
this.agentAuthorizedRoles.add(agentRole.toLowerCase());
}
}
@Override
public void destroy() {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
final HttpServletRequest req = (HttpServletRequest) request;
ErrorCapturingServletResponseWrapper res = (ErrorCapturingServletResponseWrapper) response;
String tenantIdStr = null;
try {
// According to CORS spec OPTIONS method does not pass auth info
if (req.getMethod().equals("OPTIONS")) {
chain.doFilter(request, response);
return;
}
Object tenantId = request.getAttribute(X_TENANT_ID_ATTRIBUTE);
if (tenantId == null) {
sendAuthError(res, null, null, null);
return;
}
tenantIdStr = tenantId.toString();
boolean authenticated = isAuthenticated(req);
boolean authorized = isAuthorized(req);
if (authenticated && authorized) {
HttpServletRequestWrapper wrapper = requestWrapperFor(req);
chain.doFilter(wrapper, response);
return;
}
if (authorized)
sendAuthError(res, tenantIdStr, null, null);
else
sendAuthError(res, tenantIdStr, "Tenant is missing a required role to access this service",
null);
} catch (Exception e) {
try {
sendAuthError(res, tenantIdStr, null, e);
} catch (IOException ignore) {
}
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
/**
* @return true if the request is authenticated else false
*/
private boolean isAuthenticated(HttpServletRequest request) {
Object identityStatus = request.getAttribute(X_IDENTITY_STATUS_ATTRIBUTE);
return identityStatus != null && CONFIRMED_STATUS.equalsIgnoreCase(identityStatus.toString());
}
/**
* @return true if the request is authorized else false
*/
private boolean isAuthorized(HttpServletRequest request) {
Object rolesFromKeystone = request.getAttribute(X_ROLES_ATTRIBUTE);
if (rolesFromKeystone == null)
return false;
boolean agentUser = false;
for (String role : rolesFromKeystone.toString().split(",")) {
String lowerCaseRole = role.toLowerCase();
if ((defaultAuthorizedRoles != null) && defaultAuthorizedRoles.contains(lowerCaseRole)) {
return true;
}
if ((agentAuthorizedRoles != null) && agentAuthorizedRoles.contains(lowerCaseRole)) {
agentUser = true;
}
}
if (agentUser) {
request.setAttribute(X_MONASCA_AGENT, true);
return true;
}
return false;
}
/**
* Returns an HttpServletRequestWrapper that serves tenant id headers from request attributes.
*/
private HttpServletRequestWrapper requestWrapperFor(final HttpServletRequest request) {
return new HttpServletRequestWrapper(request) {
@Override
public String getHeader(String name) {
if (name.equalsIgnoreCase(X_TENANT_ID_HEADER))
return request.getAttribute(X_TENANT_ID_ATTRIBUTE).toString();
else if (name.equalsIgnoreCase(X_ROLES_HEADER))
return request.getAttribute(X_ROLES_ATTRIBUTE).toString();
return super.getHeader(name);
}
@Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
names.add(X_TENANT_ID_HEADER);
names.add(X_ROLES_HEADER);
return Collections.enumeration(names);
}
@Override
public Enumeration<String> getHeaders(String name) {
if (name.equalsIgnoreCase(X_TENANT_ID_HEADER))
return Collections.enumeration(Collections.singleton(request.getAttribute(
X_TENANT_ID_ATTRIBUTE).toString()));
else if (name.equalsIgnoreCase(X_ROLES_HEADER))
return Collections.enumeration(Collections.singleton(request.getAttribute(
X_ROLES_ATTRIBUTE).toString()));
return super.getHeaders(name);
}
};
}
private void sendAuthError(ErrorCapturingServletResponseWrapper response,
@Nullable String tenantId, @Nullable String message, @Nullable Exception exception)
throws IOException {
response.setContentType(MediaType.APPLICATION_JSON);
if (message == null)
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
tenantId == null ? "Failed to authenticate request"
: "Failed to authenticate request for " + tenantId, exception);
else
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, String.format(message, tenantId));
}
}

View File

@ -0,0 +1,134 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.infrastructure.servlet;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.ws.rs.core.MediaType;
import org.eclipse.jetty.server.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import monasca.log.api.resource.exception.Exceptions;
import monasca.log.api.resource.exception.Exceptions.FaultType;
/**
* Authenticates requests using header information from the CsMiddleware. Provides the X-TENANT-ID
* servlet attribute as a request header. Intended to be added to a servlet filter chain after the
* CsMiddleware TokenAuth filter.
*/
public class PreAuthenticationFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(PreAuthenticationFilter.class);
static class ErrorCapturingServletResponseWrapper extends HttpServletResponseWrapper {
private int statusCode;
private String errorMessage;
private Exception exception;
public ErrorCapturingServletResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public void sendError(int statusCode) throws IOException {
this.statusCode = statusCode;
}
@Override
public void sendError(int statusCode, String msg) throws IOException {
this.statusCode = statusCode;
errorMessage = msg;
}
void sendError(int statusCode, String msg, Exception exception) throws IOException {
sendError(statusCode, msg);
this.exception = exception;
}
}
@Override
public void destroy() {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletResponse res = (HttpServletResponse) response;
ErrorCapturingServletResponseWrapper responseWrapper =
new ErrorCapturingServletResponseWrapper(res);
boolean caughtException = false;
ServletOutputStream out = null;
try {
out = res.getOutputStream();
chain.doFilter(request, responseWrapper);
if (responseWrapper.statusCode != 401 && responseWrapper.statusCode != 500)
return;
} catch (Exception e) {
LOG.error("Error while executing pre authentication filter", e);
caughtException = true;
}
try {
res.setContentType(MediaType.APPLICATION_JSON);
if (caughtException) {
res.setStatus(Response.SC_INTERNAL_SERVER_ERROR);
}
else {
res.setStatus(responseWrapper.statusCode);
FaultType faultType;
if (responseWrapper.statusCode == 500) {
faultType = FaultType.SERVER_ERROR;
}
else {
faultType = FaultType.UNAUTHORIZED;
}
String output = Exceptions.buildLoggedErrorMessage(faultType, responseWrapper.errorMessage,
null, responseWrapper.exception);
out.print(output);
}
} catch (IllegalArgumentException e) {
// CSMiddleware is throwing this error for invalid tokens.
// This problem appears to be fixed in other versions, but they are not approved yet.
try {
String output =
Exceptions.buildLoggedErrorMessage(FaultType.UNAUTHORIZED, "invalid authToken", null,
responseWrapper.exception);
out.print(output);
} catch (Exception x) {
LOG.error("Error while writing failed authentication HTTP response", x);
}
} catch (Exception e) {
LOG.error("Error while writing failed authentication HTTP response", e);
} finally {
if (out != null)
try {
out.close();
} catch (IOException ignore) {
}
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.infrastructure.servlet;
import monasca.common.middleware.AuthConstants;
import monasca.log.api.resource.exception.Exceptions;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.container.ContainerRequestFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Context;
import static monasca.log.api.infrastructure.servlet.PostAuthenticationFilter.X_MONASCA_AGENT;
public class RoleAuthorizationFilter implements ContainerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger
(ContainerRequestFilter.class);
@Context
private HttpServletRequest httpServletRequest;
private static final String[] VALID_MONASCA_AGENT_POST_PATHS = new String[] { "/v2.0/metrics", "/v2.0/log/single" };
private static final String[] VALID_MONASCA_AGENT_GET_PATHS = new String[] { "/", "/v2.0" };
@Override
public ContainerRequest filter(ContainerRequest containerRequest) {
String method = containerRequest.getMethod();
Object isAgent = httpServletRequest.getAttribute(X_MONASCA_AGENT);
String pathInfo = httpServletRequest.getPathInfo();
// X_MONASCA_AGENT is only set if the only valid role for this user is an agent role
if (isAgent != null) {
if (!(method.equals("POST") && validPath(pathInfo, VALID_MONASCA_AGENT_POST_PATHS)) &&
!(method.equals("GET") && validPath(pathInfo, VALID_MONASCA_AGENT_GET_PATHS))) {
logger.warn("User {} is missing a required role to {} on {}",
httpServletRequest.getAttribute(AuthConstants.AUTH_USER_NAME),
method, pathInfo);
throw Exceptions.badRequest("User is missing a required role to perform this request");
}
}
return containerRequest;
}
private boolean validPath(String pathInfo, String[] paths) {
// Make the comparison easier by getting rid of trailing slashes
while (!pathInfo.isEmpty() && !"/".equals(pathInfo) && pathInfo.endsWith("/")) {
pathInfo = pathInfo.substring(0, pathInfo.length() - 1);
}
for (final String validPath : paths) {
if (validPath.equals(pathInfo)) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,111 @@
/*
* Copyright 2015 FUJITSU LIMITED
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/
package monasca.log.api.model;
import java.io.Serializable;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Represents a log.
*/
public class Log
implements Serializable {
private static final long serialVersionUID = 685205295808136758L;
public String applicationType;
public Map<String, String> dimensions;
public String message;
public Log() {
}
public Log(@Nullable String applicationType, @Nullable Map<String, String> dimensions,
String message) {
this.applicationType = applicationType;
this.dimensions = dimensions;
this.message = message;
}
@Override
public String toString() {
return "Log{" + "applicationType='" + applicationType + '\'' + ", dimensions=" + dimensions
+ ", message=" + message + '}';
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof Log))
return false;
Log other = (Log) obj;
if (dimensions == null) {
if (other.dimensions != null)
return false;
} else if (!dimensions.equals(other.dimensions))
return false;
// internally handles null and empty strings as same for applicationType
if (applicationType == null) {
if (other.applicationType != null && !other.applicationType.isEmpty())
return false;
} else if (applicationType.isEmpty()) {
if (other.applicationType != null && !other.applicationType.isEmpty())
return false;
} else if (!applicationType.equals(other.applicationType))
return false;
if (!message.equals(other.message))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((dimensions == null) ? 0 : dimensions.hashCode());
result = prime * result + ((applicationType == null) ? 0 : applicationType.hashCode());
result = prime * result + message.hashCode();
return result;
}
public String getApplicationType() {
return applicationType;
}
public void setApplicationType(String applicationType) {
this.applicationType = applicationType;
}
public Map<String, String> getDimensions() {
return dimensions;
}
public void setDimensions(Map<String, String> dimensions) {
this.dimensions = dimensions;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2015 FUJITSU LIMITED
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/
package monasca.log.api.model;
import java.util.Map;
import com.google.common.base.Preconditions;
/**
* A log envelope.
*/
public class LogEnvelope {
public Log log;
public Map<String, Object> meta;
public long creationTime;
protected LogEnvelope() {
}
public LogEnvelope(Log log) {
Preconditions.checkNotNull(log, "log");
this.log = log;
this.creationTime = System.currentTimeMillis() / 1000;
}
public LogEnvelope(Log log, Map<String, Object> meta) {
this(log);
Preconditions.checkNotNull(meta, "meta");
this.meta = meta;
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2015 FUJITSU LIMITED
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/
package monasca.log.api.model;
import com.fasterxml.jackson.core.JsonProcessingException;
import monasca.common.util.Exceptions;
/**
* Utilities for working with LogEnvelopes.
*/
public final class LogEnvelopes {
private LogEnvelopes() {
}
/**
* Returns the LogEnvelope for the {@code logJson}.
*
* @throws RuntimeException if an error occurs while parsing {@code logJson}
*/
public static LogEnvelope fromJson(byte[] logJson) {
try {
String jsonStr = new String(logJson, "UTF-8");
return Logs.OBJECT_MAPPER.readValue(jsonStr, LogEnvelope.class);
} catch (Exception e) {
throw Exceptions.uncheck(e, "Failed to parse log json: %s", new String(logJson));
}
}
/**
* Returns the JSON representation of the {@code envelope} else null if it could not be converted
* to JSON.
*/
public static String toJson(LogEnvelope envelope) {
try {
return Logs.OBJECT_MAPPER.writeValueAsString(envelope);
} catch (JsonProcessingException e) {
return null;
}
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright 2015 FUJITSU LIMITED
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/
package monasca.log.api.model;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import monasca.common.util.Exceptions;
import org.apache.commons.lang3.StringEscapeUtils;
/**
* Utilities for working with Logs.
*/
public final class Logs {
static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
OBJECT_MAPPER.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
SimpleModule module = new SimpleModule();
module.addSerializer(new LogSerializer());
OBJECT_MAPPER.registerModule(module);
}
private Logs() {
}
/**
* Returns the Log for the {@code logJson}.
*
* @throws RuntimeException if an error occurs while parsing {@code logJson}
*/
public static Log fromJson(byte[] logJson) {
try {
String jsonStr = StringEscapeUtils.unescapeJava(new String(logJson, "UTF-8"));
return OBJECT_MAPPER.readValue(jsonStr, Log.class);
} catch (Exception e) {
throw Exceptions.uncheck(e, "Failed to parse log json: %s", new String(logJson));
}
}
/**
* Returns the JSON representation of the {@code log} else null if it could not be converted to
* JSON.
*/
public static String toJson(Log log) {
try {
return OBJECT_MAPPER.writeValueAsString(log);
} catch (JsonProcessingException e) {
return null;
}
}
/** Log serializer */
private static class LogSerializer
extends JsonSerializer<Log> {
@Override
public Class<Log> handledType() {
return Log.class;
}
public void serialize(Log log, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeStartObject();
if (log.applicationType != null && !log.applicationType.isEmpty()) {
jgen.writeStringField("application_type", log.applicationType);
}
if (log.dimensions != null && !log.dimensions.isEmpty()) {
jgen.writeObjectField("dimensions", log.dimensions);
}
jgen.writeStringField("message", log.message);
jgen.writeEndObject();
}
}
}

View File

@ -0,0 +1,79 @@
/*
* Copyright 2015 Fujitsu Limited
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource;
import java.util.Map;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriInfo;
import com.codahale.metrics.annotation.Timed;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import monasca.log.api.app.LogService;
import monasca.log.api.app.command.CreateLogCommand;
import monasca.log.api.app.validation.Validation;
import monasca.log.api.resource.exception.Exceptions;
/**
* Log resource implementation.
*/
@Path("/v2.0/log")
public class LogResource {
private static final String MONITORING_DELEGATE_ROLE = "monitoring-delegate";
private static final Splitter COMMA_SPLITTER = Splitter.on(',').omitEmptyStrings().trimResults();
private final LogService service;
@Inject
public LogResource(LogService service) {
this.service = service;
}
@POST
@Timed
@Consumes({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN})
@Path("/single")
public void single(@Context UriInfo uriInfo,
@HeaderParam("X-Tenant-Id") String tenantId,
@HeaderParam("X-Roles") String roles,
@HeaderParam("X-Application-Type") String applicationType,
@HeaderParam("X-Dimensions") String dimensionsStr,
@QueryParam("tenant_id") String crossTenantId, String message) {
boolean isDelegate = !Strings.isNullOrEmpty(roles) && COMMA_SPLITTER.splitToList(roles).contains(MONITORING_DELEGATE_ROLE);
Map<String, String> dimensions = Strings.isNullOrEmpty(dimensionsStr) ? null : Validation.parseLogDimensions(dimensionsStr);
CreateLogCommand command = new CreateLogCommand(applicationType, dimensions, message);
if (!isDelegate) {
if (!Strings.isNullOrEmpty(crossTenantId)) {
throw Exceptions.forbidden("Project %s cannot POST cross tenant metrics", tenantId);
}
}
command.validate();
service.create(command.toLog(), Strings.isNullOrEmpty(crossTenantId) ? tenantId : crossTenantId);
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource.exception;
import io.dropwizard.jersey.validation.ValidationErrorMessage;
import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import monasca.log.api.resource.exception.Exceptions.FaultType;
@Provider
public class ConstraintViolationExceptionMapper implements
ExceptionMapper<ConstraintViolationException> {
private static final int UNPROCESSABLE_ENTITY = 422;
@Override
public Response toResponse(ConstraintViolationException exception) {
final ValidationErrorMessage message =
new ValidationErrorMessage(exception.getConstraintViolations());
String msg =
message.getErrors().isEmpty() ? exception.getMessage() : message.getErrors().toString();
return Response.status(UNPROCESSABLE_ENTITY).type(MediaType.APPLICATION_JSON)
.entity(Exceptions.buildLoggedErrorMessage(FaultType.UNPROCESSABLE_ENTITY, msg)).build();
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource.exception;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
public class ErrorMessage {
public int code;
public String message;
public String details;
@JsonProperty("internal_code")
public String internalCode;
ErrorMessage() {}
public ErrorMessage(int code, String message, String details, String internalCode) {
Preconditions.checkNotNull(internalCode, "internalCode");
this.code = code;
this.message = message == null ? "" : message;
this.details = details == null ? "" : details;
this.internalCode = internalCode;
}
}

View File

@ -0,0 +1,169 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource.exception;
import java.util.Random;
import javax.annotation.Nullable;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.google.common.base.Splitter;
/**
* Exception factory methods.
*/
public final class Exceptions {
private static final Logger LOG = LoggerFactory.getLogger(Exceptions.class);
private static final ObjectMapper OBJECT_MAPPER;
private static final Splitter LINE_SPLITTER = Splitter.on("\n").trimResults();
private static final Random RANDOM = new Random();
static {
OBJECT_MAPPER = new ObjectMapper();
OBJECT_MAPPER
.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
}
public enum FaultType {
SERVER_ERROR(Status.INTERNAL_SERVER_ERROR, true),
BAD_REQUEST(Status.BAD_REQUEST, true),
UNAUTHORIZED(Status.UNAUTHORIZED, false),
NOT_FOUND(Status.NOT_FOUND, true),
CONFLICT(Status.CONFLICT, true),
UNPROCESSABLE_ENTITY(422, true),
FORBIDDEN(Status.FORBIDDEN, true);
public final int statusCode;
public final boolean loggable;
FaultType(int statusCode, boolean loggable) {
this.statusCode = statusCode;
this.loggable = loggable;
}
FaultType(Status status, boolean loggable) {
this.statusCode = status.getStatusCode();
this.loggable = loggable;
}
@Override
public String toString() {
return name().toLowerCase();
}
}
private static class WebAppException extends WebApplicationException {
private static final long serialVersionUID = 1L;
public WebAppException(FaultType faultType, String message) {
super(Response.status(faultType.statusCode).entity(message).type(MediaType.APPLICATION_JSON)
.build());
}
}
private Exceptions() {}
public static WebApplicationException badRequest(String msg, Object... args) {
return new WebAppException(FaultType.BAD_REQUEST, buildLoggedErrorMessage(
FaultType.BAD_REQUEST, msg, args));
}
/**
* Builds and returns an error message containing an error code, and logs the message with the
* corresponding error code.
*/
public static String buildLoggedErrorMessage(FaultType faultType, String message, Object... args) {
return buildLoggedErrorMessage(faultType,
args == null || args.length == 0 ? message : String.format(message, args), null, null);
}
/**
* Builds and returns an error message containing an error code, and logs the message with the
* corresponding error code.
*/
public static String buildLoggedErrorMessage(FaultType faultType, String message,
@Nullable String details, @Nullable Throwable exception) {
String errorCode = Long.toHexString(RANDOM.nextLong());
if (faultType.loggable) {
String withoutDetails = "{} {} - {}";
String withDetails = "{} {} - {} {}";
if (details == null) {
if (exception == null)
LOG.error(withoutDetails, faultType.name(), errorCode, message);
else
LOG.error(withoutDetails, faultType.name(), errorCode, message, exception);
} else {
if (exception == null)
LOG.error(withDetails, faultType.name(), errorCode, message, details);
else
LOG.error(withDetails, faultType.name(), errorCode, message, details, exception);
}
}
try {
StringBuilder str = new StringBuilder("{\"");
str.append(faultType.toString());
str.append("\":");
str.append(OBJECT_MAPPER.writeValueAsString(new ErrorMessage(faultType.statusCode, message,
details, errorCode)));
str.append("}");
return str.toString();
} catch (JsonProcessingException bestEffort) {
return null;
}
}
public static WebApplicationException forbidden(String msg, Object... args) {
return new WebAppException(FaultType.FORBIDDEN, buildLoggedErrorMessage(FaultType.FORBIDDEN,
msg, args));
}
/**
* Returns the first line off of a stacktrace message.
*/
public static String stripLocationFromStacktrace(String message) {
for (String s : LINE_SPLITTER.split(message))
return s;
return message;
}
/**
* Indicates that the content of a a POSTed request entity is invalid.
*/
public static WebApplicationException unprocessableEntity(String msg, Object... args) {
return new WebAppException(FaultType.UNPROCESSABLE_ENTITY, buildLoggedErrorMessage(
FaultType.UNPROCESSABLE_ENTITY, msg, args));
}
/**
* Indicates that the content of a a POSTed request entity is invalid.
*/
public static WebApplicationException unprocessableEntityDetails(String msg, String details,
Exception exception) {
return new WebAppException(FaultType.UNPROCESSABLE_ENTITY, buildLoggedErrorMessage(
FaultType.UNPROCESSABLE_ENTITY, msg, details, exception));
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource.exception;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import monasca.log.api.resource.exception.Exceptions.FaultType;
@Provider
public class IllegalArgumentExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
@Override
public Response toResponse(IllegalArgumentException e) {
return Response.status(Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON)
.entity(Exceptions.buildLoggedErrorMessage(FaultType.BAD_REQUEST, e.getMessage())).build();
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource.exception;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import com.fasterxml.jackson.databind.JsonMappingException;
import monasca.log.api.resource.exception.Exceptions.FaultType;
/**
* Adapted from Dropwizard's JsonMappingExceptionManager.
*/
@Provider
public class JsonMappingExceptionManager implements ExceptionMapper<JsonMappingException> {
@Override
public Response toResponse(JsonMappingException exception) {
return Response
.status(FaultType.BAD_REQUEST.statusCode)
.type(MediaType.APPLICATION_JSON)
.entity(
Exceptions.buildLoggedErrorMessage(FaultType.BAD_REQUEST,
"Unable to process the provided JSON",
Exceptions.stripLocationFromStacktrace(exception.getMessage()), null)).build();
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource.exception;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonProcessingException;
import monasca.log.api.resource.exception.Exceptions.FaultType;
/**
* Adapted from Dropwizard's JsonProcessingExceptionMapper.
*/
@Provider
public class JsonProcessingExceptionMapper implements ExceptionMapper<JsonProcessingException> {
@Override
public Response toResponse(JsonProcessingException exception) {
/*
* If the error is in the JSON generation, it's a server error.
*/
if (exception instanceof JsonGenerationException)
return Response
.status(Status.INTERNAL_SERVER_ERROR)
.type(MediaType.APPLICATION_JSON)
.entity(
Exceptions.buildLoggedErrorMessage(FaultType.SERVER_ERROR, "Error generating JSON",
null, exception)).build();
final String message = exception.getMessage();
/*
* If we can't deserialize the JSON because someone forgot a no-arg constructor, it's a server
* error and we should inform the developer.
*/
if (message.startsWith("No suitable constructor found"))
return Response
.status(Status.INTERNAL_SERVER_ERROR)
.type(MediaType.APPLICATION_JSON)
.entity(
Exceptions.buildLoggedErrorMessage(FaultType.SERVER_ERROR,
"Unable to deserialize the provided JSON", null, exception)).build();
/*
* Otherwise, it's those pesky users.
*/
return Response
.status(Status.BAD_REQUEST)
.type(MediaType.APPLICATION_JSON)
.entity(
Exceptions.buildLoggedErrorMessage(FaultType.BAD_REQUEST,
"Unable to process the provided JSON",
Exceptions.stripLocationFromStacktrace(message), exception)).build();
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource.exception;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
import monasca.log.api.resource.exception.Exceptions.FaultType;
/**
* Adapted from Dropwizard's LoggingExceptionMapper.
*
* @param <E> Exception type
*/
@Provider
public class ThrowableExceptionMapper<E extends Throwable> implements ExceptionMapper<E> {
@Override
public Response toResponse(E exception) {
if (exception instanceof WebApplicationException)
return ((WebApplicationException) exception).getResponse();
return Response
.status(Status.INTERNAL_SERVER_ERROR)
.type(MediaType.APPLICATION_JSON)
.entity(
Exceptions.buildLoggedErrorMessage(FaultType.SERVER_ERROR,
"An internal server error occurred", null, exception)).build();
}
}

View File

@ -0,0 +1,23 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api;
import monasca.log.api.MonApiApplication;
public class MonApiApplicationRunner {
public static void main(String... args) throws Exception {
MonApiApplication.main(new String[] {"server", "config-local.yml"});
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2015 Fujitsu Limited
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.app;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import org.mockito.Mockito;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import monasca.log.api.ApiConfig;
import monasca.log.api.model.Log;
@Test
public class LogServiceTest {
private final String APPLICATION_TYPE = "Application/Json";
private final Map<String, String> dimensions = new HashMap<String, String>();
private final String MESSAGE = "message";
private final String TENANT_ID = "Fujitsu";
private final String TOPIC = "topic";
private final String REGION = "region";
private ApiConfig config;
private Producer<String, String> producer;
LogService logService;
@BeforeMethod
protected void beforeMethod() {
dimensions.clear();
dimensions.put("a", "b");
config = new ApiConfig();
config.region = REGION;
config.logTopic = TOPIC;
producer = Mockito.mock(Producer.class);
}
public void testCreate() {
String key = "FujitsuApplication/Jsonab";
String date = Long.toString(new Date().getTime()).substring(0, 10);
String json = "{\"log\":{\"application_type\":\"Application/Json\",\"dimensions\":{\"a\":\"b\"},\"message\":\"message\"},\"meta\":{\"tenantId\":\"Fujitsu\",\"region\":\"region\"},\"creation_time\":"+ date +"}";
KeyedMessage<String, String> keyMessage = new KeyedMessage<>("topic", key, json);
Log log = new Log(APPLICATION_TYPE, dimensions, MESSAGE);
logService = new LogService(config, producer);
logService.create(log, TENANT_ID);
Mockito.verify(producer, Mockito.times(1)).send(keyMessage);
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright 2015 Fujitsu Limited
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.app.command;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;
import static org.testng.AssertJUnit.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import monasca.log.api.app.command.CreateLogCommand;
import monasca.log.api.model.Log;
@Test
public class CreateLogCommandTest {
private final String APPLICATION_TYPE = "Application/Json";
private final Map<String, String> dimensions = new HashMap<String, String>();
private final String MESSAGE = "message";
CreateLogCommand createLogCommand;
@BeforeMethod
protected void beforeMethod() {
dimensions.clear();
dimensions.put("a", "b");
createLogCommand = new CreateLogCommand(APPLICATION_TYPE, dimensions, MESSAGE);
}
public void testHashEquals() {
CreateLogCommand createLogCommandTmp1 = new CreateLogCommand(APPLICATION_TYPE, dimensions, MESSAGE);
CreateLogCommand createLogCommandTmp2 = new CreateLogCommand(APPLICATION_TYPE, dimensions, "");
CreateLogCommand createLogCommandTmp3 = new CreateLogCommand(APPLICATION_TYPE, dimensions, null);
CreateLogCommand createLogCommandTmp4 = new CreateLogCommand(APPLICATION_TYPE, null, MESSAGE);
dimensions.clear();
dimensions.put("1", "2");
CreateLogCommand createLogCommandTmp5 = new CreateLogCommand(APPLICATION_TYPE, dimensions, MESSAGE);
CreateLogCommand createLogCommandTmp6 = new CreateLogCommand(null, dimensions, MESSAGE);
CreateLogCommand createLogCommandTmp7 = new CreateLogCommand("", dimensions, MESSAGE);
assertFalse(createLogCommand.equals(new String()));
assertFalse(createLogCommand.equals(null));
assertTrue(createLogCommand.equals(createLogCommand));
assertTrue(createLogCommand.equals(createLogCommandTmp1));
assertFalse(createLogCommand.equals(createLogCommandTmp2));
assertFalse(createLogCommand.equals(createLogCommandTmp3));
assertFalse(createLogCommand.equals(createLogCommandTmp4));
assertFalse(createLogCommand.equals(createLogCommandTmp5));
assertFalse(createLogCommand.equals(createLogCommandTmp6));
assertFalse(createLogCommand.equals(createLogCommandTmp7));
assertEquals(createLogCommand.hashCode(), -446078499);
createLogCommand = new CreateLogCommand();
createLogCommand.message = "a";
assertFalse(createLogCommand.equals(new CreateLogCommand(APPLICATION_TYPE, null, "a")));
assertFalse(createLogCommand.equals(new CreateLogCommand(null, dimensions, "a")));
assertTrue(createLogCommand.equals(new CreateLogCommand(null, null, "a")));
assertEquals(createLogCommand.hashCode(), 2889727);
assertFalse(new CreateLogCommand(APPLICATION_TYPE, dimensions, "a").equals(new CreateLogCommand("", dimensions, "a")));
}
public void testSetDimensions() {
Map<String, String> validatedDimensions = new HashMap<String, String>();
validatedDimensions.put("1", "2");
dimensions.clear();
dimensions.put(" 1", "2 ");
createLogCommand.setDimensions(dimensions);
assertEquals(createLogCommand.dimensions, validatedDimensions);
}
public void testSetApplicationType() {
createLogCommand.setApplicationType(" aa ");
assertEquals(createLogCommand.applicationType, "aa");
}
public void testToLog() {
Log log = new Log(APPLICATION_TYPE, dimensions, MESSAGE);
assertEquals(createLogCommand.toLog(), log);
}
@Test(expectedExceptions = Exception.class)
public void testValidate() {
createLogCommand.validate();
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2015 Fujitsu Limited
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.app.validation;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertNull;
import static org.testng.AssertJUnit.assertTrue;
import java.io.IOException;
import javax.ws.rs.WebApplicationException;
import org.testng.annotations.Test;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import monasca.log.api.app.validation.LogApplicationTypeValidator;
@Test
public class LogApplicationTypeValidationTest {
private final String toNormalize = " Json Application ";
private final String normalized = "Json Application";
public void testNormalize() {
String result = LogApplicationTypeValidator.normalize(toNormalize);
assertEquals(normalized, result);
result = LogApplicationTypeValidator.normalize(null);
assertNull(result);
}
public void testValidateWrongFormat() throws JsonParseException, IOException {
int exceptionCount = 0;
try {
LogApplicationTypeValidator.validate(normalized);
} catch (WebApplicationException e) {
exceptionCount++;
String msg = getMessage((String) e.getResponse().getEntity());
assertEquals(msg, "Application type Json Application may only contain: a-z A-Z 0-9 _ - .");
}
assertEquals("Method throws Exception with correct message", 1, exceptionCount);
}
public void testValidateWrongLength() throws JsonParseException, IOException {
StringBuilder message = new StringBuilder();
int exceptionCount = 0;
for (int i = 0; i < 256; i++) {
message.append('a');
}
try {
LogApplicationTypeValidator.validate(message.toString());
} catch (WebApplicationException e) {
exceptionCount++;
String msg = getMessage((String) e.getResponse().getEntity());
assertTrue(msg.contains("must be 255 characters or less"));
}
assertEquals("Method throws Exception with correct message", 1, exceptionCount);
}
private String getMessage(String json) throws JsonParseException, IOException {
JsonFactory factory = new JsonFactory();
JsonParser jp = factory.createParser(json);
jp.nextToken();
while (jp.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jp.getCurrentName();
jp.nextToken();
if ("message".equals(fieldname)) {
return jp.getText();
}
}
jp.close();
return null;
}
}

View File

@ -0,0 +1,263 @@
/*
* Copyright 2015 FUJITSU LIMITED
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*
*/
package monasca.log.api.model;
import static org.testng.Assert.assertEquals;
import java.io.UnsupportedEncodingException;
import java.util.SortedMap;
import java.util.TreeMap;
import org.testng.annotations.Test;
@Test
public class LogTest {
public void shouldSerializeValue() {
String applicationType = "apache";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "WebService01");
dimensions.put("environment", "production");
String message = "Hello, world!";
Log log = new Log(applicationType, dimensions, message);
String json = Logs.toJson(log);
assertEquals(
json,
"{\"application_type\":\"apache\",\"dimensions\":{\"app_name\":\"WebService01\",\"environment\":\"production\"}," +
"\"message\":\"Hello, world!\"}");
}
public void shouldSerializeValueWithNull() {
String applicationType = null;
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "WebService01");
dimensions.put("environment", "production");
String message = "Hello, world!";
Log log = new Log(applicationType, dimensions, message);
String json = Logs.toJson(log);
assertEquals(
json,
"{\"dimensions\":{\"app_name\":\"WebService01\",\"environment\":\"production\"}," +
"\"message\":\"Hello, world!\"}");
}
public void shouldSerializeValueWithNull_2() {
String applicationType = "apache";
SortedMap<String, String> dimensions = null;
String message = "Hello, world!";
Log log = new Log(applicationType, dimensions, message);
String json = Logs.toJson(log);
assertEquals(
json,
"{\"application_type\":\"apache\"," + "\"message\":\"Hello, world!\"}");
}
public void shouldSerializeValueWithNull_3() {
String applicationType = null;
SortedMap<String, String> dimensions = null;
String message = "Hello, world!";
Log log = new Log(applicationType, dimensions, message);
String json = Logs.toJson(log);
assertEquals(
json,
"{\"message\":\"Hello, world!\"}");
}
public void shouldSerializeValueWithEmpty() {
String applicationType = "";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "WebService01");
dimensions.put("environment", "production");
String message = "Hello, world!";
Log log = new Log(applicationType, dimensions, message);
String json = Logs.toJson(log);
assertEquals(
json,
"{\"dimensions\":{\"app_name\":\"WebService01\",\"environment\":\"production\"}," +
"\"message\":\"Hello, world!\"}");
}
public void shouldSerializeValueWithEmpty_2() {
String applicationType = "apache";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "WebService01");
dimensions.put("environment", "production");
String message = "";
Log log = new Log(applicationType, dimensions, message);
String json = Logs.toJson(log);
assertEquals(
json,
"{\"application_type\":\"apache\",\"dimensions\":{\"app_name\":\"WebService01\",\"environment\":\"production\"}," +
"\"message\":\"\"}");
}
public void shouldSerializeValueWithEmpty_3() {
String applicationType = "";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "WebService01");
dimensions.put("environment", "production");
String message = "";
Log log = new Log(applicationType, dimensions, message);
String json = Logs.toJson(log);
assertEquals(
json,
"{\"dimensions\":{\"app_name\":\"WebService01\",\"environment\":\"production\"}," +
"\"message\":\"\"}");
}
public void shouldSerializeAndDeserialize() {
String applicationType = "apache";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "WebService01");
dimensions.put("environment", "production");
dimensions.put("instance_id", "123");
String message = "Hello, world!";
Log expected = new Log(applicationType, dimensions, message);
Log log = Logs.fromJson(Logs.toJson(expected).getBytes());
assertEquals(log, expected);
}
public void shouldSerializeAndDeserializeWithNull() {
String applicationType = null;
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "WebService01");
dimensions.put("environment", "production");
dimensions.put("instance_id", "123");
String message = "Hello, world!";
Log expected = new Log(applicationType, dimensions, message);
Log log = Logs.fromJson(Logs.toJson(expected).getBytes());
assertEquals(log, expected);
}
public void shouldSerializeAndDeserializeWithNull_2() {
String applicationType = "apache";
SortedMap<String, String> dimensions = null;
String message = "Hello, world!";
Log expected = new Log(applicationType, dimensions, message);
Log log = Logs.fromJson(Logs.toJson(expected).getBytes());
assertEquals(log, expected);
}
public void shouldSerializeAndDeserializeWithNull_3() {
String applicationType = null;
SortedMap<String, String> dimensions = null;
String message = "Hello, world!";
Log expected = new Log(applicationType, dimensions, message);
Log log = Logs.fromJson(Logs.toJson(expected).getBytes());
assertEquals(log, expected);
}
public void shouldSerializeAndDeserializeWithEmpty() {
String applicationType = "";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "WebService01");
dimensions.put("environment", "production");
dimensions.put("instance_id", "123");
String message = "Hello, world!";
Log expected = new Log(applicationType, dimensions, message);
Log log = Logs.fromJson(Logs.toJson(expected).getBytes());
assertEquals(log, expected);
}
public void shouldSerializeAndDeserializeWithEmpty_2() {
String applicationType = "apache";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "WebService01");
dimensions.put("environment", "production");
dimensions.put("instance_id", "123");
String message = "";
Log expected = new Log(applicationType, dimensions, message);
Log log = Logs.fromJson(Logs.toJson(expected).getBytes());
assertEquals(log, expected);
}
public void shouldSerializeAndDeserializeWithEmpty_3() {
String applicationType = "";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "WebService01");
dimensions.put("environment", "production");
dimensions.put("instance_id", "123");
String message = "";
Log expected = new Log(applicationType, dimensions, message);
Log log = Logs.fromJson(Logs.toJson(expected).getBytes());
assertEquals(log, expected);
}
public void shouldSerializeValueUTF() {
String applicationType = "foôbár";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "foôbár");
dimensions.put("instance_id", "123");
String message = "boôbár";
Log log = new Log(applicationType, dimensions, message);
String json = Logs.toJson(log);
assertEquals(
json,
"{\"application_type\":\"foôbár\",\"dimensions\":{\"app_name\":\"foôbár\",\"instance_id\":\"123\"}," +
"\"message\":\"boôbár\"}");
}
public void shouldSerializeAndDeserializeUTF8() throws UnsupportedEncodingException {
String applicationType = "foôbár";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "foôbár");
dimensions.put("environment", "production");
dimensions.put("instance_id", "123");
String message = "foôbár";
Log expected = new Log(applicationType, dimensions, message);
Log log = Logs.fromJson(Logs.toJson(expected).getBytes("UTF-8"));
assertEquals(log, expected);
}
public void shouldSerializeAndDeserializeUTF8_2() throws UnsupportedEncodingException {
String applicationType = "fo\u00f4b\u00e1r";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "fo\u00f4b\u00e1r");
dimensions.put("environment", "production");
dimensions.put("instance_id", "123");
String message = "fo\u00f4b\u00e1r";
Log expected = new Log(applicationType, dimensions, message);
Log log = Logs.fromJson(Logs.toJson(expected).getBytes("UTF-8"));
assertEquals(log, expected);
}
public void shouldSerializeAndDeserializeUTF8_3() throws UnsupportedEncodingException {
String applicationType = "fo\u00f4b\u00e1r";
String applicationType2 = "foôbár";
SortedMap<String, String> dimensions = new TreeMap<String, String>();
dimensions.put("app_name", "fo\u00f4b\u00e1r");
dimensions.put("environment", "production");
dimensions.put("instance_id", "123");
SortedMap<String, String> dimensions2 = new TreeMap<String, String>();
dimensions2.put("app_name", "foôbár");
dimensions2.put("environment", "production");
dimensions2.put("instance_id", "123");
String message = "fo\u00f4b\u00e1r";
String message2 = "foôbár";
Log expected_escaped = new Log(applicationType, dimensions, message);
Log expected_nonescaped = new Log(applicationType2, dimensions2, message2);
Log log = Logs.fromJson(Logs.toJson(expected_escaped).getBytes("UTF-8"));
assertEquals(log, expected_nonescaped);
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import monasca.common.dropwizard.AbstractResourceTest;
import monasca.log.api.resource.exception.ConstraintViolationExceptionMapper;
import monasca.log.api.resource.exception.IllegalArgumentExceptionMapper;
import monasca.log.api.resource.exception.JsonMappingExceptionManager;
import monasca.log.api.resource.exception.JsonProcessingExceptionMapper;
import monasca.log.api.resource.exception.ThrowableExceptionMapper;
/**
* Support class for monitoring resource tests.
*/
public abstract class AbstractMonApiResourceTest extends AbstractResourceTest {
@Override
protected void setupResources() throws Exception {
addSingletons(new IllegalArgumentExceptionMapper(), new JsonProcessingExceptionMapper(), new JsonMappingExceptionManager(),
new ConstraintViolationExceptionMapper(), new ThrowableExceptionMapper<Throwable>() {});
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
}
}

View File

@ -0,0 +1,701 @@
/*
* Copyright 2015 Fujitsu Limited
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.assertEquals;
import java.util.HashMap;
import java.util.Map;
import javax.ws.rs.core.MediaType;
import org.testng.annotations.Test;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import monasca.log.api.app.LogService;
import monasca.log.api.app.command.CreateLogCommand;
import monasca.log.api.model.Log;
import monasca.log.api.resource.exception.ErrorMessages;
@Test
public class LogResourceTest extends AbstractMonApiResourceTest {
private String applicationType;
private String dimensionsStr;
private Map<String, String> dimensionsMap;
private String jsonMessage;
private String textMessage;
private String tenantId;
private String longString;
private LogService service;
@Override
@SuppressWarnings("unchecked")
protected void setupResources() throws Exception {
super.setupResources();
applicationType = "apache";
dimensionsStr = "app_name:WebService01,environment:production";
dimensionsMap = new HashMap<String, String>();
dimensionsMap.put("app_name", "WebService01");
dimensionsMap.put("environment", "production");
jsonMessage = "{\n \"message\":\"Hello, world!\",\n \"from\":\"hoover\"\n}";
textMessage = "Hello, world!";
tenantId = "abc";
longString =
"12345678901234567890123456789012345678901234567890" + "12345678901234567890123456789012345678901234567890"
+ "12345678901234567890123456789012345678901234567890" + "12345678901234567890123456789012345678901234567890"
+ "12345678901234567890123456789012345678901234567890123456";
service = mock(LogService.class);
doNothing().when(service).create(any(Log.class), anyString());
addResources(new LogResource(service));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessage() {
ClientResponse response = createResponseForJson(null, null, jsonMessage);
Log log = new Log(null, null, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageByText() {
ClientResponse response = createResponseForText(null, null, jsonMessage);
Log log = new Log(null, null, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessage() {
ClientResponse response = createResponseForText(null, null, textMessage);
Log log = new Log(null, null, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageByJson() {
ClientResponse response = createResponseForJson(null, null, textMessage);
Log log = new Log(null, null, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateEmptyMessageByJson() {
ClientResponse response = createResponseForJson(null, null, "");
Log log = new Log(null, null, "");
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateEmptyMessageByText() {
ClientResponse response = createResponseForText(null, null, "");
Log log = new Log(null, null, "");
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithType() {
ClientResponse response = createResponseForJson(applicationType, null, jsonMessage);
Log log = new Log(applicationType, null, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithType() {
ClientResponse response = createResponseForText(applicationType, null, textMessage);
Log log = new Log(applicationType, null, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithEmptyType() {
ClientResponse response = createResponseForJson("", null, jsonMessage);
Log log = new Log("", null, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithEmptyType() {
ClientResponse response = createResponseForText("", null, textMessage);
Log log = new Log("", null, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithNonNumericAZType() {
ClientResponse response = createResponseForJson("azAZ19.-_", null, jsonMessage);
Log log = new Log("azAZ19.-_", null, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithNonNumericAZType() {
ClientResponse response = createResponseForText("azAZ19.-_", null, textMessage);
Log log = new Log("azAZ19.-_", null, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithSpaceType() {
ClientResponse response = createResponseForJson(" apache ", null, jsonMessage);
Log log = new Log("apache", null, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithSpaceType() {
ClientResponse response = createResponseForText(" apache ", null, textMessage);
Log log = new Log("apache", null, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithIllegalCharsInType() {
ClientResponse response = createResponseForJson("@apache", null, jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
"Application type @apache may only contain: a-z A-Z 0-9 _ - .");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithIllegalCharsInType() {
ClientResponse response = createResponseForText("@apache", null, textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
"Application type @apache may only contain: a-z A-Z 0-9 _ - .");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithTooLongType() {
String tooLongType = longString;
ClientResponse response = createResponseForJson(tooLongType, null, jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
String.format("Application type %s must be %d characters or less", tooLongType, CreateLogCommand.MAX_NAME_LENGTH));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithTooLongType() {
String tooLongType = longString;
ClientResponse response = createResponseForText(tooLongType, null, textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
String.format("Application type %s must be %d characters or less", tooLongType, CreateLogCommand.MAX_NAME_LENGTH));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithDimensions() {
ClientResponse response = createResponseForJson(null, dimensionsStr, jsonMessage);
Log log = new Log(null, dimensionsMap, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithDimensions() {
ClientResponse response = createResponseForText(null, dimensionsStr, textMessage);
Log log = new Log(null, dimensionsMap, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithEmptyDimensions() {
ClientResponse response = createResponseForJson(null, "", jsonMessage);
Log log = new Log(null, null, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithEmptyDimensions() {
ClientResponse response = createResponseForText(null, "", textMessage);
Log log = new Log(null, null, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithPartiallyEmptyDimensions() {
String partiallyEmptyDimensions = ",environment:production";
Map<String, String> expectedDimensionsMap = new HashMap<String, String>();
expectedDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForJson(null, partiallyEmptyDimensions, jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension cannot be empty");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithPartiallyEmptyDimensions() {
String partiallyEmptyDimensions = ",environment:production";
Map<String, String> expectedDimensionsMap = new HashMap<String, String>();
expectedDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForText(null, partiallyEmptyDimensions, textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension cannot be empty");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithPartiallyEmptyDimensions_2() {
String partiallyEmptyDimensions = "environment:production,";
Map<String, String> expectedDimensionsMap = new HashMap<String, String>();
expectedDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForJson(null, partiallyEmptyDimensions, jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension cannot be empty");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithPartiallyEmptyDimensions_2() {
String partiallyEmptyDimensions = "environment:production,";
Map<String, String> expectedDimensionsMap = new HashMap<String, String>();
expectedDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForText(null, partiallyEmptyDimensions, textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension cannot be empty");
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithDimensionsWithSpaceInKey() {
String spaceInKeyDimensions = " app_name :WebService01, environment :production";
ClientResponse response = createResponseForJson(null, spaceInKeyDimensions, jsonMessage);
Log log = new Log(null, dimensionsMap, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithDimensionsWithSpaceInKey() {
String spaceInKeyDimensions = " app_name :WebService01, environment :production";
ClientResponse response = createResponseForText(null, spaceInKeyDimensions, textMessage);
Log log = new Log(null, dimensionsMap, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithDimensionsWithSpaceInValue() {
String spaceInValueDimensions = "app_name: WebService01 ,environment: production ";
ClientResponse response = createResponseForJson(null, spaceInValueDimensions, jsonMessage);
Log log = new Log(null, dimensionsMap, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithDimensionsWithSpaceInValue() {
String spaceInValueDimensions = "app_name: WebService01 ,environment: production ";
ClientResponse response = createResponseForText(null, spaceInValueDimensions, textMessage);
Log log = new Log(null, dimensionsMap, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithEmptyKeyDimensions() {
String emptyKeyDimensionsStr = ":WebService01,environment:production";
ClientResponse response = createResponseForJson(null, emptyKeyDimensionsStr, jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension name cannot be empty");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithEmptyKeyDimensions() {
String emptyKeyDimensionsStr = ":WebService01,environment:production";
ClientResponse response = createResponseForText(null, emptyKeyDimensionsStr, textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension name cannot be empty");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithEmptyValueDimensions() {
String emptyKeyDimensionsStr = "app_name:,environment:production";
ClientResponse response = createResponseForJson(null, emptyKeyDimensionsStr, jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension app_name cannot have an empty value");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithEmptyValueDimensions() {
String emptyKeyDimensionsStr = "app_name:,environment:production";
ClientResponse response = createResponseForText(null, emptyKeyDimensionsStr, textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Dimension app_name cannot have an empty value");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithDimensionsWithoutColon() {
String emptyKeyDimensionsStr = "app_name,environment:production";
ClientResponse response = createResponseForJson(null, emptyKeyDimensionsStr, jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "app_name is not a valid dimension");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithDimensionsWithoutColon() {
String emptyKeyDimensionsStr = "app_name,environment:production";
ClientResponse response = createResponseForText(null, emptyKeyDimensionsStr, textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "app_name is not a valid dimension");
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithMultipleColonDimensions() {
String multipleColonDimensionsStr = "app_name:WebService01:abc,environment:production";
Map<String, String> multipleColonDimensionsMap = new HashMap<String, String>();
multipleColonDimensionsMap.put("app_name", "WebService01:abc");
multipleColonDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForJson(null, multipleColonDimensionsStr, jsonMessage);
Log log = new Log(null, multipleColonDimensionsMap, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithMultipleColonDimensions() {
String multipleColonDimensionsStr = "app_name:WebService01:abc,environment:production";
Map<String, String> multipleColonDimensionsMap = new HashMap<String, String>();
multipleColonDimensionsMap.put("app_name", "WebService01:abc");
multipleColonDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForText(null, multipleColonDimensionsStr, textMessage);
Log log = new Log(null, multipleColonDimensionsMap, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithDimensionsWithoutIllgalCharsInKey() {
String legalCharsInKeyDimensionsStr = "azAZ19.-_:WebService01,environment:production";
Map<String, String> expectedDimensionsMap = new HashMap<String, String>();
expectedDimensionsMap.put("azAZ19.-_", "WebService01");
expectedDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForJson(null, legalCharsInKeyDimensionsStr, jsonMessage);
Log log = new Log(null, expectedDimensionsMap, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithDimensionsWithoutIllegalCharsInKey() {
String legalCharsInKeyDimensionsStr = "azAZ19.-_:WebService01,environment:production";
Map<String, String> expectedDimensionsMap = new HashMap<String, String>();
expectedDimensionsMap.put("azAZ19.-_", "WebService01");
expectedDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForText(null, legalCharsInKeyDimensionsStr, textMessage);
Log log = new Log(null, expectedDimensionsMap, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithIllegalCharsInKey() {
String illegalCharsInKeyDimensionsStr = "{app_name:WebService01,environment:production";
ClientResponse response = createResponseForJson(null, illegalCharsInKeyDimensionsStr, jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
"Dimension name {app_name may not contain: > < = { } ( ) ' \" , ; &");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithIllegalCharsInKey() {
String illegalCharsInKeyDimensionsStr = "{app_name:WebService01,environment:production";
ClientResponse response = createResponseForText(null, illegalCharsInKeyDimensionsStr, textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
"Dimension name {app_name may not contain: > < = { } ( ) ' \" , ; &");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithIllegalCharsInKey_2() {
String illegalCharsInKeyDimensionsStr = "app=name:WebService01,environment:production";
ClientResponse response = createResponseForJson(null, illegalCharsInKeyDimensionsStr, jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
"Dimension name app=name may not contain: > < = { } ( ) ' \" , ; &");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithIllegalCharsInKey_2() {
String illegalCharsInKeyDimensionsStr = "app=name:WebService01,environment:production";
ClientResponse response = createResponseForText(null, illegalCharsInKeyDimensionsStr, textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
"Dimension name app=name may not contain: > < = { } ( ) ' \" , ; &");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithLegalCharsInValue() {
String legalCharsInValueDimensionsStr = "app_name:@WebService01,environment:production";
Map<String, String> legalCharsInValueDimensionsMap = new HashMap<String, String>();
legalCharsInValueDimensionsMap.put("app_name", "@WebService01");
legalCharsInValueDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForJson(null, legalCharsInValueDimensionsStr, jsonMessage);
Log log = new Log(null, legalCharsInValueDimensionsMap, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithLegalCharsInValue() {
String legalCharsInValueDimensionsStr = "app_name:@WebService01,environment:production";
Map<String, String> legalCharsInValueDimensionsMap = new HashMap<String, String>();
legalCharsInValueDimensionsMap.put("app_name", "@WebService01");
legalCharsInValueDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForText(null, legalCharsInValueDimensionsStr, textMessage);
Log log = new Log(null, legalCharsInValueDimensionsMap, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithLegalCharsInValue_2() {
String legalCharsInValueDimensionsStr = "app_name:Web Service01,environment:production";
Map<String, String> legalCharsInValueDimensionsMap = new HashMap<String, String>();
legalCharsInValueDimensionsMap.put("app_name", "Web Service01");
legalCharsInValueDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForJson(null, legalCharsInValueDimensionsStr, jsonMessage);
Log log = new Log(null, legalCharsInValueDimensionsMap, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithLegalCharsInValue_2() {
String legalCharsInValueDimensionsStr = "app_name:Web Service01,environment:production";
Map<String, String> legalCharsInValueDimensionsMap = new HashMap<String, String>();
legalCharsInValueDimensionsMap.put("app_name", "Web Service01");
legalCharsInValueDimensionsMap.put("environment", "production");
ClientResponse response = createResponseForText(null, legalCharsInValueDimensionsStr, textMessage);
Log log = new Log(null, legalCharsInValueDimensionsMap, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithTooLongKey() {
String tooLongKey = longString;
ClientResponse response = createResponseForJson(null, tooLongKey + ":abc", jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
String.format("Dimension name %s must be 255 characters or less", tooLongKey));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithTooLongKey() {
String tooLongKey = longString;
ClientResponse response = createResponseForText(null, tooLongKey + ":abc", textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
String.format("Dimension name %s must be 255 characters or less", tooLongKey));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateJsonMessageWithTooLongValue() {
String tooLongValue = longString;
ClientResponse response = createResponseForJson(null, "abc:" + tooLongValue, jsonMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
String.format("Dimension value %s must be 255 characters or less", tooLongValue));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTextMessageWithTooLongValue() {
String tooLongValue = longString;
ClientResponse response = createResponseForText(null, "abc:" + tooLongValue, textMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422,
String.format("Dimension value %s must be 255 characters or less", tooLongValue));
}
@SuppressWarnings("unchecked")
public void shouldCreateJsonMessageWithTypeAndDimensions() {
ClientResponse response = createResponseForJson(applicationType, dimensionsStr, jsonMessage);
Log log = new Log(applicationType, dimensionsMap, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldCreateTextMessageWithTypeAndDimensions() {
ClientResponse response = createResponseForText(applicationType, dimensionsStr, textMessage);
Log log = new Log(applicationType, dimensionsMap, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq(tenantId));
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTooLongJsonMessage() {
String tmpString = longString + longString + longString + longString;
StringBuffer buf = new StringBuffer(1024 * 1024 + 1);
buf.append("{\"a\":\"");
for (int i = 0; i < 1024; i++) {
buf.append(tmpString);
}
buf.append("\"}");
String tooLongMessage = buf.toString();
ClientResponse response = createResponseForJson(null, null, tooLongMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Log must be 1048576 characters or less");
}
@SuppressWarnings("unchecked")
public void shouldErrorOnCreateTooLongTextMessage() {
String tmpString = longString + longString + longString + longString;
StringBuffer buf = new StringBuffer(1024 * 1024 + 1);
for (int i = 0; i < 1024; i++) {
buf.append(tmpString);
}
buf.append('0');
String tooLongMessage = buf.toString();
ClientResponse response = createResponseForText(null, null, tooLongMessage);
ErrorMessages.assertThat(response.getEntity(String.class)).matches("unprocessable_entity", 422, "Log must be 1048576 characters or less");
}
public void shouldErrorOnCreateJsonMessageWithCrossTenant() {
ClientResponse response = createResponseForJsonWithCrossTenant(null, null, jsonMessage, "illegal-role", "def");
ErrorMessages.assertThat(response.getEntity(String.class)).matches("forbidden", 403, "Project abc cannot POST cross tenant");
}
public void shouldErrorOnCreateTextMessageWithCrossTenant() {
ClientResponse response = createResponseForTextWithCrossTenant(null, null, textMessage, "illegal-role", "def");
ErrorMessages.assertThat(response.getEntity(String.class)).matches("forbidden", 403, "Project abc cannot POST cross tenant");
}
public void shouldCreateJsonMessageWithCrossTenant() {
ClientResponse response = createResponseForJsonWithCrossTenant(null, null, jsonMessage, "monitoring-delegate", "def");
Log log = new Log(null, null, jsonMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq("def"));
}
public void shouldCreateTextMessageWithCrossTenant() {
ClientResponse response = createResponseForTextWithCrossTenant(null, null, textMessage, "monitoring-delegate", "def");
Log log = new Log(null, null, textMessage);
assertEquals(response.getStatus(), 204);
verify(service).create(eq(log), eq("def"));
}
private ClientResponse createResponseForJson(String applicationType, String dimensions, String message) {
WebResource.Builder builder =
client().resource("/v2.0/log/single").header("X-Tenant-Id", tenantId).header("Content-Type", MediaType.APPLICATION_JSON);
if (applicationType != null)
builder = builder.header("X-Application-Type", applicationType);
if (dimensions != null)
builder = builder.header("X-Dimensions", dimensions);
return builder.post(ClientResponse.class, message);
}
private ClientResponse createResponseForText(String applicationType, String dimensions, String message) {
WebResource.Builder builder = client().resource("/v2.0/log/single").header("X-Tenant-Id", tenantId).header("Content-Type", MediaType.TEXT_PLAIN);
if (applicationType != null)
builder = builder.header("X-Application-Type", applicationType);
if (dimensions != null)
builder = builder.header("X-Dimensions", dimensions);
return builder.post(ClientResponse.class, message);
}
private ClientResponse createResponseForJsonWithCrossTenant(String applicationType, String dimensions, String message, String roles,
String crossTenantId) {
WebResource.Builder builder =
client().resource("/v2.0/log/single?tenant_id=" + crossTenantId).header("X-Tenant-Id", tenantId).header("X-Roles", roles)
.header("Content-Type", MediaType.APPLICATION_JSON);
if (applicationType != null)
builder = builder.header("X-Application-Type", applicationType);
if (dimensions != null)
builder = builder.header("X-Dimensions", dimensions);
return builder.post(ClientResponse.class, message);
}
private ClientResponse createResponseForTextWithCrossTenant(String applicationType, String dimensions, String message, String roles,
String crossTenantId) {
WebResource.Builder builder =
client().resource("/v2.0/log/single?tenant_id=" + crossTenantId).header("X-Tenant-Id", tenantId).header("X-Roles", roles)
.header("Content-Type", MediaType.TEXT_PLAIN);
if (applicationType != null)
builder = builder.header("X-Application-Type", applicationType);
if (dimensions != null)
builder = builder.header("X-Dimensions", dimensions);
return builder.post(ClientResponse.class, message);
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package monasca.log.api.resource.exception;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import javax.annotation.Nullable;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import monasca.log.api.resource.exception.ErrorMessage;
/**
* Error message utilities.
*/
public final class ErrorMessages {
private static final ObjectMapper MAPPER = new ObjectMapper();
public interface ErrorMessageMatcher {
void matches(String faultType, int code, String messagePrefix);
void matches(String faultType, int code, String messagePrefix, @Nullable String detailsPrefix);
}
public static ErrorMessageMatcher assertThat(final String errorMessage) {
try {
JsonNode node = MAPPER.readTree(errorMessage);
final String rootKey = node.fieldNames().next();
node = node.get(rootKey);
final ErrorMessage message = MAPPER.reader(ErrorMessage.class).readValue(node);
return new ErrorMessageMatcher() {
@Override
public void matches(String faultType, int code, String messagePrefix) {
matches(faultType, code, messagePrefix, null);
}
@Override
public void matches(String faultType, int code, String messagePrefix,
@Nullable String detailsPrefix) {
assertEquals(rootKey, faultType);
assertEquals(message.code, code);
assertTrue(message.message.startsWith(messagePrefix), message.message);
if (detailsPrefix != null)
assertTrue(message.details.startsWith(detailsPrefix), message.details);
}
};
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

53
pom.xml Normal file
View File

@ -0,0 +1,53 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>monasca</groupId>
<artifactId>monasca-log-api-base</artifactId>
<version>1.1.0-SNAPSHOT</version>
<url>http://github.com/stackforge/monasca-log-api</url>
<packaging>pom</packaging>
<properties>
<!-- Versioning -->
<exec.args>${version} ${sun.java.command}</exec.args>
<skipITs>true</skipITs>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<scm>
<connection>scm:git:git@github.com:stackforge/monasca-log-api</connection>
<developerConnection>scm:git:git@github.com:stackforge/monasca-log-api</developerConnection>
</scm>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.1.1</version>
<executions>
<execution>
<id>package-execution</id>
<phase>package</phase>
<goals>
<goal>exec</goal>
</goals>
</execution>
<execution>
<id>clean-execution</id>
<phase>clean</phase>
<goals>
<goal>exec</goal>
</goals>
</execution>
</executions>
<configuration>
<executable>run_maven.sh</executable>
</configuration>
</plugin>
</plugins>
</build>
</project>

49
run_maven.sh Executable file
View File

@ -0,0 +1,49 @@
#!/bin/bash
# Download maven 3 if the system maven isn't maven 3
VERSION=`mvn -v | grep "Apache Maven 3"`
if [ -z "${VERSION}" ]; then
curl http://archive.apache.org/dist/maven/binaries/apache-maven-3.2.1-bin.tar.gz > apache-maven-3.2.1-bin.tar.gz
tar -xvzf apache-maven-3.2.1-bin.tar.gz
MVN=${PWD}/apache-maven-3.2.1/bin/mvn
else
MVN=mvn
fi
# Get the expected common version
COMMON_VERSION=$1
# Get rid of the version argument
shift
# Get rid of the java property name containing the args
shift
RUN_BUILD=false
for ARG in $*; do
if [ "$ARG" = "package" ]; then
RUN_BUILD=true
fi
if [ "$ARG" = "install" ]; then
RUN_BUILD=true
fi
done
if [ $RUN_BUILD = "true" ]; then
( cd common; ./build_common.sh ${MVN} ${COMMON_VERSION} )
RC=$?
if [ $RC != 0 ]; then
exit $RC
fi
fi
# Invoke the maven 3 on the real pom.xml
( cd java; ${MVN} -DgitRevision=`git rev-list HEAD --max-count 1 --abbrev=0 --abbrev-commit` $* )
RC=$?
# Copy the jars where the publisher will find them
if [ $RUN_BUILD = "true" ]; then
ln -sf java/target target
fi
rm -fr apache-maven-3.2.1*
exit $RC