Skip to content

Commit 0b33d57

Browse files
committed
Add configuration, scheduler, and summary generation logic
- Introduced `Config` class to manage environment-based application settings. - Added `GeminiClient` for interaction with the Google Generative Language (Gemini) API. - Implemented `SummaryJob` for querying InfluxDB, generating summaries, and saving results. - Refactored and modularized logic from `Main` for better separation of concerns. - Added `TimeUtil` for scheduling utilities. - Created a multi-stage `Dockerfile` for building and running the application.
1 parent a567b99 commit 0b33d57

File tree

8 files changed

+429
-255
lines changed

8 files changed

+429
-255
lines changed

Dockerfile

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Multi-stage build for DashboardSummary Java 17 application
2+
# Build stage
3+
FROM eclipse-temurin:21-jdk AS build
4+
WORKDIR /app
5+
6+
# Copy Gradle wrapper and build files first to leverage Docker layer caching
7+
COPY gradlew ./
8+
COPY gradle ./gradle
9+
COPY settings.gradle build.gradle ./
10+
11+
# Ensure the Gradle wrapper is executable
12+
RUN chmod +x gradlew
13+
14+
# Copy source code
15+
COPY src ./src
16+
17+
# Build a self-contained application distribution (bin + libs)
18+
RUN ./gradlew --no-daemon clean installDist
19+
20+
# Runtime stage
21+
FROM eclipse-temurin:21-jre
22+
WORKDIR /app
23+
24+
# Copy the application distribution produced by the build stage
25+
COPY --from=build /app/build/install/DashboardSummary /app
26+
27+
# Optional: place for additional JVM options
28+
ENV JAVA_OPTS=""
29+
30+
# Default command runs the application. Environment variables are read by the app at runtime.
31+
ENTRYPOINT ["./bin/DashboardSummary"]

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ repositories {
2323
dependencies {
2424
implementation 'com.influxdb:influxdb-client-java:7.2.0'
2525
implementation 'com.google.code.gson:gson:2.11.0'
26+
implementation 'org.slf4j:slf4j-simple:2.0.13'
2627

2728
testImplementation platform('org.junit:junit-bom:5.10.0')
2829
testImplementation 'org.junit.jupiter:junit-jupiter'
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package com.pathvariable.smartgarden.summary;
2+
3+
import java.time.ZoneId;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
7+
/**
8+
* Application configuration loaded from environment variables.
9+
*/
10+
public record Config(String influxUrl, String influxToken, String influxOrg, String influxBucket,
11+
String measurementRegex, String fieldRegex, String googleApiKey, String model,
12+
String outputMeasurement, int intervalMinutes, String timezone, boolean runOnce,
13+
String systemInstruction) {
14+
15+
public static Config fromEnv() {
16+
return Config.builder()
17+
.influxUrl(getenv("INFLUX_URL", "http://localhost:8086"))
18+
.influxToken(getenv("INFLUX_TOKEN", null))
19+
.influxOrg(getenv("INFLUX_ORG", null))
20+
.influxBucket(getenv("INFLUX_BUCKET", null))
21+
.measurementRegex(getenv("INFLUX_MEASUREMENT_REGEX", ".*"))
22+
.fieldRegex(getenv("INFLUX_FIELD_REGEX", null))
23+
.googleApiKey(getenv("GOOGLE_API_KEY", null))
24+
.model(getenv("GOOGLE_MODEL", "gemini-2.5-flash"))
25+
.outputMeasurement(getenv("OUTPUT_MEASUREMENT", "dashboard_summary"))
26+
.intervalMinutes(Integer.parseInt(getenv("INTERVAL_MINUTES", "15")))
27+
.timezone(getenv("TIMEZONE", ZoneId.systemDefault().getId()))
28+
.runOnce(Boolean.parseBoolean(getenv("RUN_ONCE", "false")))
29+
.systemInstruction(getenv("SYSTEM_INSTRUCTION", "You are a concise observability assistant."))
30+
.build();
31+
}
32+
33+
public boolean isValid() {
34+
return notBlank(influxUrl) && notBlank(influxToken) && notBlank(influxOrg) && notBlank(influxBucket)
35+
&& notBlank(googleApiKey) && notBlank(model) && intervalMinutes > 0;
36+
}
37+
38+
public String missingDescription() {
39+
List<String> m = new ArrayList<>();
40+
if (!notBlank(influxUrl)) m.add("INFLUX_URL");
41+
if (!notBlank(influxToken)) m.add("INFLUX_TOKEN");
42+
if (!notBlank(influxOrg)) m.add("INFLUX_ORG");
43+
if (!notBlank(influxBucket)) m.add("INFLUX_BUCKET");
44+
if (!notBlank(googleApiKey)) m.add("GOOGLE_API_KEY");
45+
if (!notBlank(model)) m.add("GOOGLE_MODEL");
46+
if (intervalMinutes <= 0) m.add("INTERVAL_MINUTES");
47+
return "Missing/invalid: " + String.join(", ", m);
48+
}
49+
50+
private static boolean notBlank(String s) {
51+
return s != null && !s.isBlank();
52+
}
53+
54+
private static String getenv(String k, String def) {
55+
String v = System.getenv(k);
56+
return v == null ? def : v;
57+
}
58+
59+
public static Builder builder() {
60+
return new Builder();
61+
}
62+
63+
public static class Builder {
64+
private String influxUrl = "http://localhost:8086";
65+
private String influxToken;
66+
private String influxOrg;
67+
private String influxBucket;
68+
private String measurementRegex = ".*";
69+
private String fieldRegex;
70+
private String googleApiKey;
71+
private String model = "gemini-2.5-flash";
72+
private String outputMeasurement = "dashboard_summary";
73+
private int intervalMinutes = 15;
74+
private String timezone = ZoneId.systemDefault().getId();
75+
private boolean runOnce = false;
76+
private String systemInstruction = "You are a concise observability assistant.";
77+
78+
public Builder influxUrl(String influxUrl) {
79+
this.influxUrl = influxUrl;
80+
return this;
81+
}
82+
83+
public Builder influxToken(String influxToken) {
84+
this.influxToken = influxToken;
85+
return this;
86+
}
87+
88+
public Builder influxOrg(String influxOrg) {
89+
this.influxOrg = influxOrg;
90+
return this;
91+
}
92+
93+
public Builder influxBucket(String influxBucket) {
94+
this.influxBucket = influxBucket;
95+
return this;
96+
}
97+
98+
public Builder measurementRegex(String measurementRegex) {
99+
this.measurementRegex = measurementRegex;
100+
return this;
101+
}
102+
103+
public Builder fieldRegex(String fieldRegex) {
104+
this.fieldRegex = fieldRegex;
105+
return this;
106+
}
107+
108+
public Builder googleApiKey(String googleApiKey) {
109+
this.googleApiKey = googleApiKey;
110+
return this;
111+
}
112+
113+
public Builder model(String model) {
114+
this.model = model;
115+
return this;
116+
}
117+
118+
public Builder outputMeasurement(String outputMeasurement) {
119+
this.outputMeasurement = outputMeasurement;
120+
return this;
121+
}
122+
123+
public Builder intervalMinutes(int intervalMinutes) {
124+
this.intervalMinutes = intervalMinutes;
125+
return this;
126+
}
127+
128+
public Builder timezone(String timezone) {
129+
this.timezone = timezone;
130+
return this;
131+
}
132+
133+
public Builder runOnce(boolean runOnce) {
134+
this.runOnce = runOnce;
135+
return this;
136+
}
137+
138+
public Builder systemInstruction(String systemInstruction) {
139+
this.systemInstruction = systemInstruction;
140+
return this;
141+
}
142+
143+
public Config build() {
144+
return new Config(influxUrl, influxToken, influxOrg, influxBucket, measurementRegex,
145+
fieldRegex, googleApiKey, model, outputMeasurement, intervalMinutes,
146+
timezone, runOnce, systemInstruction);
147+
}
148+
}
149+
150+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.pathvariable.smartgarden.summary;
2+
3+
import com.google.gson.Gson;
4+
import com.google.gson.JsonArray;
5+
import com.google.gson.JsonObject;
6+
7+
import java.net.URI;
8+
import java.net.http.HttpClient;
9+
import java.net.http.HttpRequest;
10+
import java.net.http.HttpResponse;
11+
import java.nio.charset.StandardCharsets;
12+
import java.time.Duration;
13+
14+
/**
15+
* Client for Google Generative Language (Gemini) API.
16+
*/
17+
public class GeminiClient {
18+
19+
private static final Gson gson = new Gson();
20+
21+
private final Config config;
22+
23+
public GeminiClient(Config config) {
24+
this.config = config;
25+
}
26+
27+
28+
public String generateSummary(String prompt) throws Exception {
29+
String url = "https://generativelanguage.googleapis.com/v1beta/models/" + config.model() + ":generateContent?key=" + config.googleApiKey();
30+
31+
JsonObject userPart = new JsonObject();
32+
userPart.addProperty("text", prompt);
33+
JsonObject content = new JsonObject();
34+
content.add("parts", arrayOf(userPart));
35+
36+
JsonObject req = new JsonObject();
37+
req.add("contents", arrayOf(content));
38+
if (config.systemInstruction() != null && !config.systemInstruction().isBlank()) {
39+
JsonObject sys = new JsonObject();
40+
sys.add("parts", arrayOf(textPart(config.systemInstruction())));
41+
req.add("system_instruction", sys);
42+
}
43+
44+
HttpClient http = HttpClient.newBuilder()
45+
.connectTimeout(Duration.ofSeconds(15))
46+
.build();
47+
HttpRequest httpReq = HttpRequest.newBuilder(URI.create(url))
48+
.timeout(Duration.ofSeconds(60))
49+
.header("Content-Type", "application/json; charset=utf-8")
50+
.POST(HttpRequest.BodyPublishers.ofString(gson.toJson(req), StandardCharsets.UTF_8))
51+
.build();
52+
HttpResponse<String> resp = http.send(httpReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
53+
if (resp.statusCode() < 200 || resp.statusCode() >= 300) {
54+
throw new RuntimeException("Gemini API error: HTTP " + resp.statusCode() + " - " + resp.body());
55+
}
56+
JsonObject json = gson.fromJson(resp.body(), JsonObject.class);
57+
try {
58+
JsonArray candidates = json.getAsJsonArray("candidates");
59+
if (candidates == null || candidates.isEmpty()) return "";
60+
JsonObject first = candidates.get(0).getAsJsonObject();
61+
JsonObject c = first.getAsJsonObject("content");
62+
JsonArray parts = c.getAsJsonArray("parts");
63+
if (parts == null || parts.isEmpty()) return "";
64+
JsonObject p0 = parts.get(0).getAsJsonObject();
65+
return p0.has("text") ? p0.get("text").getAsString() : "";
66+
} catch (Exception e) {
67+
throw new RuntimeException("Failed to parse Gemini response: " + e.getMessage() + " Body: " + resp.body());
68+
}
69+
}
70+
71+
private static JsonArray arrayOf(JsonObject obj) {
72+
JsonArray arr = new JsonArray();
73+
arr.add(obj);
74+
return arr;
75+
}
76+
77+
private static JsonObject textPart(String text) {
78+
JsonObject p = new JsonObject();
79+
p.addProperty("text", text);
80+
return p;
81+
}
82+
}

0 commit comments

Comments
 (0)