You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							534 lines
						
					
					
						
							22 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							534 lines
						
					
					
						
							22 KiB
						
					
					
				| package main | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"encoding/json" | |
| 	"fmt" | |
| 	"log" | |
| 	"math/big" | |
| 	"math/rand" | |
| 	"os" | |
| 	"strings" | |
| 	"time" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/cluster" | |
| 	"github.com/seaweedfs/seaweedfs/weed/mq/client/pub_client" | |
| 	"github.com/seaweedfs/seaweedfs/weed/mq/pub_balancer" | |
| 	"github.com/seaweedfs/seaweedfs/weed/mq/topic" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/master_pb" | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb" | |
| 	"google.golang.org/grpc" | |
| 	"google.golang.org/grpc/credentials/insecure" | |
| ) | |
| 
 | |
| type UserEvent struct { | |
| 	ID            int64     `json:"id"` | |
| 	UserID        int64     `json:"user_id"` | |
| 	UserType      string    `json:"user_type"` | |
| 	Action        string    `json:"action"` | |
| 	Status        string    `json:"status"` | |
| 	Amount        float64   `json:"amount,omitempty"` | |
| 	PreciseAmount string    `json:"precise_amount,omitempty"` // Will be converted to DECIMAL | |
| 	BirthDate     time.Time `json:"birth_date"`               // Will be converted to DATE | |
| 	Timestamp     time.Time `json:"timestamp"` | |
| 	Metadata      string    `json:"metadata,omitempty"` | |
| } | |
| 
 | |
| type SystemLog struct { | |
| 	ID        int64     `json:"id"` | |
| 	Level     string    `json:"level"` | |
| 	Service   string    `json:"service"` | |
| 	Message   string    `json:"message"` | |
| 	ErrorCode int       `json:"error_code,omitempty"` | |
| 	Timestamp time.Time `json:"timestamp"` | |
| } | |
| 
 | |
| type MetricEntry struct { | |
| 	ID        int64     `json:"id"` | |
| 	Name      string    `json:"name"` | |
| 	Value     float64   `json:"value"` | |
| 	Tags      string    `json:"tags"` | |
| 	Timestamp time.Time `json:"timestamp"` | |
| } | |
| 
 | |
| type ProductView struct { | |
| 	ID        int64     `json:"id"` | |
| 	ProductID int64     `json:"product_id"` | |
| 	UserID    int64     `json:"user_id"` | |
| 	Category  string    `json:"category"` | |
| 	Price     float64   `json:"price"` | |
| 	ViewCount int       `json:"view_count"` | |
| 	Timestamp time.Time `json:"timestamp"` | |
| } | |
| 
 | |
| func main() { | |
| 	// Get SeaweedFS configuration from environment | |
| 	masterAddr := getEnv("SEAWEEDFS_MASTER", "localhost:9333") | |
| 	filerAddr := getEnv("SEAWEEDFS_FILER", "localhost:8888") | |
| 
 | |
| 	log.Printf("Creating MQ test data...") | |
| 	log.Printf("Master: %s", masterAddr) | |
| 	log.Printf("Filer: %s", filerAddr) | |
| 
 | |
| 	// Wait for SeaweedFS to be ready | |
| 	log.Println("Waiting for SeaweedFS to be ready...") | |
| 	time.Sleep(10 * time.Second) | |
| 
 | |
| 	// Create topics and populate with data | |
| 	topics := []struct { | |
| 		namespace string | |
| 		topic     string | |
| 		generator func() interface{} | |
| 		count     int | |
| 	}{ | |
| 		{"analytics", "user_events", generateUserEvent, 1000}, | |
| 		{"analytics", "system_logs", generateSystemLog, 500}, | |
| 		{"analytics", "metrics", generateMetric, 800}, | |
| 		{"ecommerce", "product_views", generateProductView, 1200}, | |
| 		{"ecommerce", "user_events", generateUserEvent, 600}, | |
| 		{"logs", "application_logs", generateSystemLog, 2000}, | |
| 		{"logs", "error_logs", generateErrorLog, 300}, | |
| 	} | |
| 
 | |
| 	for _, topicConfig := range topics { | |
| 		log.Printf("Creating topic %s.%s with %d records...", | |
| 			topicConfig.namespace, topicConfig.topic, topicConfig.count) | |
| 
 | |
| 		err := createTopicData(masterAddr, filerAddr, | |
| 			topicConfig.namespace, topicConfig.topic, | |
| 			topicConfig.generator, topicConfig.count) | |
| 		if err != nil { | |
| 			log.Printf("Error creating topic %s.%s: %v", | |
| 				topicConfig.namespace, topicConfig.topic, err) | |
| 		} else { | |
| 			log.Printf("-Successfully created %s.%s", | |
| 				topicConfig.namespace, topicConfig.topic) | |
| 		} | |
| 
 | |
| 		// Small delay between topics | |
| 		time.Sleep(2 * time.Second) | |
| 	} | |
| 
 | |
| 	log.Println("-MQ test data creation completed!") | |
| 	log.Println("\nCreated namespaces:") | |
| 	log.Println("  - analytics (user_events, system_logs, metrics)") | |
| 	log.Println("  - ecommerce (product_views, user_events)") | |
| 	log.Println("  - logs (application_logs, error_logs)") | |
| 	log.Println("\nYou can now test with PostgreSQL clients:") | |
| 	log.Println("  psql -h localhost -p 5432 -U seaweedfs -d analytics") | |
| 	log.Println("  postgres=> SHOW TABLES;") | |
| 	log.Println("  postgres=> SELECT COUNT(*) FROM user_events;") | |
| } | |
| 
 | |
| // createSchemaForTopic creates a proper RecordType schema based on topic name | |
| func createSchemaForTopic(topicName string) *schema_pb.RecordType { | |
| 	switch topicName { | |
| 	case "user_events": | |
| 		return &schema_pb.RecordType{ | |
| 			Fields: []*schema_pb.Field{ | |
| 				{Name: "id", FieldIndex: 0, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}}, IsRequired: true}, | |
| 				{Name: "user_id", FieldIndex: 1, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}}, IsRequired: true}, | |
| 				{Name: "user_type", FieldIndex: 2, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "action", FieldIndex: 3, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "status", FieldIndex: 4, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "amount", FieldIndex: 5, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_DOUBLE}}, IsRequired: false}, | |
| 				{Name: "timestamp", FieldIndex: 6, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "metadata", FieldIndex: 7, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: false}, | |
| 			}, | |
| 		} | |
| 	case "system_logs": | |
| 		return &schema_pb.RecordType{ | |
| 			Fields: []*schema_pb.Field{ | |
| 				{Name: "id", FieldIndex: 0, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}}, IsRequired: true}, | |
| 				{Name: "level", FieldIndex: 1, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "service", FieldIndex: 2, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "message", FieldIndex: 3, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "error_code", FieldIndex: 4, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT32}}, IsRequired: false}, | |
| 				{Name: "timestamp", FieldIndex: 5, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 			}, | |
| 		} | |
| 	case "metrics": | |
| 		return &schema_pb.RecordType{ | |
| 			Fields: []*schema_pb.Field{ | |
| 				{Name: "id", FieldIndex: 0, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}}, IsRequired: true}, | |
| 				{Name: "name", FieldIndex: 1, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "value", FieldIndex: 2, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_DOUBLE}}, IsRequired: true}, | |
| 				{Name: "tags", FieldIndex: 3, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "timestamp", FieldIndex: 4, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 			}, | |
| 		} | |
| 	case "product_views": | |
| 		return &schema_pb.RecordType{ | |
| 			Fields: []*schema_pb.Field{ | |
| 				{Name: "id", FieldIndex: 0, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}}, IsRequired: true}, | |
| 				{Name: "product_id", FieldIndex: 1, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}}, IsRequired: true}, | |
| 				{Name: "user_id", FieldIndex: 2, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}}, IsRequired: true}, | |
| 				{Name: "category", FieldIndex: 3, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "price", FieldIndex: 4, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_DOUBLE}}, IsRequired: true}, | |
| 				{Name: "view_count", FieldIndex: 5, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT32}}, IsRequired: true}, | |
| 				{Name: "timestamp", FieldIndex: 6, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 			}, | |
| 		} | |
| 	case "application_logs", "error_logs": | |
| 		return &schema_pb.RecordType{ | |
| 			Fields: []*schema_pb.Field{ | |
| 				{Name: "id", FieldIndex: 0, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}}, IsRequired: true}, | |
| 				{Name: "level", FieldIndex: 1, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "service", FieldIndex: 2, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "message", FieldIndex: 3, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 				{Name: "error_code", FieldIndex: 4, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT32}}, IsRequired: false}, | |
| 				{Name: "timestamp", FieldIndex: 5, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}, IsRequired: true}, | |
| 			}, | |
| 		} | |
| 	default: | |
| 		// Default generic schema | |
| 		return &schema_pb.RecordType{ | |
| 			Fields: []*schema_pb.Field{ | |
| 				{Name: "data", FieldIndex: 0, Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_BYTES}}, IsRequired: true}, | |
| 			}, | |
| 		} | |
| 	} | |
| } | |
| 
 | |
| // convertToDecimal converts a string to decimal format for Parquet logical type | |
| func convertToDecimal(value string) ([]byte, int32, int32) { | |
| 	// Parse the decimal string using big.Rat for precision | |
| 	rat := new(big.Rat) | |
| 	if _, success := rat.SetString(value); !success { | |
| 		return nil, 0, 0 | |
| 	} | |
| 
 | |
| 	// Convert to a fixed scale (e.g., 4 decimal places) | |
| 	scale := int32(4) | |
| 	precision := int32(18) // Total digits | |
|  | |
| 	// Scale the rational number to integer representation | |
| 	multiplier := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(scale)), nil) | |
| 	scaled := new(big.Int).Mul(rat.Num(), multiplier) | |
| 	scaled.Div(scaled, rat.Denom()) | |
| 
 | |
| 	return scaled.Bytes(), precision, scale | |
| } | |
| 
 | |
| // convertToRecordValue converts Go structs to RecordValue format | |
| func convertToRecordValue(data interface{}) (*schema_pb.RecordValue, error) { | |
| 	fields := make(map[string]*schema_pb.Value) | |
| 
 | |
| 	switch v := data.(type) { | |
| 	case UserEvent: | |
| 		fields["id"] = &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: v.ID}} | |
| 		fields["user_id"] = &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: v.UserID}} | |
| 		fields["user_type"] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v.UserType}} | |
| 		fields["action"] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v.Action}} | |
| 		fields["status"] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v.Status}} | |
| 		fields["amount"] = &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: v.Amount}} | |
| 
 | |
| 		// Convert precise amount to DECIMAL logical type | |
| 		if v.PreciseAmount != "" { | |
| 			if decimal, precision, scale := convertToDecimal(v.PreciseAmount); decimal != nil { | |
| 				fields["precise_amount"] = &schema_pb.Value{Kind: &schema_pb.Value_DecimalValue{DecimalValue: &schema_pb.DecimalValue{ | |
| 					Value:     decimal, | |
| 					Precision: precision, | |
| 					Scale:     scale, | |
| 				}}} | |
| 			} | |
| 		} | |
| 
 | |
| 		// Convert birth date to DATE logical type | |
| 		fields["birth_date"] = &schema_pb.Value{Kind: &schema_pb.Value_DateValue{DateValue: &schema_pb.DateValue{ | |
| 			DaysSinceEpoch: int32(v.BirthDate.Unix() / 86400), // Convert to days since epoch | |
| 		}}} | |
| 
 | |
| 		fields["timestamp"] = &schema_pb.Value{Kind: &schema_pb.Value_TimestampValue{TimestampValue: &schema_pb.TimestampValue{ | |
| 			TimestampMicros: v.Timestamp.UnixMicro(), | |
| 			IsUtc:           true, | |
| 		}}} | |
| 		fields["metadata"] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v.Metadata}} | |
| 
 | |
| 	case SystemLog: | |
| 		fields["id"] = &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: v.ID}} | |
| 		fields["level"] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v.Level}} | |
| 		fields["service"] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v.Service}} | |
| 		fields["message"] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v.Message}} | |
| 		fields["error_code"] = &schema_pb.Value{Kind: &schema_pb.Value_Int32Value{Int32Value: int32(v.ErrorCode)}} | |
| 		fields["timestamp"] = &schema_pb.Value{Kind: &schema_pb.Value_TimestampValue{TimestampValue: &schema_pb.TimestampValue{ | |
| 			TimestampMicros: v.Timestamp.UnixMicro(), | |
| 			IsUtc:           true, | |
| 		}}} | |
| 
 | |
| 	case MetricEntry: | |
| 		fields["id"] = &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: v.ID}} | |
| 		fields["name"] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v.Name}} | |
| 		fields["value"] = &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: v.Value}} | |
| 		fields["tags"] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v.Tags}} | |
| 		fields["timestamp"] = &schema_pb.Value{Kind: &schema_pb.Value_TimestampValue{TimestampValue: &schema_pb.TimestampValue{ | |
| 			TimestampMicros: v.Timestamp.UnixMicro(), | |
| 			IsUtc:           true, | |
| 		}}} | |
| 
 | |
| 	case ProductView: | |
| 		fields["id"] = &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: v.ID}} | |
| 		fields["product_id"] = &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: v.ProductID}} | |
| 		fields["user_id"] = &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: v.UserID}} | |
| 		fields["category"] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v.Category}} | |
| 		fields["price"] = &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: v.Price}} | |
| 		fields["view_count"] = &schema_pb.Value{Kind: &schema_pb.Value_Int32Value{Int32Value: int32(v.ViewCount)}} | |
| 		fields["timestamp"] = &schema_pb.Value{Kind: &schema_pb.Value_TimestampValue{TimestampValue: &schema_pb.TimestampValue{ | |
| 			TimestampMicros: v.Timestamp.UnixMicro(), | |
| 			IsUtc:           true, | |
| 		}}} | |
| 
 | |
| 	default: | |
| 		// Fallback to JSON for unknown types | |
| 		jsonData, err := json.Marshal(data) | |
| 		if err != nil { | |
| 			return nil, fmt.Errorf("failed to marshal unknown type: %v", err) | |
| 		} | |
| 		fields["data"] = &schema_pb.Value{Kind: &schema_pb.Value_BytesValue{BytesValue: jsonData}} | |
| 	} | |
| 
 | |
| 	return &schema_pb.RecordValue{Fields: fields}, nil | |
| } | |
| 
 | |
| // No need for convertHTTPToGRPC - pb.ServerAddress.ToGrpcAddress() already handles this | |
|  | |
| // discoverFiler finds a filer from the master server | |
| func discoverFiler(masterHTTPAddress string) (string, error) { | |
| 	httpAddr := pb.ServerAddress(masterHTTPAddress) | |
| 	masterGRPCAddress := httpAddr.ToGrpcAddress() | |
| 
 | |
| 	conn, err := grpc.NewClient(masterGRPCAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) | |
| 	if err != nil { | |
| 		return "", fmt.Errorf("failed to connect to master at %s: %v", masterGRPCAddress, err) | |
| 	} | |
| 	defer conn.Close() | |
| 
 | |
| 	client := master_pb.NewSeaweedClient(conn) | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | |
| 	defer cancel() | |
| 
 | |
| 	resp, err := client.ListClusterNodes(ctx, &master_pb.ListClusterNodesRequest{ | |
| 		ClientType: cluster.FilerType, | |
| 	}) | |
| 	if err != nil { | |
| 		return "", fmt.Errorf("failed to list filers from master: %v", err) | |
| 	} | |
| 
 | |
| 	if len(resp.ClusterNodes) == 0 { | |
| 		return "", fmt.Errorf("no filers found in cluster") | |
| 	} | |
| 
 | |
| 	// Use the first available filer and convert HTTP address to gRPC | |
| 	filerHTTPAddress := resp.ClusterNodes[0].Address | |
| 	httpAddr := pb.ServerAddress(filerHTTPAddress) | |
| 	return httpAddr.ToGrpcAddress(), nil | |
| } | |
| 
 | |
| // discoverBroker finds the broker balancer using filer lock mechanism | |
| func discoverBroker(masterHTTPAddress string) (string, error) { | |
| 	// First discover filer from master | |
| 	filerAddress, err := discoverFiler(masterHTTPAddress) | |
| 	if err != nil { | |
| 		return "", fmt.Errorf("failed to discover filer: %v", err) | |
| 	} | |
| 
 | |
| 	conn, err := grpc.NewClient(filerAddress, grpc.WithTransportCredentials(insecure.NewCredentials())) | |
| 	if err != nil { | |
| 		return "", fmt.Errorf("failed to connect to filer at %s: %v", filerAddress, err) | |
| 	} | |
| 	defer conn.Close() | |
| 
 | |
| 	client := filer_pb.NewSeaweedFilerClient(conn) | |
| 
 | |
| 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | |
| 	defer cancel() | |
| 
 | |
| 	resp, err := client.FindLockOwner(ctx, &filer_pb.FindLockOwnerRequest{ | |
| 		Name: pub_balancer.LockBrokerBalancer, | |
| 	}) | |
| 	if err != nil { | |
| 		return "", fmt.Errorf("failed to find broker balancer: %v", err) | |
| 	} | |
| 
 | |
| 	return resp.Owner, nil | |
| } | |
| 
 | |
| func createTopicData(masterAddr, filerAddr, namespace, topicName string, | |
| 	generator func() interface{}, count int) error { | |
| 
 | |
| 	// Create schema based on topic type | |
| 	recordType := createSchemaForTopic(topicName) | |
| 
 | |
| 	// Dynamically discover broker address instead of hardcoded port replacement | |
| 	brokerAddress, err := discoverBroker(masterAddr) | |
| 	if err != nil { | |
| 		// Fallback to hardcoded port replacement if discovery fails | |
| 		log.Printf("Warning: Failed to discover broker dynamically (%v), using hardcoded port replacement", err) | |
| 		brokerAddress = strings.Replace(masterAddr, ":9333", ":17777", 1) | |
| 	} | |
| 
 | |
| 	// Create publisher configuration | |
| 	config := &pub_client.PublisherConfiguration{ | |
| 		Topic:          topic.NewTopic(namespace, topicName), | |
| 		PartitionCount: 1, | |
| 		Brokers:        []string{brokerAddress}, // Use dynamically discovered broker address | |
| 		PublisherName:  fmt.Sprintf("test-producer-%s-%s", namespace, topicName), | |
| 		RecordType:     recordType, // Use structured schema | |
| 	} | |
| 
 | |
| 	// Create publisher | |
| 	publisher, err := pub_client.NewTopicPublisher(config) | |
| 	if err != nil { | |
| 		return fmt.Errorf("failed to create publisher: %v", err) | |
| 	} | |
| 	defer publisher.Shutdown() | |
| 
 | |
| 	// Generate and publish data | |
| 	for i := 0; i < count; i++ { | |
| 		data := generator() | |
| 
 | |
| 		// Convert struct to RecordValue | |
| 		recordValue, err := convertToRecordValue(data) | |
| 		if err != nil { | |
| 			log.Printf("Error converting data to RecordValue: %v", err) | |
| 			continue | |
| 		} | |
| 
 | |
| 		// Publish structured record | |
| 		err = publisher.PublishRecord([]byte(fmt.Sprintf("key-%d", i)), recordValue) | |
| 		if err != nil { | |
| 			log.Printf("Error publishing message %d: %v", i+1, err) | |
| 			continue | |
| 		} | |
| 
 | |
| 		// Small delay every 100 messages | |
| 		if (i+1)%100 == 0 { | |
| 			log.Printf("  Published %d/%d messages to %s.%s", | |
| 				i+1, count, namespace, topicName) | |
| 			time.Sleep(100 * time.Millisecond) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Finish publishing | |
| 	err = publisher.FinishPublish() | |
| 	if err != nil { | |
| 		return fmt.Errorf("failed to finish publishing: %v", err) | |
| 	} | |
| 
 | |
| 	return nil | |
| } | |
| 
 | |
| func generateUserEvent() interface{} { | |
| 	userTypes := []string{"premium", "standard", "trial", "enterprise"} | |
| 	actions := []string{"login", "logout", "purchase", "view", "search", "click", "download"} | |
| 	statuses := []string{"active", "inactive", "pending", "completed", "failed"} | |
| 
 | |
| 	// Generate a birth date between 1970 and 2005 (18+ years old) | |
| 	birthYear := 1970 + rand.Intn(35) | |
| 	birthMonth := 1 + rand.Intn(12) | |
| 	birthDay := 1 + rand.Intn(28) // Keep it simple, avoid month-specific day issues | |
| 	birthDate := time.Date(birthYear, time.Month(birthMonth), birthDay, 0, 0, 0, 0, time.UTC) | |
| 
 | |
| 	// Generate a precise amount as a string with 4 decimal places | |
| 	preciseAmount := fmt.Sprintf("%.4f", rand.Float64()*10000) | |
| 
 | |
| 	return UserEvent{ | |
| 		ID:            rand.Int63n(1000000) + 1, | |
| 		UserID:        rand.Int63n(10000) + 1, | |
| 		UserType:      userTypes[rand.Intn(len(userTypes))], | |
| 		Action:        actions[rand.Intn(len(actions))], | |
| 		Status:        statuses[rand.Intn(len(statuses))], | |
| 		Amount:        rand.Float64() * 1000, | |
| 		PreciseAmount: preciseAmount, | |
| 		BirthDate:     birthDate, | |
| 		Timestamp:     time.Now().Add(-time.Duration(rand.Intn(86400*30)) * time.Second), | |
| 		Metadata:      fmt.Sprintf("{\"session_id\":\"%d\"}", rand.Int63n(100000)), | |
| 	} | |
| } | |
| 
 | |
| func generateSystemLog() interface{} { | |
| 	levels := []string{"debug", "info", "warning", "error", "critical"} | |
| 	services := []string{"auth-service", "payment-service", "user-service", "notification-service", "api-gateway"} | |
| 	messages := []string{ | |
| 		"Request processed successfully", | |
| 		"User authentication completed", | |
| 		"Payment transaction initiated", | |
| 		"Database connection established", | |
| 		"Cache miss for key", | |
| 		"API rate limit exceeded", | |
| 		"Service health check passed", | |
| 	} | |
| 
 | |
| 	return SystemLog{ | |
| 		ID:        rand.Int63n(1000000) + 1, | |
| 		Level:     levels[rand.Intn(len(levels))], | |
| 		Service:   services[rand.Intn(len(services))], | |
| 		Message:   messages[rand.Intn(len(messages))], | |
| 		ErrorCode: rand.Intn(1000), | |
| 		Timestamp: time.Now().Add(-time.Duration(rand.Intn(86400*7)) * time.Second), | |
| 	} | |
| } | |
| 
 | |
| func generateErrorLog() interface{} { | |
| 	levels := []string{"error", "critical", "fatal"} | |
| 	services := []string{"auth-service", "payment-service", "user-service", "notification-service", "api-gateway"} | |
| 	messages := []string{ | |
| 		"Database connection failed", | |
| 		"Authentication token expired", | |
| 		"Payment processing error", | |
| 		"Service unavailable", | |
| 		"Memory limit exceeded", | |
| 		"Timeout waiting for response", | |
| 		"Invalid request parameters", | |
| 	} | |
| 
 | |
| 	return SystemLog{ | |
| 		ID:        rand.Int63n(1000000) + 1, | |
| 		Level:     levels[rand.Intn(len(levels))], | |
| 		Service:   services[rand.Intn(len(services))], | |
| 		Message:   messages[rand.Intn(len(messages))], | |
| 		ErrorCode: rand.Intn(100) + 400, // 400-499 error codes | |
| 		Timestamp: time.Now().Add(-time.Duration(rand.Intn(86400*7)) * time.Second), | |
| 	} | |
| } | |
| 
 | |
| func generateMetric() interface{} { | |
| 	names := []string{"cpu_usage", "memory_usage", "disk_usage", "request_latency", "error_rate", "throughput"} | |
| 	tags := []string{ | |
| 		"service=web,region=us-east", | |
| 		"service=api,region=us-west", | |
| 		"service=db,region=eu-central", | |
| 		"service=cache,region=asia-pacific", | |
| 	} | |
| 
 | |
| 	return MetricEntry{ | |
| 		ID:        rand.Int63n(1000000) + 1, | |
| 		Name:      names[rand.Intn(len(names))], | |
| 		Value:     rand.Float64() * 100, | |
| 		Tags:      tags[rand.Intn(len(tags))], | |
| 		Timestamp: time.Now().Add(-time.Duration(rand.Intn(86400*3)) * time.Second), | |
| 	} | |
| } | |
| 
 | |
| func generateProductView() interface{} { | |
| 	categories := []string{"electronics", "books", "clothing", "home", "sports", "automotive"} | |
| 
 | |
| 	return ProductView{ | |
| 		ID:        rand.Int63n(1000000) + 1, | |
| 		ProductID: rand.Int63n(10000) + 1, | |
| 		UserID:    rand.Int63n(5000) + 1, | |
| 		Category:  categories[rand.Intn(len(categories))], | |
| 		Price:     rand.Float64() * 500, | |
| 		ViewCount: rand.Intn(100) + 1, | |
| 		Timestamp: time.Now().Add(-time.Duration(rand.Intn(86400*14)) * time.Second), | |
| 	} | |
| } | |
| 
 | |
| func getEnv(key, defaultValue string) string { | |
| 	if value, exists := os.LookupEnv(key); exists { | |
| 		return value | |
| 	} | |
| 	return defaultValue | |
| }
 |