Skip to content
Merged
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
1 change: 1 addition & 0 deletions bom/dify-bom/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ description = "dify bom"
dependencyManagement {
dependencies {
dependency "${APP_GROUP}:dify-core:${APP_VERSION}"
dependency "${APP_GROUP}:dify-status:${APP_VERSION}"
dependency "${APP_GROUP}:dify-client-spring5:${APP_VERSION}"
dependency "${APP_GROUP}:dify-client-spring6:${APP_VERSION}"
dependency "${APP_GROUP}:dify-client-spring7:${APP_VERSION}"
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ subprojects {
if (!onlyPom) {
dependencyManagement {
imports {
if (project.name.contains("spring5") || project.name.contains("core") || project.name.contains("support") || project.name.contains("spring-boot2-starter")) {
if (project.name.contains("spring5") || project.name.contains("core") || project.name.contains("support") || project.name.contains("status") || project.name.contains("spring-boot2-starter")) {
mavenBom "org.springframework.boot:spring-boot-dependencies:${libs.versions.springBoot2.get()}"
} else if (project.name.contains("spring-boot4-starter") || project.name.contains("spring7")) {
mavenBom "org.springframework.boot:spring-boot-dependencies:${libs.versions.springBoot4.get()}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
package io.github.guoshiqiufeng.dify.core.config;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

/**
* Dify 配置
Expand Down Expand Up @@ -52,6 +54,8 @@ public class DifyProperties implements Serializable {
*/
private ClientConfig clientConfig = new ClientConfig();

private StatusConfig status = new StatusConfig();

@Data
@AllArgsConstructor
@NoArgsConstructor
Expand Down Expand Up @@ -87,4 +91,38 @@ public static class ClientConfig implements Serializable {

private Boolean logging = true;
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class StatusConfig implements Serializable {
private static final long serialVersionUID = -9124280674952624154L;

@Builder.Default
private Boolean healthIndicatorEnabled = false;

@Builder.Default
private Boolean healthIndicatorInitByServer = true;

/**
* all apikey
*/
private String apiKey;

/**
* Dataset API key (for DifyDataset client)
*/
private String datasetApiKey;

/**
* Chat API key (for DifyChat client)
*/
private List<String> chatApiKey;

/**
* Workflow API key (for DifyWorkflow client)
*/
private List<String> workflowApiKey;
}
}
33 changes: 33 additions & 0 deletions dify/dify-status/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
dependencies {
implementation project(":dify:dify-support:dify-support-chat")
implementation project(":dify:dify-support:dify-support-dataset")
implementation project(":dify:dify-support:dify-support-server")
implementation project(":dify:dify-support:dify-support-workflow")
implementation project(":dify:dify-core")

// Spring Boot Actuator
compileOnly "org.springframework.boot:spring-boot-starter-actuator"

// Spring Web for HTTP client exceptions
compileOnly "org.springframework:spring-web"

// Lombok
compileOnly "org.projectlombok:lombok"
annotationProcessor "org.projectlombok:lombok"

// Jackson for JSON
compileOnly "com.fasterxml.jackson.core:jackson-annotations"

// SLF4J for logging
compileOnly "org.slf4j:slf4j-api"

// Test dependencies
testImplementation "org.junit.jupiter:junit-jupiter:5.9.3"
testImplementation "org.mockito:mockito-core:5.3.1"
testImplementation "org.mockito:mockito-junit-jupiter:5.3.1"
testImplementation "org.mockito:mockito-inline:5.2.0"
testImplementation "org.springframework:spring-test"
testImplementation "org.springframework:spring-web"
testImplementation "org.springframework.boot:spring-boot-starter-test"
testImplementation "org.springframework.boot:spring-boot-starter-actuator"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2025-2025, fubluesky ([email protected])
*
* 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 io.github.guoshiqiufeng.dify.status.actuator;

import io.github.guoshiqiufeng.dify.core.config.DifyProperties;
import io.github.guoshiqiufeng.dify.status.dto.AggregatedStatusReport;
import io.github.guoshiqiufeng.dify.status.enums.ApiStatus;
import io.github.guoshiqiufeng.dify.status.service.DifyStatusService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;

/**
* Dify health indicator for Spring Boot Actuator
*
* @author yanghq
* @version 1.7.0
* @since 2025/12/19 14:00
*/
@Slf4j
public class DifyHealthIndicator implements HealthIndicator {

private final DifyStatusService statusService;
private final DifyProperties.StatusConfig statusConfig;

public DifyHealthIndicator(DifyStatusService statusService, DifyProperties.StatusConfig statusConfig) {
this.statusService = statusService;
this.statusConfig = statusConfig;
}

@Override
public Health health() {
try {
AggregatedStatusReport report;
if(statusConfig.getHealthIndicatorInitByServer()) {
report = statusService.checkAllClientsStatusByServer();
} else {
report = statusService.checkAllClientsStatus(statusConfig);
}

if (report.getOverallStatus() == ApiStatus.NORMAL) {
return Health.up()
.withDetail("totalApis", report.getTotalApis())
.withDetail("healthyApis", report.getHealthyApis())
.withDetail("unhealthyApis", report.getUnhealthyApis())
.withDetail("clientSummary", report.getClientSummary())
.withDetail("reportTime", report.getReportTime())
.withDetail("clientReports", report.getClientReports())
.build();
} else {
Health.Builder healthBuilder;
if(report.getHealthyApis() > 0) {
healthBuilder = Health.up();
} else {
healthBuilder = Health.down();
}
return healthBuilder
.withDetail("overallStatus", report.getOverallStatus())
.withDetail("totalApis", report.getTotalApis())
.withDetail("healthyApis", report.getHealthyApis())
.withDetail("unhealthyApis", report.getUnhealthyApis())
.withDetail("clientSummary", report.getClientSummary())
.withDetail("reportTime", report.getReportTime())
.withDetail("clientReports", report.getClientReports())
.build();
}
} catch (Exception e) {
log.error("Error checking Dify health", e);
return Health.down()
.withException(e)
.build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2025-2025, fubluesky ([email protected])
*
* 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 io.github.guoshiqiufeng.dify.status.cache;

import io.github.guoshiqiufeng.dify.status.dto.AggregatedStatusReport;
import io.github.guoshiqiufeng.dify.status.dto.ClientStatusReport;

import java.util.Optional;

/**
* Status cache service interface
*
* @author yanghq
* @version 1.7.0
* @since 2025/12/19 14:00
*/
public interface StatusCacheService {
/**
* Cache a client status report
*
* @param clientName Client name
* @param report Client status report
* @param ttlSeconds TTL in seconds
*/
void cacheClientReport(String clientName, ClientStatusReport report, long ttlSeconds);

/**
* Get cached client report
*
* @param clientName Client name
* @return Optional of ClientStatusReport
*/
Optional<ClientStatusReport> getCachedClientReport(String clientName);

/**
* Cache aggregated report
*
* @param report Aggregated status report
* @param ttlSeconds TTL in seconds
*/
void cacheAggregatedReport(AggregatedStatusReport report, long ttlSeconds);

/**
* Get cached aggregated report
*
* @return Optional of AggregatedStatusReport
*/
Optional<AggregatedStatusReport> getCachedAggregatedReport();

/**
* Clear all cached reports
*/
void clearCache();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright (c) 2025-2025, fubluesky ([email protected])
*
* 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 io.github.guoshiqiufeng.dify.status.cache.impl;

import io.github.guoshiqiufeng.dify.status.cache.StatusCacheService;
import io.github.guoshiqiufeng.dify.status.dto.AggregatedStatusReport;
import io.github.guoshiqiufeng.dify.status.dto.ClientStatusReport;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

import java.time.LocalDateTime;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
* In-memory status cache service implementation
*
* @author yanghq
* @version 1.7.0
* @since 2025/12/19 14:00
*/
@Slf4j
public class InMemoryStatusCacheService implements StatusCacheService {

private final ConcurrentHashMap<String, CacheEntry<ClientStatusReport>> clientReportCache = new ConcurrentHashMap<>();
private volatile CacheEntry<AggregatedStatusReport> aggregatedReportCache;

@Override
public void cacheClientReport(String clientName, ClientStatusReport report, long ttlSeconds) {
LocalDateTime expirationTime = LocalDateTime.now().plusSeconds(ttlSeconds);
clientReportCache.put(clientName, new CacheEntry<>(report, expirationTime));
log.debug("Cached client report for {} with TTL {} seconds", clientName, ttlSeconds);
}

@Override
public Optional<ClientStatusReport> getCachedClientReport(String clientName) {
CacheEntry<ClientStatusReport> entry = clientReportCache.get(clientName);
if (entry == null) {
log.debug("No cached report found for {}", clientName);
return Optional.empty();
}

if (entry.isExpired()) {
clientReportCache.remove(clientName);
log.debug("Cached report for {} has expired", clientName);
return Optional.empty();
}

log.debug("Retrieved cached report for {}", clientName);
return Optional.of(entry.getData());
}

@Override
public void cacheAggregatedReport(AggregatedStatusReport report, long ttlSeconds) {
LocalDateTime expirationTime = LocalDateTime.now().plusSeconds(ttlSeconds);
aggregatedReportCache = new CacheEntry<>(report, expirationTime);
log.debug("Cached aggregated report with TTL {} seconds", ttlSeconds);
}

@Override
public Optional<AggregatedStatusReport> getCachedAggregatedReport() {
CacheEntry<AggregatedStatusReport> entry = aggregatedReportCache;
if (entry == null) {
log.debug("No cached aggregated report found");
return Optional.empty();
}

if (entry.isExpired()) {
aggregatedReportCache = null;
log.debug("Cached aggregated report has expired");
return Optional.empty();
}

log.debug("Retrieved cached aggregated report");
return Optional.of(entry.getData());
}

@Override
public void clearCache() {
clientReportCache.clear();
aggregatedReportCache = null;
log.info("Cleared all cached reports");
}

/**
* Cache entry with expiration time
*
* @param <T> Type of cached data
*/
@Data
@AllArgsConstructor
private static class CacheEntry<T> {
private final T data;
private final LocalDateTime expirationTime;

/**
* Check if the cache entry has expired
*
* @return true if expired, false otherwise
*/
public boolean isExpired() {
return LocalDateTime.now().isAfter(expirationTime);
}
}
}
Loading
Loading