minio connection log
This commit is contained in:
@@ -187,6 +187,14 @@ func (s *tenderService) lookupSummaryForTender(ctx context.Context, t *Tender) (
|
|||||||
if !errors.Is(err, ai_summarizer.ErrObjectNotFound) &&
|
if !errors.Is(err, ai_summarizer.ErrObjectNotFound) &&
|
||||||
!errors.Is(err, ai_summarizer.ErrSummaryNotReady) &&
|
!errors.Is(err, ai_summarizer.ErrSummaryNotReady) &&
|
||||||
!errors.Is(err, ai_summarizer.ErrNoOverallSummary) {
|
!errors.Is(err, ai_summarizer.ErrNoOverallSummary) {
|
||||||
|
if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
|
||||||
|
s.logger.Error("MinIO unavailable while fetching AI summary", map[string]interface{}{
|
||||||
|
"tender_id": t.ID,
|
||||||
|
"contract_folder_id": t.ContractFolderID,
|
||||||
|
"notice_id": noticeID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
return "", noticeID, err
|
return "", noticeID, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -889,6 +897,15 @@ func (s *tenderService) GetAISummary(ctx context.Context, id string) (*AISummary
|
|||||||
if errors.Is(err, ai_summarizer.ErrObjectNotFound) || errors.Is(err, ai_summarizer.ErrNoOverallSummary) {
|
if errors.Is(err, ai_summarizer.ErrObjectNotFound) || errors.Is(err, ai_summarizer.ErrNoOverallSummary) {
|
||||||
return emptyResp("unavailable"), nil
|
return emptyResp("unavailable"), nil
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
|
||||||
|
s.logger.Error("Cannot retrieve AI summary: MinIO connection unavailable", map[string]interface{}{
|
||||||
|
"tender_id": id,
|
||||||
|
"contract_folder_id": t.ContractFolderID,
|
||||||
|
"notice_id": noticeID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
return nil, fmt.Errorf("failed to get AI summary: MinIO connection unavailable: %w", err)
|
||||||
|
}
|
||||||
return nil, fmt.Errorf("failed to get AI summary: %w", err)
|
return nil, fmt.Errorf("failed to get AI summary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,9 +1005,6 @@ func (s *tenderService) TriggerAISummarize(ctx context.Context, id string) (*AIS
|
|||||||
// It lists all tender.json files in the bucket and returns summaries for tenders
|
// It lists all tender.json files in the bucket and returns summaries for tenders
|
||||||
// where the AI pipeline has completed processing.
|
// where the AI pipeline has completed processing.
|
||||||
func (s *tenderService) GetAllAISummaries(ctx context.Context) ([]ai_summarizer.TenderSummaryItem, error) {
|
func (s *tenderService) GetAllAISummaries(ctx context.Context) ([]ai_summarizer.TenderSummaryItem, error) {
|
||||||
if s.aiSummarizerStorage == nil {
|
|
||||||
return nil, fmt.Errorf("AI summarizer storage is not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.logger.Info("Retrieving all AI summaries from storage", map[string]interface{}{})
|
s.logger.Info("Retrieving all AI summaries from storage", map[string]interface{}{})
|
||||||
|
|
||||||
@@ -1007,9 +1021,14 @@ func (s *tenderService) GetAllAISummaries(ctx context.Context) ([]ai_summarizer.
|
|||||||
|
|
||||||
results, err := storage.GetAllSummaries(ctx)
|
results, err := storage.GetAllSummaries(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to get all AI summaries", map[string]interface{}{
|
fields := map[string]interface{}{
|
||||||
"error": err.Error(),
|
"error": err.Error(),
|
||||||
})
|
}
|
||||||
|
if errors.Is(err, ai_summarizer.ErrMinIOUnavailable) {
|
||||||
|
s.logger.Error("Failed to list AI summaries: MinIO connection unavailable", fields)
|
||||||
|
return nil, fmt.Errorf("failed to get all AI summaries: MinIO connection unavailable: %w", err)
|
||||||
|
}
|
||||||
|
s.logger.Error("Failed to get all AI summaries from MinIO", fields)
|
||||||
return nil, fmt.Errorf("failed to get all AI summaries: %w", err)
|
return nil, fmt.Errorf("failed to get all AI summaries: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ var (
|
|||||||
// ErrBucketNotFound is returned when the MinIO bucket does not exist.
|
// ErrBucketNotFound is returned when the MinIO bucket does not exist.
|
||||||
ErrBucketNotFound = errors.New("ai_summarizer: bucket not found")
|
ErrBucketNotFound = errors.New("ai_summarizer: bucket not found")
|
||||||
|
|
||||||
|
// ErrMinIOUnavailable is returned when MinIO cannot be reached (network,
|
||||||
|
// timeout, connection refused, etc.) as opposed to a missing object key.
|
||||||
|
ErrMinIOUnavailable = errors.New("ai_summarizer: MinIO connection unavailable")
|
||||||
|
|
||||||
// ErrAPINonSuccess is returned when the AI API returns a non-2xx status code.
|
// ErrAPINonSuccess is returned when the AI API returns a non-2xx status code.
|
||||||
ErrAPINonSuccess = errors.New("ai_summarizer: API returned non-success status")
|
ErrAPINonSuccess = errors.New("ai_summarizer: API returned non-success status")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package ai_summarizer
|
package ai_summarizer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -29,3 +32,55 @@ func isMinioNotFound(err error) bool {
|
|||||||
strings.Contains(msg, "nosuchkey") ||
|
strings.Contains(msg, "nosuchkey") ||
|
||||||
strings.Contains(msg, "not found")
|
strings.Contains(msg, "not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isMinioConnectionError reports whether err indicates MinIO is unreachable.
|
||||||
|
func isMinioConnectionError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, ErrMinIOUnavailable) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) && netErr.Timeout() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var opErr *net.OpError
|
||||||
|
if errors.As(err, &opErr) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
msg := strings.ToLower(err.Error())
|
||||||
|
connectionHints := []string{
|
||||||
|
"connection refused",
|
||||||
|
"connection reset",
|
||||||
|
"no such host",
|
||||||
|
"dial tcp",
|
||||||
|
"dial udp",
|
||||||
|
"i/o timeout",
|
||||||
|
"network is unreachable",
|
||||||
|
"broken pipe",
|
||||||
|
"tls handshake",
|
||||||
|
"server gave http response to https client",
|
||||||
|
"eof",
|
||||||
|
}
|
||||||
|
for _, hint := range connectionHints {
|
||||||
|
if strings.Contains(msg, hint) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapMinIOError attaches ErrMinIOUnavailable when the root cause is connectivity.
|
||||||
|
func wrapMinIOError(err error) error {
|
||||||
|
if err == nil || isMinioNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isMinioConnectionError(err) {
|
||||||
|
return fmt.Errorf("%w: %v", ErrMinIOUnavailable, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -184,10 +184,13 @@ func NewStorageClient(config *Config, log logger.Logger) (*StorageClient, error)
|
|||||||
func (s *StorageClient) verifyConnection(ctx context.Context) error {
|
func (s *StorageClient) verifyConnection(ctx context.Context) error {
|
||||||
exists, err := s.client.BucketExists(ctx, s.config.MinioBucket)
|
exists, err := s.client.BucketExists(ctx, s.config.MinioBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("bucket exists check for %q: %w", s.config.MinioBucket, err)
|
return s.logMinIOFailuref("bucket_exists", err, nil, "bucket exists check for %q", s.config.MinioBucket)
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
return fmt.Errorf("%w: %q", ErrBucketNotFound, s.config.MinioBucket)
|
err := fmt.Errorf("%w: %q", ErrBucketNotFound, s.config.MinioBucket)
|
||||||
|
return s.logMinIOFailure("bucket_exists", err, map[string]interface{}{
|
||||||
|
"bucket": s.config.MinioBucket,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
buckets, err := s.client.ListBuckets(ctx)
|
buckets, err := s.client.ListBuckets(ctx)
|
||||||
@@ -228,7 +231,11 @@ func (s *StorageClient) resolveNoticeStoragePrefix(ctx context.Context, contract
|
|||||||
return prefix, nil
|
return prefix, nil
|
||||||
}
|
}
|
||||||
if !isMinioNotFound(err) {
|
if !isMinioNotFound(err) {
|
||||||
return "", fmt.Errorf("ai_summarizer: stat tender.json under %s: %w", prefix, err)
|
return "", s.logMinIOFailure("stat_tender_json", err, map[string]interface{}{
|
||||||
|
"prefix": prefix,
|
||||||
|
"contract_folder_id": contractFolderID,
|
||||||
|
"notice_id": noticePublicationID,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +248,11 @@ func (s *StorageClient) resolveNoticeStoragePrefix(ctx context.Context, contract
|
|||||||
})
|
})
|
||||||
for object := range objectCh {
|
for object := range objectCh {
|
||||||
if object.Err != nil {
|
if object.Err != nil {
|
||||||
return "", fmt.Errorf("ai_summarizer: search notice folder: %w", object.Err)
|
return "", s.logMinIOFailure("list_notice_folder", object.Err, map[string]interface{}{
|
||||||
|
"notice_id": noticePublicationID,
|
||||||
|
"contract_folder_id": contractFolderID,
|
||||||
|
"notice_key": noticeKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if idx := strings.Index(object.Key, marker); idx >= 0 {
|
if idx := strings.Index(object.Key, marker); idx >= 0 {
|
||||||
resolved = object.Key[:idx+len(marker)]
|
resolved = object.Key[:idx+len(marker)]
|
||||||
@@ -290,6 +301,15 @@ func (s *StorageClient) GetSummaryFromStorage(ctx context.Context, contractFolde
|
|||||||
|
|
||||||
prefix, err := s.resolveNoticeStoragePrefix(ctx, contractFolderID, noticePublicationID)
|
prefix, err := s.resolveNoticeStoragePrefix(ctx, contractFolderID, noticePublicationID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if !errors.Is(err, ErrObjectNotFound) {
|
||||||
|
s.logger.Warn("Failed to resolve MinIO path for tender summary", map[string]interface{}{
|
||||||
|
"endpoint": s.config.MinioEndpoint,
|
||||||
|
"bucket": s.config.MinioBucket,
|
||||||
|
"contract_folder_id": contractFolderID,
|
||||||
|
"notice_id": noticePublicationID,
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return s.getSummaryFromObjectKey(ctx, prefix+"tender.json", contractFolderID, noticePublicationID)
|
return s.getSummaryFromObjectKey(ctx, prefix+"tender.json", contractFolderID, noticePublicationID)
|
||||||
@@ -346,9 +366,13 @@ func (s *StorageClient) readTenderJSONAtKey(ctx context.Context, objectKey strin
|
|||||||
}
|
}
|
||||||
var minioErr minio.ErrorResponse
|
var minioErr minio.ErrorResponse
|
||||||
if errors.As(err, &minioErr) && minioErr.StatusCode == http.StatusForbidden {
|
if errors.As(err, &minioErr) && minioErr.StatusCode == http.StatusForbidden {
|
||||||
return nil, fmt.Errorf("ai_summarizer: access denied to bucket %s: %w", s.config.MinioBucket, err)
|
return nil, s.logMinIOFailure("get_object", err, map[string]interface{}{
|
||||||
|
"object_key": objectKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("ai_summarizer: failed to get object: %w", err)
|
return nil, s.logMinIOFailure("get_object", err, map[string]interface{}{
|
||||||
|
"object_key": objectKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
defer object.Close()
|
defer object.Close()
|
||||||
|
|
||||||
@@ -361,7 +385,9 @@ func (s *StorageClient) readTenderJSONAtKey(ctx context.Context, objectKey strin
|
|||||||
})
|
})
|
||||||
return nil, ErrObjectNotFound
|
return nil, ErrObjectNotFound
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("ai_summarizer: failed to read tender.json: %w", err)
|
return nil, s.logMinIOFailure("read_tender_json", err, map[string]interface{}{
|
||||||
|
"object_key": objectKey,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var tenderJSON TenderJSON
|
var tenderJSON TenderJSON
|
||||||
@@ -547,10 +573,16 @@ func (s *StorageClient) DownloadDocument(ctx context.Context, objectName string)
|
|||||||
// under the PROC_ prefix; if none match, it falls back to a full-bucket recursive
|
// under the PROC_ prefix; if none match, it falls back to a full-bucket recursive
|
||||||
// scan (slower on large buckets) so alternate key layouts still work.
|
// scan (slower on large buckets) so alternate key layouts still work.
|
||||||
func (s *StorageClient) GetAllSummaries(ctx context.Context) ([]TenderSummaryItem, error) {
|
func (s *StorageClient) GetAllSummaries(ctx context.Context) ([]TenderSummaryItem, error) {
|
||||||
|
s.logger.Info("Listing all tender summaries from MinIO", map[string]interface{}{
|
||||||
|
"endpoint": s.config.MinioEndpoint,
|
||||||
|
"bucket": s.config.MinioBucket,
|
||||||
|
})
|
||||||
|
|
||||||
results := make([]TenderSummaryItem, 0)
|
results := make([]TenderSummaryItem, 0)
|
||||||
seen := make(map[string]struct{})
|
seen := make(map[string]struct{})
|
||||||
|
var fatalErr error
|
||||||
|
|
||||||
appendFromPrefix := func(prefix string) {
|
appendFromPrefix := func(prefix string) bool {
|
||||||
s.logger.Debug("Listing tender.json files from MinIO", map[string]interface{}{
|
s.logger.Debug("Listing tender.json files from MinIO", map[string]interface{}{
|
||||||
"bucket": s.config.MinioBucket,
|
"bucket": s.config.MinioBucket,
|
||||||
"prefix": prefix,
|
"prefix": prefix,
|
||||||
@@ -563,11 +595,10 @@ func (s *StorageClient) GetAllSummaries(ctx context.Context) ([]TenderSummaryIte
|
|||||||
|
|
||||||
for object := range objectCh {
|
for object := range objectCh {
|
||||||
if object.Err != nil {
|
if object.Err != nil {
|
||||||
s.logger.Error("Error listing MinIO objects", map[string]interface{}{
|
fatalErr = s.logMinIOFailure("list_tender_json", object.Err, map[string]interface{}{
|
||||||
"prefix": prefix,
|
"prefix": prefix,
|
||||||
"error": object.Err.Error(),
|
|
||||||
})
|
})
|
||||||
continue
|
return false
|
||||||
}
|
}
|
||||||
key := object.Key
|
key := object.Key
|
||||||
if !strings.HasSuffix(key, "/tender.json") {
|
if !strings.HasSuffix(key, "/tender.json") {
|
||||||
@@ -606,6 +637,9 @@ func (s *StorageClient) GetAllSummaries(ctx context.Context) ([]TenderSummaryIte
|
|||||||
OverallSummary: "",
|
OverallSummary: "",
|
||||||
Done: true,
|
Done: true,
|
||||||
})
|
})
|
||||||
|
case errors.Is(err, ErrMinIOUnavailable):
|
||||||
|
fatalErr = err
|
||||||
|
return false
|
||||||
default:
|
default:
|
||||||
s.logger.Debug("Skipping tender summary", map[string]interface{}{
|
s.logger.Debug("Skipping tender summary", map[string]interface{}{
|
||||||
"object_key": key,
|
"object_key": key,
|
||||||
@@ -624,17 +658,28 @@ func (s *StorageClient) GetAllSummaries(ctx context.Context) ([]TenderSummaryIte
|
|||||||
Done: true,
|
Done: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
appendFromPrefix(procedurePrefix)
|
if !appendFromPrefix(procedurePrefix) {
|
||||||
|
return nil, fatalErr
|
||||||
|
}
|
||||||
if len(seen) == 0 {
|
if len(seen) == 0 {
|
||||||
s.logger.Info("No tender.json keys under PROC_; listing entire bucket for */tender.json (may be slower on large buckets)", map[string]interface{}{
|
s.logger.Info("No tender.json keys under PROC_; listing entire bucket for */tender.json (may be slower on large buckets)", map[string]interface{}{
|
||||||
"bucket": s.config.MinioBucket,
|
"bucket": s.config.MinioBucket,
|
||||||
})
|
})
|
||||||
appendFromPrefix("")
|
if !appendFromPrefix("") {
|
||||||
|
return nil, fatalErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fatalErr != nil {
|
||||||
|
return nil, fatalErr
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Info("Retrieved all summaries from storage", map[string]interface{}{
|
s.logger.Info("Retrieved all summaries from storage", map[string]interface{}{
|
||||||
|
"endpoint": s.config.MinioEndpoint,
|
||||||
|
"bucket": s.config.MinioBucket,
|
||||||
"total_count": len(results),
|
"total_count": len(results),
|
||||||
"completed_count": countDone(results),
|
"completed_count": countDone(results),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package ai_summarizer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// logMinIOFailure records a structured log for a MinIO error and returns a wrapped
|
||||||
|
// error suitable for callers (connection issues include ErrMinIOUnavailable).
|
||||||
|
func (s *StorageClient) logMinIOFailure(operation string, err error, fields map[string]interface{}) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if fields == nil {
|
||||||
|
fields = make(map[string]interface{}, 4)
|
||||||
|
}
|
||||||
|
fields["endpoint"] = s.config.MinioEndpoint
|
||||||
|
fields["bucket"] = s.config.MinioBucket
|
||||||
|
fields["operation"] = operation
|
||||||
|
fields["error"] = err.Error()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case isMinioConnectionError(err):
|
||||||
|
fields["failure_type"] = "connection"
|
||||||
|
s.logger.Error("MinIO connection unavailable", fields)
|
||||||
|
return wrapMinIOError(err)
|
||||||
|
case errors.Is(err, ErrBucketNotFound):
|
||||||
|
fields["failure_type"] = "bucket_not_found"
|
||||||
|
s.logger.Error("MinIO bucket not found", fields)
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
fields["failure_type"] = "storage"
|
||||||
|
s.logger.Error("MinIO operation failed", fields)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// logMinIOFailuref is like logMinIOFailure with a formatted message prefix on the error.
|
||||||
|
func (s *StorageClient) logMinIOFailuref(operation string, err error, fields map[string]interface{}, format string, args ...interface{}) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
wrapped := fmt.Errorf(format+": %w", append(args, err)...)
|
||||||
|
return s.logMinIOFailure(operation, wrapped, fields)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user