-
Notifications
You must be signed in to change notification settings - Fork 131
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[OPIK-130] Add anonymous usage information
- Loading branch information
1 parent
4fab35c
commit 8a3afe6
Showing
14 changed files
with
486 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
apps/opik-backend/src/main/java/com/comet/opik/infrastructure/OpikMetadataConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.comet.opik.infrastructure; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import jakarta.validation.Valid; | ||
import jakarta.validation.constraints.NotNull; | ||
import lombok.Data; | ||
|
||
@Data | ||
public class OpikMetadataConfig { | ||
|
||
public record UsageReport(@Valid @JsonProperty boolean enabled, @Valid @JsonProperty String url) {} | ||
|
||
@Valid | ||
@JsonProperty | ||
@NotNull | ||
private String version; | ||
|
||
@Valid | ||
@NotNull | ||
@JsonProperty | ||
private UsageReport usageReport; | ||
} |
142 changes: 142 additions & 0 deletions
142
...ik-backend/src/main/java/com/comet/opik/infrastructure/bi/ApplicationStartupListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
package com.comet.opik.infrastructure.bi; | ||
|
||
import com.comet.opik.domain.IdGenerator; | ||
import com.comet.opik.infrastructure.OpikConfiguration; | ||
import com.comet.opik.infrastructure.lock.LockService; | ||
import com.google.inject.Injector; | ||
import jakarta.inject.Provider; | ||
import jakarta.inject.Singleton; | ||
import jakarta.ws.rs.client.Client; | ||
import jakarta.ws.rs.client.ClientBuilder; | ||
import jakarta.ws.rs.client.Entity; | ||
import jakarta.ws.rs.core.Response; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.apache.commons.lang3.StringUtils; | ||
import reactor.core.publisher.Mono; | ||
import reactor.core.scheduler.Schedulers; | ||
import ru.vyarus.dropwizard.guice.module.context.SharedConfigurationState; | ||
import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycle; | ||
import ru.vyarus.dropwizard.guice.module.lifecycle.GuiceyLifecycleListener; | ||
import ru.vyarus.dropwizard.guice.module.lifecycle.event.GuiceyLifecycleEvent; | ||
import ru.vyarus.dropwizard.guice.module.lifecycle.event.InjectorPhaseEvent; | ||
import ru.vyarus.dropwizard.guice.module.lifecycle.event.JerseyPhaseEvent; | ||
import ru.vyarus.dropwizard.guice.module.lifecycle.event.run.InjectorCreationEvent; | ||
|
||
import java.net.URI; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
|
||
import static com.comet.opik.infrastructure.lock.LockService.Lock; | ||
|
||
@Slf4j | ||
@Singleton | ||
@RequiredArgsConstructor | ||
public class ApplicationStartupListener implements GuiceyLifecycleListener { | ||
|
||
// This event cannot depend on authentication | ||
private final Client client = ClientBuilder.newClient(); | ||
private final AtomicReference<Injector> injector = new AtomicReference<>(); | ||
|
||
@Override | ||
public void onEvent(GuiceyLifecycleEvent event) { | ||
|
||
if (event instanceof InjectorPhaseEvent injectorEvent) { | ||
injector.set(injectorEvent.getInjector()); | ||
} | ||
|
||
if (event.getType() == GuiceyLifecycle.ApplicationStarted) { | ||
|
||
String eventType = GuiceyLifecycle.ApplicationStarted.name(); | ||
|
||
var config = (OpikConfiguration) event.getSharedState().getConfiguration().get(); | ||
|
||
if (!config.getMetadata().getUsageReport().enabled()) { | ||
log.info("Usage report is disabled"); | ||
return; | ||
} | ||
|
||
if (StringUtils.isEmpty(config.getMetadata().getUsageReport().url())) { | ||
log.warn("Usage report URL is not set"); | ||
return; | ||
} | ||
|
||
var lockService = injector.get().getInstance(LockService.class); | ||
var generator = injector.get().getInstance(IdGenerator.class); | ||
var usageReport = injector.get().getInstance(UsageReportDAO.class); | ||
|
||
var lock = new Lock("opik-%s".formatted(eventType)); | ||
|
||
lockService.executeWithLock(lock, tryToReportStartupEvent(usageReport, generator, eventType, config)) | ||
.subscribeOn(Schedulers.boundedElastic()) | ||
.block(); | ||
} | ||
} | ||
|
||
private Mono<Mono<Object>> tryToReportStartupEvent(UsageReportDAO usageReport, IdGenerator generator, String eventType, OpikConfiguration config) { | ||
return Mono.fromCallable(() -> { | ||
|
||
Optional<String> anonymousId = getAnonymousId(usageReport, generator); | ||
|
||
log.info("Anonymous ID: {}", anonymousId.orElse("not found")); | ||
|
||
if (anonymousId.isEmpty()) { | ||
log.warn("Anonymous ID not found, skipping event reporting"); | ||
return Mono.empty(); | ||
} | ||
|
||
if (usageReport.isEventReported(eventType)) { | ||
log.info("Event already reported"); | ||
return Mono.empty(); | ||
} | ||
|
||
usageReport.addEvent(eventType); | ||
|
||
reportEvent(anonymousId.get(), eventType, config, usageReport); | ||
|
||
return Mono.empty(); | ||
}); | ||
} | ||
|
||
private static Optional<String> getAnonymousId(UsageReportDAO usageReport, IdGenerator generator) { | ||
var anonymousId = usageReport.getAnonymousId(); | ||
|
||
if (anonymousId.isEmpty()) { | ||
log.info("Anonymous ID not found, generating a new one"); | ||
var newId = generator.generateId(); | ||
log.info("Generated new ID: {}", newId); | ||
|
||
// Save the new ID | ||
usageReport.saveAnonymousId(newId.toString()); | ||
|
||
anonymousId = Optional.of(newId.toString()); | ||
} | ||
|
||
return anonymousId; | ||
} | ||
|
||
private void reportEvent(String anonymousId, String eventType, OpikConfiguration config, UsageReportDAO usageReport) { | ||
|
||
var startupEvent = new OpikStartupEvent( | ||
anonymousId, | ||
eventType, | ||
Map.of("opik_app_version", config.getMetadata().getVersion()) | ||
); | ||
|
||
try (Response response = client.target(URI.create(config.getMetadata().getUsageReport().url())) | ||
.request() | ||
.post(Entity.json(startupEvent))) { | ||
|
||
if (response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL) { | ||
log.info("Event reported successfully"); | ||
usageReport.markEventAsReported(eventType); | ||
} else { | ||
log.warn("Failed to report event: {}", response.getStatusInfo()); | ||
if (response.hasEntity()) { | ||
log.warn("Response: {}", response.readEntity(String.class)); | ||
} | ||
} | ||
} | ||
} | ||
} |
6 changes: 6 additions & 0 deletions
6
apps/opik-backend/src/main/java/com/comet/opik/infrastructure/bi/Metadata.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.comet.opik.infrastructure.bi; | ||
|
||
enum Metadata { | ||
anonymous_id, | ||
; | ||
} |
14 changes: 14 additions & 0 deletions
14
apps/opik-backend/src/main/java/com/comet/opik/infrastructure/bi/OpikStartupEvent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.comet.opik.infrastructure.bi; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
import com.fasterxml.jackson.databind.PropertyNamingStrategies; | ||
import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
import lombok.Builder; | ||
|
||
import java.util.Map; | ||
|
||
@Builder(toBuilder = true) | ||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) | ||
record OpikStartupEvent(String anonymousId, String eventType, Map<String, String> eventProperties) { | ||
} |
76 changes: 76 additions & 0 deletions
76
apps/opik-backend/src/main/java/com/comet/opik/infrastructure/bi/UsageReportDAO.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package com.comet.opik.infrastructure.bi; | ||
|
||
import com.google.inject.ImplementedBy; | ||
import jakarta.inject.Inject; | ||
import jakarta.inject.Singleton; | ||
import lombok.NonNull; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.jdbi.v3.core.Jdbi; | ||
import org.jdbi.v3.core.statement.UnableToExecuteStatementException; | ||
|
||
import java.sql.SQLIntegrityConstraintViolationException; | ||
import java.util.Optional; | ||
|
||
@ImplementedBy(UsageReportDAOImpl.class) | ||
interface UsageReportDAO { | ||
|
||
Optional<String> getAnonymousId(); | ||
|
||
void saveAnonymousId(@NonNull String id); | ||
|
||
boolean isEventReported(@NonNull String eventType); | ||
|
||
void addEvent(@NonNull String eventType); | ||
|
||
void markEventAsReported(@NonNull String eventType); | ||
} | ||
|
||
@Slf4j | ||
@RequiredArgsConstructor(onConstructor_ = @Inject) | ||
@Singleton | ||
class UsageReportDAOImpl implements UsageReportDAO { | ||
|
||
private final Jdbi jdbi; | ||
|
||
public Optional<String> getAnonymousId() { | ||
return jdbi.inTransaction(handle -> handle.createQuery("SELECT value FROM metadata WHERE `key` = :key") | ||
.bind("key", Metadata.anonymous_id) | ||
.mapTo(String.class) | ||
.findFirst()); | ||
} | ||
|
||
public void saveAnonymousId(@NonNull String id) { | ||
jdbi.useHandle(handle -> handle.createUpdate("INSERT INTO metadata (`key`, value) VALUES (:key, :value)") | ||
.bind("key", Metadata.anonymous_id) | ||
.bind("value", id) | ||
.execute()); | ||
} | ||
|
||
public boolean isEventReported(@NonNull String eventType) { | ||
return jdbi.inTransaction(handle -> handle.createQuery("SELECT COUNT(*) > 0 FROM usage_information WHERE event_type = :eventType AND reported_at IS NOT NULL") | ||
.bind("eventType", eventType) | ||
.mapTo(Boolean.class) | ||
.one()); | ||
} | ||
|
||
public void addEvent(@NonNull String eventType) { | ||
try { | ||
jdbi.useHandle(handle -> handle.createUpdate("INSERT INTO usage_information (event_type) VALUES (:eventType)") | ||
.bind("eventType",eventType) | ||
.execute()); | ||
} catch (UnableToExecuteStatementException e) { | ||
if (e.getCause() instanceof SQLIntegrityConstraintViolationException) { | ||
log.warn("Event type already exists: {}", eventType); | ||
} else { | ||
log.error("Failed to add event", e); | ||
} | ||
} | ||
} | ||
|
||
public void markEventAsReported(@NonNull String eventType) { | ||
jdbi.useHandle(handle -> handle.createUpdate("UPDATE usage_information SET reported_at = current_timestamp(6) WHERE event_type = :eventType") | ||
.bind("eventType", eventType) | ||
.execute()); | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
...sources/liquibase/db-app-state/migrations/000002_create_usage_usage_information_table.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
--liquibase formatted sql | ||
--changeset thiagohora:create_usage_usage_information_table | ||
|
||
CREATE TABLE metadata ( | ||
`key` VARCHAR(255) NOT NULL, | ||
value VARCHAR(255) NOT NULL, | ||
last_updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), | ||
PRIMARY KEY `metadata_pk` (`key`) | ||
); | ||
|
||
CREATE TABLE usage_information ( | ||
event_type VARCHAR(255) NOT NULL, | ||
last_updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), | ||
reported_at TIMESTAMP(6) DEFAULT NULL, | ||
PRIMARY KEY `usage_information_pk` (event_type) | ||
); | ||
|
||
--rollback DROP TABLE IF EXISTS metadata; | ||
--rollback DROP TABLE IF EXISTS usage_information; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.