Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementations for Ktor, Spring, and Play #55

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions analytics/jvm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Override for main repo's gitignore
!main*

### Java template
# Compiled class file
*.class

# Log file
*.log

# BlueJ files
*.ctxt

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*

### JetBrains template
# This section has been modified specifically for this project.
.idea/

# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr

# CMake
cmake-build-*/

# Mongo Explorer plugin
.idea/**/mongoSettings.xml

# File-based project format
*.iws

# IntelliJ
out/

# mpeltonen/sbt-idea plugin
.idea_modules/

# JIRA plugin
atlassian-ide-plugin.xml

# Cursive Clojure plugin
.idea/replstate.xml

# SonarLint plugin
.idea/sonarlint/

# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

# Editor-based Rest Client
.idea/httpRequests

# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser

### Gradle template
.gradle
**/build/
!src/**/build/

# Ignore Gradle GUI config
gradle-app.setting

# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

# Avoid ignore Gradle wrappper properties
!gradle-wrapper.properties

# Cache of project
.gradletasknamecache

# Eclipse Gradle plugin generated files
# Eclipse Core
.project
# JDT-specific (Eclipse Java Development Tools)
.classpath

### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db

# Dump file
*.stackdump

# Folder config file
[Dd]esktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp

# Windows shortcuts
*.lnk

### Kotlin template
# Compiled class file

# Log file

# BlueJ files

# Mobile Tools for Java (J2ME)

# Package Files #

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml

120 changes: 120 additions & 0 deletions analytics/jvm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# API Analytics

A free and lightweight API analytics solution, complete with a dashboard.

## Getting Started

### 1. Generate an API key

Head to [apianalytics.dev/generate](https://apianalytics.dev/generate) to generate your unique API key with a single click. This key is used to monitor your specific API and should be stored privately. It's also required in order to access your API analytics dashboard and data.

### 2. Add middleware to your API

Add our lightweight middleware to your API. Almost all processing is handled by our servers so there is minimal impact on the performance of your API.

#### Ktor

First, import it using Gradle;

```kotlin
repositories {
mavenCentral() // This package and all it's dependencies are in Maven Central
}

dependencies {
implementation("dev.tomdraper.apianalytics:ktor:1.0.0")
}
```

Lastly, install it. The middleware usually creates it's own `HttpClient`, but overwriting it is highly recommended in case you have specific settings you use that the default doesnt have.\
Though it should be noted that the example doesnt do anything, as the default one has the exact same settings.

```kotlin
// Imports simplified due to space constraints
import io.ktor.client.*
import io.ktor.server.*
import io.ktor.http.*
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation as ServerContentNegotiation

private val myHttpClient: HttpClient = HttpClient(engineFactory = OkHttp) {
install(ClientContentNegotiation) { jackson { } }
}

embeddedServer(factory = Netty) {
install(ServerContentNegotiation) { jackson { } }
install(AnalyticsPlugin) {
apiKey = "[YOUR API KEY HERE]"
httpClient = myHttpClient
}
}
```

#### Spring

First is import it;

From Maven:

```xml
<dependency>
<groupId>dev.tomdraper.apianalytics</groupId>
<artifactId>spring</artifactId>
<version>1.0.0</version>
</dependency>
```

Or from Gradle:

```kotlin
// While this is the kotlin specific format, it will still work (at least in modern Gradle)
implementation("dev.tomdraper.apianalytics:spring:1.0.0")
```

Next configure the plugin, either in;

`application.properties`:
```properties
apianalytics.apiKey = [YOUR API KEY HERE]
# Spring-only configs:
apianalytics.sendUserId = false # If a numerical user-id is found in the header name below, it is sent as well
apianalytics.userHeader = "" # The request header checked for numerical IDs
```

or `application.yml`:
```yml
apianalytics:
apiKey = "[YOUR API KEY HERE]"
sendUserId = false
userHeader = ""
```

Then you're done, as the `@Component` annotation on the middleware will register it automatically.

#### PlayFramework

First import it in SBT:

```sbt
libraryDependencies += "dev.tomdraper.apianalytics" % "play" % "1.0.0"
```

Next, set up the `application.conf` file:

```conf
apianalytics {
apiKey = [YOUR API KEY HERE]
}
```

Then add it to your project:

```scala
import dev.tomdraper.apianalytics.play.PlayAnalyticsFilter
import play.api.http.HttpFilters
import javax.inject.Inject

class MyFilters @Inject()(playAnalyticsFilter: PlayAnalyticsFilter) extends HttpFilters {
def filters = Seq(playAnalyticsFilter)
}
```
7 changes: 7 additions & 0 deletions analytics/jvm/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
alias(libs.plugins.kotlin) apply false
alias(libs.plugins.ktor) apply false
alias(libs.plugins.spring.kotlin) apply false
alias(libs.plugins.spring.boot) apply false
alias(libs.plugins.play) apply false
}
15 changes: 15 additions & 0 deletions analytics/jvm/common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
plugins {
alias(libs.plugins.kotlin)
}

group = "org.example"
version = "unspecified"

repositories {
mavenCentral()
}

dependencies {
implementation(libs.jackson.databind)
implementation(libs.jackson.kotlin)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package dev.tomdraper.apianalytics

import com.fasterxml.jackson.databind.ObjectMapper

abstract class AbstractAnalyticsHandler<R, C>(
open val apiKey: String?,
open val client: C,
open val loggingTimeout: Long,
open val serverUrl: String,
private val frameworkName: String,
open val privacyLevel: Int,
) {
//
private val requests: MutableList<PayloadHandler.RequestData> = emptyList<PayloadHandler.RequestData>().toMutableList()
//
private var lastPosted: Long = System.currentTimeMillis()
/** Universal [ObjectMapper] for implementations to use instead of creating their own. */
protected val objectMapper = ObjectMapper()

//
abstract fun send(payload: PayloadHandler.AnalyticsPayload, endpoint: String): R

//
fun logRequest(requestData: PayloadHandler.RequestData): R? {
this.requests += requestData

return if ((System.currentTimeMillis() - this.lastPosted) > this.loggingTimeout) forceSend()
else null
}

/** Force-sends a request to the API, ignoring any usage limits.
* Usually only called during shutdown or panic, but also used in [logRequest].
* This function should be used *very* carefully. */
fun forceSend(): R? {
if (this.apiKey == null) return null

val endpoint = getServerEndpoint()
val payload = PayloadHandler.AnalyticsPayload(
apiKey!!,
requests,
frameworkName,
privacyLevel
)
println(objectMapper.writeValueAsString(payload))

return send(payload, endpoint)
}

//
private fun getServerEndpoint(): String =
if (serverUrl.endsWith("/")) "${serverUrl}api/log-request"
else "$serverUrl/api/log-request"
}
Loading