1// Package conf contains configuration structures used to setup the SDK
2package conf
3
4import (
5	"errors"
6	"fmt"
7	"math"
8	"os/user"
9	"path"
10	"strings"
11
12	impressionlistener "github.com/splitio/go-client/v6/splitio/impressionListener"
13	"github.com/splitio/go-split-commons/v3/conf"
14	"github.com/splitio/go-toolkit/v4/datastructures/set"
15	"github.com/splitio/go-toolkit/v4/logging"
16	"github.com/splitio/go-toolkit/v4/nethelpers"
17)
18
19const (
20	// RedisConsumer mode
21	RedisConsumer = "redis-consumer"
22	// Localhost mode
23	Localhost = "localhost"
24	// InMemoryStandAlone mode
25	InMemoryStandAlone = "inmemory-standalone"
26)
27
28// SplitSdkConfig struct ...
29// struct used to setup a Split.io SDK client.
30//
31// Parameters:
32// - OperationMode (Required) Must be one of ["inmemory-standalone", "redis-consumer"]
33// - InstanceName (Optional) Name to be used when submitting metrics & impressions to split servers
34// - IPAddress (Optional) Address to be used when submitting metrics & impressions to split servers
35// - BlockUntilReady (Optional) How much to wait until the sdk is ready
36// - SplitFile (Optional) File with splits to use when running in localhost mode
37// - LabelsEnabled (Optional) Can be used to disable labels if the user does not want to send that info to split servers.
38// - Logger: (Optional) Custom logger complying with logging.LoggerInterface
39// - LoggerConfig: (Optional) Options to setup the sdk's own logger
40// - TaskPeriods: (Optional) How often should each task run
41// - Redis: (Required for "redis-consumer". Sets up Redis config
42// - Advanced: (Optional) Sets up various advanced options for the sdk
43// - ImpressionsMode (Optional) Flag for enabling local impressions dedupe - Possible values <'optimized'|'debug'>
44type SplitSdkConfig struct {
45	OperationMode      string
46	InstanceName       string
47	IPAddress          string
48	IPAddressesEnabled bool
49	BlockUntilReady    int
50	SplitFile          string
51	LabelsEnabled      bool
52	SplitSyncProxyURL  string
53	Logger             logging.LoggerInterface
54	LoggerConfig       logging.LoggerOptions
55	TaskPeriods        TaskPeriods
56	Advanced           AdvancedConfig
57	Redis              conf.RedisConfig
58	ImpressionsMode    string
59}
60
61// TaskPeriods struct is used to configure the period for each synchronization task
62type TaskPeriods struct {
63	SplitSync      int
64	SegmentSync    int
65	ImpressionSync int
66	GaugeSync      int
67	CounterSync    int
68	LatencySync    int
69	EventsSync     int
70	TelemetrySync  int
71}
72
73// AdvancedConfig exposes more configurable parameters that can be used to further tailor the sdk to the user's needs
74// - ImpressionListener - struct that will be notified each time an impression bulk is ready
75// - HTTPTimeout - Timeout for HTTP requests when doing synchronization
76// - SegmentQueueSize - How many segments can be queued for updating (should be >= # segments the user has)
77// - SegmentWorkers - How many workers will be used when performing segments sync.
78type AdvancedConfig struct {
79	ImpressionListener   impressionlistener.ImpressionListener
80	HTTPTimeout          int
81	SegmentQueueSize     int
82	SegmentWorkers       int
83	AuthServiceURL       string
84	SdkURL               string
85	EventsURL            string
86	StreamingServiceURL  string
87	TelemetryServiceURL  string
88	EventsBulkSize       int64
89	EventsQueueSize      int
90	ImpressionsQueueSize int
91	ImpressionsBulkSize  int64
92	StreamingEnabled     bool
93}
94
95// Default returns a config struct with all the default values
96func Default() *SplitSdkConfig {
97	instanceName := "unknown"
98	ipAddress, err := nethelpers.ExternalIP()
99	if err != nil {
100		ipAddress = "unknown"
101	} else {
102		instanceName = fmt.Sprintf("ip-%s", strings.Replace(ipAddress, ".", "-", -1))
103	}
104
105	var splitFile string
106	usr, err := user.Current()
107	if err != nil {
108		splitFile = "splits"
109	} else {
110		splitFile = path.Join(usr.HomeDir, ".splits")
111	}
112
113	return &SplitSdkConfig{
114		OperationMode:      InMemoryStandAlone,
115		LabelsEnabled:      true,
116		IPAddress:          ipAddress,
117		IPAddressesEnabled: true,
118		InstanceName:       instanceName,
119		Logger:             nil,
120		LoggerConfig:       logging.LoggerOptions{},
121		SplitFile:          splitFile,
122		ImpressionsMode:    conf.ImpressionsModeOptimized,
123		Redis: conf.RedisConfig{
124			Database: 0,
125			Host:     "localhost",
126			Password: "",
127			Port:     6379,
128			Prefix:   "",
129		},
130		TaskPeriods: TaskPeriods{
131			GaugeSync:      defaultTelemetrySync,
132			CounterSync:    defaultTelemetrySync,
133			LatencySync:    defaultTelemetrySync,
134			TelemetrySync:  defaultTelemetrySync,
135			ImpressionSync: defaultImpressionSyncOptimized,
136			SegmentSync:    defaultTaskPeriod,
137			SplitSync:      defaultTaskPeriod,
138			EventsSync:     defaultTaskPeriod,
139		},
140		Advanced: AdvancedConfig{
141			AuthServiceURL:       "",
142			EventsURL:            "",
143			SdkURL:               "",
144			StreamingServiceURL:  "",
145			TelemetryServiceURL:  "",
146			HTTPTimeout:          0,
147			ImpressionListener:   nil,
148			SegmentQueueSize:     500,
149			SegmentWorkers:       10,
150			EventsBulkSize:       5000,
151			EventsQueueSize:      10000,
152			ImpressionsQueueSize: 10000,
153			ImpressionsBulkSize:  5000,
154			StreamingEnabled:     true,
155		},
156	}
157}
158
159func checkImpressionSync(cfg *SplitSdkConfig) error {
160	if cfg.TaskPeriods.ImpressionSync == 0 {
161		cfg.TaskPeriods.ImpressionSync = defaultImpressionSyncOptimized
162	} else {
163		if cfg.TaskPeriods.ImpressionSync < minImpressionSyncOptimized {
164			return fmt.Errorf("ImpressionSync must be >= %d. Actual is: %d", minImpressionSyncOptimized, cfg.TaskPeriods.ImpressionSync)
165		}
166		cfg.TaskPeriods.ImpressionSync = int(math.Max(float64(minImpressionSyncOptimized), float64(cfg.TaskPeriods.ImpressionSync)))
167	}
168	return nil
169}
170
171func validConfigRates(cfg *SplitSdkConfig) error {
172	if cfg.OperationMode == RedisConsumer {
173		return nil
174	}
175
176	if cfg.TaskPeriods.SplitSync < minSplitSync {
177		return fmt.Errorf("SplitSync must be >= %d. Actual is: %d", minSplitSync, cfg.TaskPeriods.SplitSync)
178	}
179	if cfg.TaskPeriods.SegmentSync < minSegmentSync {
180		return fmt.Errorf("SegmentSync must be >= %d. Actual is: %d", minSegmentSync, cfg.TaskPeriods.SegmentSync)
181	}
182
183	cfg.ImpressionsMode = strings.ToLower(cfg.ImpressionsMode)
184	switch cfg.ImpressionsMode {
185	case conf.ImpressionsModeOptimized:
186		err := checkImpressionSync(cfg)
187		if err != nil {
188			return err
189		}
190	case conf.ImpressionsModeDebug:
191		if cfg.TaskPeriods.ImpressionSync == 0 {
192			cfg.TaskPeriods.ImpressionSync = defaultImpressionSyncDebug
193		} else {
194			if cfg.TaskPeriods.ImpressionSync < minImpressionSync {
195				return fmt.Errorf("ImpressionSync must be >= %d. Actual is: %d", minImpressionSync, cfg.TaskPeriods.ImpressionSync)
196			}
197		}
198	default:
199		fmt.Println(`You passed an invalid impressionsMode, impressionsMode should be one of the following values: 'debug' or 'optimized'. Defaulting to 'optimized' mode.`)
200		cfg.ImpressionsMode = conf.ImpressionsModeOptimized
201		err := checkImpressionSync(cfg)
202		if err != nil {
203			return err
204		}
205	}
206
207	if cfg.TaskPeriods.EventsSync < minEventSync {
208		return fmt.Errorf("EventsSync must be >= %d. Actual is: %d", minEventSync, cfg.TaskPeriods.EventsSync)
209	}
210	if cfg.TaskPeriods.TelemetrySync < minTelemetrySync {
211		return fmt.Errorf("TelemetrySync must be >= %d. Actual is: %d", minTelemetrySync, cfg.TaskPeriods.TelemetrySync)
212	}
213	if cfg.Advanced.SegmentWorkers <= 0 {
214		return errors.New("Number of workers for fetching segments MUST be greater than zero")
215	}
216	return nil
217}
218
219// Normalize checks that the parameters passed by the user are correct and updates parameters if necessary.
220// returns an error if something is wrong
221func Normalize(apikey string, cfg *SplitSdkConfig) error {
222	// Fail if no apikey is provided
223	if apikey == "" && cfg.OperationMode != Localhost {
224		return errors.New("Factory instantiation: you passed an empty apikey, apikey must be a non-empty string")
225	}
226
227	// To keep the interface consistent with other sdks we accept "localhost" as an apikey,
228	// which sets the operation mode to localhost
229	if apikey == Localhost {
230		cfg.OperationMode = Localhost
231	}
232
233	// Fail if an invalid operation-mode is provided
234	operationModes := set.NewSet(
235		Localhost,
236		InMemoryStandAlone,
237		RedisConsumer,
238	)
239
240	if !operationModes.Has(cfg.OperationMode) {
241		return fmt.Errorf("OperationMode parameter must be one of: %v", operationModes.List())
242	}
243
244	if cfg.SplitSyncProxyURL != "" {
245		cfg.Advanced.AuthServiceURL = cfg.SplitSyncProxyURL
246		cfg.Advanced.SdkURL = cfg.SplitSyncProxyURL
247		cfg.Advanced.EventsURL = cfg.SplitSyncProxyURL
248		cfg.Advanced.StreamingServiceURL = cfg.SplitSyncProxyURL
249		cfg.Advanced.TelemetryServiceURL = cfg.SplitSyncProxyURL
250	}
251
252	if !cfg.IPAddressesEnabled {
253		cfg.IPAddress = "NA"
254		cfg.InstanceName = "NA"
255	}
256
257	return validConfigRates(cfg)
258}
259