Files
Byron.wang ca3d00229e Feat/Implement structured logging and Trace ID propagation (#552)
* use slog instead of log package and format to new log schema

* update the environment name to LOG_OUTPUT_FORMAT

* add the env to .env.example

* fix log reference error

* change the order of milldlewares

* delete unused code

* fix the concurrently session potential race condition

* fix the log format in tests

* update the duplicate code

* refactor: convert log functions to slog structured format

- Change log.Error/Info/Warn/Debug/Panic to accept msg + key-value pairs
- Remove printf-style formatting from log functions
- Update log calls in internal/cluster, internal/db, internal/core/session_manager
- Remove unused 'initialized' variable from log package
- Remaining files will be updated in follow-up commits

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: update all log call sites to use slog structured format

Convert all log.Error, log.Info, log.Warn, log.Debug, and log.Panic
calls from printf-style formatting to slog key-value pairs.

Before: log.Error("failed to do something: %s", err.Error())
After:  log.Error("failed to do something", "error", err)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: update cmd/ log calls to use slog structured format

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: implement GnetLogger for structured logging in gnet

* refactor: remove deprecated log visibility functions and related calls

* feat: enhance session management with trace and identity context propagation

* feat: implement serverless transaction handler and writer for plugin runtime

* refactor: rename context field to traceCtx in RealBackwardsInvocation

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Yeuoly <admin@srmxy.cn>
2025-12-30 11:00:48 +08:00

442 lines
13 KiB
Go

package service
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"sync/atomic"
"time"
"github.com/gin-gonic/gin"
"github.com/langgenius/dify-plugin-daemon/internal/core/dify_invocation"
"github.com/langgenius/dify-plugin-daemon/internal/core/io_tunnel"
"github.com/langgenius/dify-plugin-daemon/internal/core/io_tunnel/access_types"
"github.com/langgenius/dify-plugin-daemon/internal/core/plugin_manager"
"github.com/langgenius/dify-plugin-daemon/internal/core/session_manager"
"github.com/langgenius/dify-plugin-daemon/internal/db"
"github.com/langgenius/dify-plugin-daemon/internal/service/install_service"
"github.com/langgenius/dify-plugin-daemon/internal/types/exception"
"github.com/langgenius/dify-plugin-daemon/internal/types/models"
"github.com/langgenius/dify-plugin-daemon/pkg/entities"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/endpoint_entities"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
"github.com/langgenius/dify-plugin-daemon/pkg/entities/requests"
routinepkg "github.com/langgenius/dify-plugin-daemon/pkg/routine"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/cache"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/cache/helper"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/encryption"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/log"
"github.com/langgenius/dify-plugin-daemon/pkg/utils/routine"
)
func copyRequest(req *http.Request, hookId string, path string) (*bytes.Buffer, error) {
newReq := req.Clone(context.Background())
// get query params
queryParams := req.URL.Query()
// replace path with endpoint path
newReq.URL.Path = path
// set query params
newReq.URL.RawQuery = queryParams.Encode()
// read request body until complete, max 10MB
body, err := io.ReadAll(io.LimitReader(req.Body, 10*1024*1024))
if err != nil {
return nil, err
}
// replace with a new reader
newReq.Body = io.NopCloser(bytes.NewReader(body))
newReq.ContentLength = int64(len(body))
newReq.TransferEncoding = nil
// remove ip traces for security
newReq.Header.Del("X-Forwarded-For")
newReq.Header.Del("X-Real-IP")
newReq.Header.Del("X-Forwarded")
newReq.Header.Del("X-Original-Forwarded-For")
newReq.Header.Del("X-Original-Url")
newReq.Header.Del("X-Original-Host")
// check if X-Original-Host is set
if originalHost := req.Header.Get(endpoint_entities.HeaderXOriginalHost); originalHost != "" {
// replace host with original host
newReq.Host = originalHost
}
// setup hook id to request
newReq.Header.Set("Dify-Hook-Id", hookId)
// check if Dify-Hook-Url is set
if url := req.Header.Get("Dify-Hook-Url"); url == "" {
newReq.Header.Set(
"Dify-Hook-Url",
fmt.Sprintf("http://%s/e/%s%s", newReq.Host, hookId, path),
)
}
var buffer bytes.Buffer
err = newReq.Write(&buffer)
if err != nil {
return nil, err
}
return &buffer, nil
}
func Endpoint(
ctx *gin.Context,
endpoint *models.Endpoint,
pluginInstallation *models.PluginInstallation,
maxExecutionTime time.Duration,
path string,
) {
if !endpoint.Enabled {
ctx.JSON(404, exception.NotFoundError(errors.New("endpoint not found")).ToResponse())
return
}
buffer, err := copyRequest(ctx.Request, endpoint.HookID, path)
if err != nil {
ctx.JSON(500, exception.InternalServerError(err).ToResponse())
return
}
identifier, err := plugin_entities.NewPluginUniqueIdentifier(pluginInstallation.PluginUniqueIdentifier)
if err != nil {
ctx.JSON(400, exception.UniqueIdentifierError(err).ToResponse())
return
}
// fetch plugin
manager := plugin_manager.Manager()
runtime, err := manager.GetPluginRuntime(identifier)
if err != nil {
ctx.JSON(404, exception.ErrPluginNotFound().ToResponse())
return
}
// fetch endpoint declaration
endpointDeclaration := runtime.Configuration().Endpoint
if endpointDeclaration == nil {
ctx.JSON(404, exception.ErrPluginNotFound().ToResponse())
return
}
// decrypt settings
settings, err := manager.BackwardsInvocation().InvokeEncrypt(&dify_invocation.InvokeEncryptRequest{
BaseInvokeDifyRequest: dify_invocation.BaseInvokeDifyRequest{
TenantId: endpoint.TenantID,
UserId: "",
Type: dify_invocation.INVOKE_TYPE_ENCRYPT,
},
InvokeEncryptSchema: dify_invocation.InvokeEncryptSchema{
Opt: dify_invocation.ENCRYPT_OPT_DECRYPT,
Namespace: dify_invocation.ENCRYPT_NAMESPACE_ENDPOINT,
Identity: endpoint.ID,
Data: endpoint.Settings,
Config: endpointDeclaration.Settings,
},
})
if err != nil {
ctx.JSON(500, exception.InternalServerError(err).ToResponse())
return
}
session := session_manager.NewSession(
session_manager.NewSessionPayload{
TenantID: endpoint.TenantID,
UserID: "",
PluginUniqueIdentifier: identifier,
ClusterID: ctx.GetString("cluster_id"),
InvokeFrom: access_types.PLUGIN_ACCESS_TYPE_ENDPOINT,
Action: access_types.PLUGIN_ACCESS_ACTION_INVOKE_ENDPOINT,
Declaration: runtime.Configuration(),
BackwardsInvocation: manager.BackwardsInvocation(),
IgnoreCache: false,
EndpointID: &endpoint.ID,
RequestContext: ctx.Request.Context(),
},
)
defer session.Close(session_manager.CloseSessionPayload{
IgnoreCache: false,
})
session.BindRuntime(runtime)
statusCode, headers, response, err := io_tunnel.InvokeEndpoint(
session, &requests.RequestInvokeEndpoint{
RawHttpRequest: hex.EncodeToString(buffer.Bytes()),
Settings: settings,
},
)
if err != nil {
ctx.JSON(500, exception.InternalServerError(err).ToResponse())
return
}
defer response.Close()
done := make(chan bool)
closed := new(int32)
ctx.Status(statusCode)
for k, v := range *headers {
if len(v) > 0 {
ctx.Writer.Header().Set(k, v[0])
}
}
close := func() {
if atomic.CompareAndSwapInt32(closed, 0, 1) {
close(done)
}
}
defer close()
routine.Submit(routinepkg.Labels{
routinepkg.RoutineLabelKeyModule: "service",
routinepkg.RoutineLabelKeyMethod: "Endpoint",
}, func() {
defer close()
for response.Next() {
chunk, err := response.Read()
if err != nil {
ctx.Writer.Write([]byte(err.Error()))
ctx.Writer.Flush()
return
}
ctx.Writer.Write(chunk)
ctx.Writer.Flush()
}
})
select {
case <-ctx.Writer.CloseNotify():
case <-done:
case <-time.After(maxExecutionTime):
ctx.JSON(500, exception.InternalServerError(errors.New("killed by timeout")).ToResponse())
}
}
func EnableEndpoint(endpointID string, tenantID string) *entities.Response {
endpoint, err := install_service.EnabledEndpoint(endpointID, tenantID)
if err != nil {
return handleEndpointStateError(err, "enable")
}
invalidateEndpointCache(endpoint.HookID)
return entities.NewSuccessResponse(true)
}
func DisableEndpoint(endpointID string, tenantID string) *entities.Response {
endpoint, err := install_service.DisabledEndpoint(endpointID, tenantID)
if err != nil {
return handleEndpointStateError(err, "disable")
}
invalidateEndpointCache(endpoint.HookID)
return entities.NewSuccessResponse(true)
}
func handleEndpointStateError(err error, action string) *entities.Response {
if err == nil {
return nil
}
if errors.Is(err, db.ErrDatabaseNotFound) {
return exception.NotFoundError(errors.New("endpoint not found")).ToResponse()
}
return exception.InternalServerError(fmt.Errorf("failed to %s endpoint: %w", action, err)).ToResponse()
}
func invalidateEndpointCache(hookID string) {
endpointCacheKey := helper.EndpointCacheKey(hookID)
if _, err := cache.AutoDelete[models.Endpoint](endpointCacheKey); err != nil {
log.Warn("failed to invalidate endpoint cache", "hook_id", hookID, "error", err)
}
}
func ListEndpoints(tenant_id string, page int, page_size int) *entities.Response {
endpoints, err := db.GetAll[models.Endpoint](
db.Equal("tenant_id", tenant_id),
db.OrderBy("created_at", true),
db.Page(page, page_size),
)
if err != nil {
return exception.InternalServerError(fmt.Errorf("failed to list endpoints: %v", err)).ToResponse()
}
manager := plugin_manager.Manager()
if manager == nil {
return exception.InternalServerError(errors.New("failed to get plugin manager")).ToResponse()
}
// decrypt settings
for i, endpoint := range endpoints {
pluginInstallation, err := db.GetOne[models.PluginInstallation](
db.Equal("plugin_id", endpoint.PluginID),
db.Equal("tenant_id", tenant_id),
)
if err != nil {
// use empty settings and declaration for uninstalled plugins
endpoint.Settings = map[string]any{}
endpoint.Declaration = &plugin_entities.EndpointProviderDeclaration{
Settings: []plugin_entities.ProviderConfig{},
Endpoints: []plugin_entities.EndpointDeclaration{},
EndpointFiles: []string{},
}
endpoints[i] = endpoint
continue
}
pluginUniqueIdentifier, err := plugin_entities.NewPluginUniqueIdentifier(
pluginInstallation.PluginUniqueIdentifier,
)
if err != nil {
return exception.UniqueIdentifierError(
fmt.Errorf("failed to parse plugin unique identifier: %v", err),
).ToResponse()
}
pluginDeclaration, err := manager.GetDeclaration(
pluginUniqueIdentifier,
tenant_id,
plugin_entities.PluginRuntimeType(pluginInstallation.RuntimeType),
)
if err != nil {
return exception.InternalServerError(
fmt.Errorf("failed to get plugin declaration: %v", err),
).ToResponse()
}
if pluginDeclaration.Endpoint == nil {
return exception.NotFoundError(errors.New("plugin does not have an endpoint")).ToResponse()
}
decryptedSettings, err := manager.BackwardsInvocation().InvokeEncrypt(&dify_invocation.InvokeEncryptRequest{
BaseInvokeDifyRequest: dify_invocation.BaseInvokeDifyRequest{
TenantId: tenant_id,
UserId: "",
Type: dify_invocation.INVOKE_TYPE_ENCRYPT,
},
InvokeEncryptSchema: dify_invocation.InvokeEncryptSchema{
Opt: dify_invocation.ENCRYPT_OPT_DECRYPT,
Namespace: dify_invocation.ENCRYPT_NAMESPACE_ENDPOINT,
Identity: endpoint.ID,
Data: endpoint.Settings,
Config: pluginDeclaration.Endpoint.Settings,
},
})
if err != nil {
return exception.InternalServerError(
fmt.Errorf("failed to decrypt settings: %v", err),
).ToResponse()
}
// mask settings
decryptedSettings = encryption.MaskConfigCredentials(decryptedSettings, pluginDeclaration.Endpoint.Settings)
endpoint.Settings = decryptedSettings
endpoint.Declaration = pluginDeclaration.Endpoint
endpoints[i] = endpoint
}
return entities.NewSuccessResponse(endpoints)
}
func ListPluginEndpoints(tenant_id string, plugin_id string, page int, page_size int) *entities.Response {
endpoints, err := db.GetAll[models.Endpoint](
db.Equal("plugin_id", plugin_id),
db.Equal("tenant_id", tenant_id),
db.OrderBy("created_at", true),
db.Page(page, page_size),
)
if err != nil {
return exception.InternalServerError(
fmt.Errorf("failed to list endpoints: %v", err),
).ToResponse()
}
manager := plugin_manager.Manager()
if manager == nil {
return exception.InternalServerError(
errors.New("failed to get plugin manager"),
).ToResponse()
}
// decrypt settings
for i, endpoint := range endpoints {
// get installation
pluginInstallation, err := db.GetOne[models.PluginInstallation](
db.Equal("plugin_id", plugin_id),
db.Equal("tenant_id", tenant_id),
)
if err != nil {
return exception.NotFoundError(
fmt.Errorf("failed to find plugin installation: %v", err),
).ToResponse()
}
pluginUniqueIdentifier, err := plugin_entities.NewPluginUniqueIdentifier(
pluginInstallation.PluginUniqueIdentifier,
)
if err != nil {
return exception.UniqueIdentifierError(
fmt.Errorf("failed to parse plugin unique identifier: %v", err),
).ToResponse()
}
pluginDeclaration, err := manager.GetDeclaration(
pluginUniqueIdentifier,
tenant_id,
plugin_entities.PluginRuntimeType(pluginInstallation.RuntimeType),
)
if err != nil {
return exception.InternalServerError(
fmt.Errorf("failed to get plugin declaration: %v", err),
).ToResponse()
}
if pluginDeclaration.Endpoint == nil {
return exception.NotFoundError(errors.New("plugin does not have an endpoint")).ToResponse()
}
decryptedSettings, err := manager.BackwardsInvocation().InvokeEncrypt(&dify_invocation.InvokeEncryptRequest{
BaseInvokeDifyRequest: dify_invocation.BaseInvokeDifyRequest{
TenantId: tenant_id,
UserId: "",
Type: dify_invocation.INVOKE_TYPE_ENCRYPT,
},
InvokeEncryptSchema: dify_invocation.InvokeEncryptSchema{
Opt: dify_invocation.ENCRYPT_OPT_DECRYPT,
Namespace: dify_invocation.ENCRYPT_NAMESPACE_ENDPOINT,
Identity: endpoint.ID,
Data: endpoint.Settings,
Config: pluginDeclaration.Endpoint.Settings,
},
})
if err != nil {
return exception.InternalServerError(
fmt.Errorf("failed to decrypt settings: %v", err),
).ToResponse()
}
// mask settings
decryptedSettings = encryption.MaskConfigCredentials(decryptedSettings, pluginDeclaration.Endpoint.Settings)
endpoint.Settings = decryptedSettings
endpoint.Declaration = pluginDeclaration.Endpoint
endpoints[i] = endpoint
}
return entities.NewSuccessResponse(endpoints)
}