This guide shows you how to quickly use the Simple Log Service SDK for iOS to collect log data.
Prerequisites
You have installed the iOS SDK. For details, see Install the iOS SDK.
Quick start
Initialize the SDK and call the addLog method to send logs.
The iOS SDK supports initializing multiple instances. The
LogProducerConfigandLogProducerClientinstances must be used in pairs.When you send logs to Simple Log Service, you must use the AccessKey pair of an Alibaba Cloud account or a RAM user to authenticate requests and prevent tampering. To prevent security risks from exposed credentials, do not hard-code the AccessKey pair in your mobile application. Instead, we recommend using the direct log transfer service for mobile devices to configure the AccessKey pair. For more information, see Build a service to upload logs from mobile devices to Simple Log Service.
@interface ProducerExampleController ()
// We recommend keeping the LogProducerConfig and LogProducerClient instances as global properties.
@property(nonatomic, strong) LogProducerConfig *config;
@property(nonatomic, strong) LogProducerClient *client;
@end
@implementation ProducerExampleController
// The callback is optional. Use it to track the log sending status.
// To configure the AccessKey pair dynamically, set a callback to update it when invoked.
static void _on_log_send_done(const char * config_name, log_producer_result result, size_t log_bytes, size_t compressed_bytes, const char * req_id, const char * message, const unsigned char * raw_buffer, void * userparams) {
if (result == LOG_PRODUCER_OK) {
NSString *success = [NSString stringWithFormat:@"send success, config : %s, result : %d, log bytes : %d, compressed bytes : %d, request id : %s", config_name, (result), (int)log_bytes, (int)compressed_bytes, req_id];
SLSLogV("%@", success);
} else {
NSString *fail = [NSString stringWithFormat:@"send fail , config : %s, result : %d, log bytes : %d, compressed bytes : %d, request id : %s, error message : %s", config_name, (result), (int)log_bytes, (int)compressed_bytes, req_id, message];
SLSLogV("%@", fail);
}
}
- (void) initLogProducer {
// The service endpoint. The value must begin with https:// or http://.
NSString *endpoint = @"";
NSString *project = @"";
NSString *logstore = @"";
_config = [[LogProducerConfig alloc] initWithEndpoint:endpoint
project:project
logstore:logstore
];
// Set a log topic.
[_config SetTopic:@"example_topic"];
// Add a global tag, which is attached to all logs.
[_config AddTag:@"example" value:@"example_tag"];
// Specifies whether to drop expired logs. 0: Keep the logs and update their timestamps to the current time. 1 (Default): Drop the logs.
[_config SetDropDelayLog:1];
// Specifies whether to drop logs that fail authentication. 0 (Default): Keep the logs. 1: Drop the logs.
[_config SetDropUnauthorizedLog:0];
// To monitor the log sending status, pass a callback.
_client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done];
}
// Request the AccessKey pair.
- (void) requestAccessKey {
// We recommend using the direct log transfer service for mobile devices to configure the AccessKey pair.
// ...
// After obtaining the AccessKey pair, call this method to update it.
[self updateAccessKey:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken];
}
// Update the AccessKey pair.
- (void) updateAccessKey:(NSString *)accessKeyId accessKeySecret:(NSString *)accessKeySecret securityToken:(NSString *)securityToken {
// For an AccessKey pair from Security Token Service (STS) that includes a security token, use this method to update the credentials.
if (securityToken.length > 0) {
if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
[_config ResetSecurityToken:accessKeyId
accessKeySecret:accessKeySecret
securityToken:securityToken
];
}
} else {
// For an AccessKey pair that does not use a security token, use this method.
if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
[_config setAccessKeyId: accessKeyId];
[_config setAccessKeySecret: accessKeySecret];
}
}
}
// Send a log.
- (void) addLog {
Log *log = [Log log];
// Customize the log fields based on your business requirements.
[log putContent:@"content_key_1" intValue:123456];
[log putContent:@"content_key_2" floatValue:23.34f];
[log putContent:@"content_key_3" value:@"Chinese characters"];
[_client AddLog:log];
}
@end Advanced usage
Dynamically configure parameters
The iOS SDK allows you to dynamically configure parameters such as the Project, Logstore, endpoint, and AccessKey pair. For more information about obtaining an endpoint, see Endpoints. For more information about obtaining an AccessKey pair, see AccessKey pair.
Dynamically configure the endpoint, Project, and Logstore.
// You can configure the endpoint, Project, and Logstore parameters independently or together. // Update the endpoint. [_config setEndpoint:@"your new-endpoint"]; // Update the Project. [_config setProject:@"your new-project"]; // Update the Logstore. [_config setLogstore:@"your new-logstore"];Dynamically configure an AccessKey pair.
When dynamically configuring an AccessKey pair, we recommend using a callback.
// If you have initialized the callback when you initialize LogProducerClient, you can ignore the following code. static void _on_log_send_done(const char * config_name, log_producer_result result, size_t log_bytes, size_t compressed_bytes, const char * req_id, const char * message, const unsigned char * raw_buffer, void * userparams) { if (LOG_PRODUCER_SEND_UNAUTHORIZED == result || LOG_PRODUCER_PARAMETERS_INVALID) { [selfClzz requestAccessKey]; // selfClzz is a reference to the current class instance, captured for use in the C-style callback. } } // If you want to check the send status of logs, pass a callback as the second parameter. _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done]; // Request the AccessKey pair. - (void) requestAccessKey { // We recommend that you first use the service for direct log transfer from mobile devices to configure the AccessKey pair. // ... // After you obtain the AccessKey pair, update the credentials. [self updateAccessKey:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken]; } // Update the AccessKey pair. - (void) updateAccessKey:(NSString *)accessKeyId accessKeySecret:(NSString *)accessKeySecret securityToken:(NSString *)securityToken { // If you obtain an AccessKey pair by using STS, the AccessKey pair contains a security token. In this case, you must update the AccessKey pair in the following way. if (securityToken.length > 0) { if (accessKeyId.length > 0 && accessKeySecret.length > 0) { [_config ResetSecurityToken:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken ]; } } else { // If you do not obtain an AccessKey pair by using STS, update the AccessKey pair in the following way. if (accessKeyId.length > 0 && accessKeySecret.length > 0) { [_config setAccessKeyId: accessKeyId]; [_config setAccessKeySecret: accessKeySecret]; } } }Dynamically configure the log source, log topic, and tag.
ImportantYou cannot configure log source, log topic, and tags for individual logs. These settings are global and apply to all unsent logs, including those in the retry buffer. This global application can cause unexpected behavior if you use these parameters to track specific log categories. To track categories, we recommend adding a custom field to each log instead.
// Set the log topic. [_config SetTopic:@"your new-topic"]; // Set the log source. [_config SetSource:@"your new-source"]; // Set tags. The tags are attached to each log. [_config AddTag:@"test" value:@"your new-tag"];
Resumable upload
The iOS SDK supports resumable upload. When enabled, this feature persists logs submitted with the addLog method to a local binary log (binlog) file. The SDK deletes this local data only after successfully sending the logs, ensuring at-least-once delivery.
To enable resumable upload, add the following code during SDK initialization.
When initializing multiple LogProducerConfig instances, you must provide a unique file path to the
setPersistentFilePathmethod for eachLogProducerConfiginstance.If your app uses multiple processes with resumable upload enabled, initialize the SDK only in the main process. If child processes also need to collect data, ensure the file path provided to the
SetPersistentFilePathmethod is unique for each process. Otherwise, log data may become disordered or lost.Repeatedly initializing LogProducerConfig from multiple threads can cause issues.
- (void) initLogProducer {
// A value of 1 enables resumable upload, and 0 disables it. The default value is 0.
[_config SetPersistent:1];
// The name of the persistence file. Ensure that the directory for this file exists.
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *Path = [[paths lastObject] stringByAppendingString:@"/log.dat"];
[_config SetPersistentFilePath:Path];
// The number of persistent files to roll over. We recommend a value of 10.
[_config SetPersistentMaxFileCount:10];
// The size of each persistent file in bytes, specified as N*1024*1024. We recommend a value for N from 1 to 10.
[_config SetPersistentMaxFileSize:N*1024*1024];
// The maximum number of logs to cache locally. Do not exceed 1,048,576. The default is 65,536.
[_config SetPersistentMaxLogCount:65536];
}Configuration parameters
All configuration parameters are provided by the LogProducerConfig class. The following table describes these parameters.
Parameter | Type | Description |
SetTopic | String | Specifies the value for the topic field. The default value is an empty string. |
AddTag | String | Specifies a tag for a log in the |
SetSource | String | Specifies the value for the source field. The default value is |
SetPacketLogBytes | Int | Specifies the maximum size of a log packet in the buffer. Packets exceeding this limit are sent immediately. Value range: 1 to 5,242,880 bytes. Default: 1,048,576 bytes (1 MiB). |
SetPacketLogCount | Int | Specifies the maximum number of logs per packet. If this limit is exceeded, the log packet is sent immediately. Value range: 1 to 4,096. Default: 1,024. |
SetPacketTimeout | Int | Specifies the maximum time a log packet can remain in the buffer. The packet is sent immediately when this timeout is reached. Default: 3,000 milliseconds. |
SetMaxBufferLimit | Int | Specifies the maximum buffer size in bytes for a producer instance. If this limit is exceeded, calls to Default: 67,108,864 (64 MiB). |
SetPersistent | Int | Enables or disables the resumable upload feature.
|
SetPersistentFilePath | String | Specifies the file path for data persistence. The directory must exist. Each The default value is an empty string. |
SetPersistentForceFlush | Int | Specifies whether to force a flush to the persistence file after every
We recommend this option for high-reliability scenarios. |
SetPersistentMaxFileCount | Int | Specifies the maximum number of persistence files for rotation. A value of 0 disables rotation and uses a single file. Recommended range: 1 to 10. Default: 0. |
SetPersistentMaxFileSize | Int | Specifies the maximum size of each persistence file, in bytes. The recommended value is between 1,048,576 bytes (1 MiB) and 10,485,760 bytes (10 MiB). |
SetPersistentMaxLogCount | Int | Specifies the maximum number of logs that can be stored locally for persistence. The value should not exceed 1,048,576. Default: 65,536. |
SetConnectTimeoutSec | Int | Specifies the network connection timeout, in seconds. The default value is 10. |
SetSendTimeoutSec | Int | Specifies the timeout for sending a log packet, in seconds. The default value is 15. |
SetDestroyFlusherWaitSec | Int | Specifies the maximum wait time in seconds for the flusher thread to be destroyed. The default value is 1. |
SetDestroySenderWaitSec | Int | Specifies the maximum wait time in seconds for the sender thread pool to be destroyed. The default value is 1. |
SetCompressType | Int | Specifies the data compression type for uploads.
|
SetNtpTimeOffset | Int | Specifies the offset between the device time and standard NTP time, in seconds. This is calculated as |
SetMaxLogDelayTime | Int | Specifies the maximum allowed time difference between a log's timestamp and the device's local time, in seconds. If this difference is exceeded, the log is processed based on the |
SetDropDelayLog | Int | Specifies whether to drop logs that exceed the
|
SetDropUnauthorizedLog | Int | Specifies whether to drop logs that fail authentication.
|
Error codes
All error codes are defined in log_producer_result. The following table describes each error code and its solution.
Error code | Value | Description | Solution |
LOG_PRODUCER_OK | 0 | Success. | N/A. |
LOG_PRODUCER_INVALID | 1 | The SDK has been destroyed or is invalid. |
|
LOG_PRODUCER_WRITE_ERROR | 2 | Failed to write data. This error can occur if the write traffic for the Project has reached its limit. | Increase the write traffic limit for the Project. For more information, see Adjust resource quotas. |
LOG_PRODUCER_DROP_ERROR | 3 | The disk or memory cache is full. | Increase the values of the |
LOG_PRODUCER_SEND_NETWORK_ERROR | 4 | A network error occurred. | Verify the Endpoint, Project, and Logstore configurations. |
LOG_PRODUCER_SEND_QUOTA_ERROR | 5 | The write traffic quota for the Project has been exceeded. | Increase the write traffic limit for the Project. For more information, see Adjust resource quotas. |
LOG_PRODUCER_SEND_UNAUTHORIZED | 6 | The AccessKey is expired, invalid, or has an incorrectly configured access policy. | Verify the AccessKey. The RAM user must have permissions to manage Simple Log Service resources. For more information, see Grant permissions to a RAM user. |
LOG_PRODUCER_SEND_SERVER_ERROR | 7 | A server-side error occurred. | Contact technical support. |
LOG_PRODUCER_SEND_DISCARD_ERROR | 8 | Data was discarded, usually due to clock skew between the device and the server. | The SDK automatically retries the request. |
LOG_PRODUCER_SEND_TIME_ERROR | 9 | The device clock is not synchronized with the server time. | The SDK automatically corrects this issue. |
LOG_PRODUCER_SEND_EXIT_BUFFERED | 10 | The SDK was destroyed before all cached data could be sent. | To prevent data loss, enable the resumable upload feature. |
LOG_PRODUCER_PARAMETERS_INVALID | 11 | Invalid SDK initialization parameters. | Verify parameters such as AccessKey, Endpoint, Project, and Logstore. |
LOG_PRODUCER_PERSISTENT_ERROR | 99 | Failed to write cached data to the disk. | 1. Ensure the cache file path is correct. 2. Check if the cache file is full. 3. Ensure sufficient disk space is available. |
FAQ
Why do duplicate logs occur?
The iOS SDK sends logs asynchronously. Network issues can cause transmission failures and retries. The SDK confirms delivery only upon receiving a 200 status code. This process can lead to duplicate logs. Use SQL queries to deduplicate data during analysis.
If you observe a high rate of duplicate logs, check for issues in the SDK initialization. Common causes and their solutions include:
Incorrect resumable upload configuration
To resolve this, verify that the file path passed to the
SetPersistentFilePathmethod is globally unique.Repeated SDK initialization
Repeated SDK initialization is often caused by failing to use the singleton pattern correctly. Initialize the SDK as shown in the following example.
// AliyunLogHelper.h @interface AliyunLogHelper : NSObject + (instancetype)sharedInstance; - (void) addLog:(Log *)log; @end // AliyunLogHelper.m @interface AliyunLogHelper () @property(nonatomic, strong) LogProducerConfig *config; @property(nonatomic, strong) LogProducerClient *client; @end @implementation AliyunLogHelper + (instancetype)sharedInstance { static AliyunLogHelper *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } - (instancetype)init { self = [super init]; if (self) { [self initLogProducer]; } return self; } - (void) initLogProducer { // Replace with your initialization code. _config = [[LogProducerConfig alloc] initWithEndpoint:@"" project:@"" logstore:@"" accessKeyID:@"" accessKeySecret:@"" securityToken:@"" ]; _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done]; } - (void) addLog:(Log *)log { if (nil == log) { return; } [_client AddLog:log]; } @endUsing multiple processes can also cause repeated initialization. Initialize the SDK only in the main process. If you must initialize the SDK across different processes, ensure uniqueness by setting a different value for
SetPersistentFilePathin each process.
Optimizing the configuration for weak network environments
If your application operates in a weak network environment, optimize the SDK configuration as shown below.
// Initialize the SDK. - (void) initProducer() { // Adjust the HTTP connection and send timeouts to help reduce the rate of duplicate logs. // You can adjust the timeout values as needed. [_config SetConnectTimeoutSec:20]; [_config SetSendTimeoutSec:20]; // Other initialization parameters. // ... }
Handling log loss
Log reporting is an asynchronous process. If the application closes before reporting completes, log loss can occur. We recommend enabling the resumable upload feature. For more information, see Resumable upload.
Handling log reporting delays
Network conditions and specific application use cases can delay log reporting. If log reporting delays occur on only a few devices, this is typically normal. Otherwise, troubleshoot the issue by using the error codes in the following table.
Error code | Description |
LOG_PRODUCER_SEND_NETWORK_ERROR | Verify that the Endpoint, Project, and Logstore are configured correctly. |
LOG_PRODUCER_SEND_UNAUTHORIZED | Verify that the access key is valid, has not expired, and that its associated permission policy is configured correctly. |
LOG_PRODUCER_SEND_QUOTA_ERROR | The write traffic quota for the Project has been exceeded. For more information, see Adjust resource quotas. |
DNS pre-resolution and cache policies
Yes. The following example shows how to use the iOS SDK with the HTTPDNS SDK to implement DNS pre-resolution and cache policies. For more information, see Solutions for HTTPS and SNI scenarios.
Note: If the SLS Endpoint uses an HTTPS domain name, you must refer to Solutions for HTTPS and SNI scenarios.
Implement a custom NSURLProtocol.
#import <Foundation/Foundation.h> #import "HttpDnsNSURLProtocolImpl.h" #import <arpa/inet.h> #import <zlib.h> #import <objc/runtime.h> static NSString *const hasBeenInterceptedCustomLabelKey = @"HttpDnsHttpMessagePropertyKey"; static NSString *const kAnchorAlreadyAdded = @"AnchorAlreadyAdded"; @interface HttpDnsNSURLProtocolImpl () <NSStreamDelegate> @property (strong, readwrite, nonatomic) NSMutableURLRequest *curRequest; @property (strong, readwrite, nonatomic) NSRunLoop *curRunLoop; @property (strong, readwrite, nonatomic) NSInputStream *inputStream; @property (nonatomic, assign) BOOL responseIsHandle; @property (assign, nonatomic) z_stream gzipStream; @end @implementation HttpDnsNSURLProtocolImpl - (instancetype)initWithRequest:(NSURLRequest *)request cachedResponse:(nullable NSCachedURLResponse *)cachedResponse client:(nullable id <NSURLProtocolClient>)client { self = [super initWithRequest:request cachedResponse:cachedResponse client:client]; if (self) { _gzipStream.zalloc = Z_NULL; _gzipStream.zfree = Z_NULL; if (inflateInit2(&_gzipStream, 16 + MAX_WBITS) != Z_OK) { [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"gzip initialize fail" code:-1 userInfo:nil]]; } } return self; } /** * Specifies whether to intercept and process the specified request. * * @param request The specified request. * @return YES to intercept the request, or NO to pass it through. */ + (BOOL)canInitWithRequest:(NSURLRequest *)request { if([[request.URL absoluteString] isEqual:@"about:blank"]) { return NO; } // Prevents an infinite loop that can occur if a request is re-intercepted. if ([NSURLProtocol propertyForKey:hasBeenInterceptedCustomLabelKey inRequest:request]) { return NO; } NSString * url = request.URL.absoluteString; NSString * domain = request.URL.host; // Intercept only HTTPS requests. if (![url hasPrefix:@"https"]) { return NO; } // You can add more conditions, such as configuring a host array to intercept only the hosts in that array. // Intercept only requests whose hosts are replaced with IP addresses. if (![self isPlainIpAddress:domain]) { return NO; } return YES; } + (BOOL)isPlainIpAddress:(NSString *)hostStr { if (!hostStr) { return NO; } // Checks whether the address is an IPv4 address. const char *utf8 = [hostStr UTF8String]; int success = 0; struct in_addr dst; success = inet_pton(AF_INET, utf8, &dst); if (success == 1) { return YES; } // Checks whether the address is an IPv6 address. struct in6_addr dst6; success = inet_pton(AF_INET6, utf8, &dst6); if (success == 1) { return YES; } return NO; } // To redirect the request or add headers, perform the operations in this method. + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } // Starts to load the request. - (void)startLoading { NSMutableURLRequest *request = [self.request mutableCopy]; // Marks the request as processed to prevent infinite loops. [NSURLProtocol setProperty:@(YES) forKey:hasBeenInterceptedCustomLabelKey inRequest:request]; self.curRequest = [self createNewRequest:request]; [self startRequest]; } - (NSString *)cookieForURL:(NSURL *)URL { NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; NSMutableArray *cookieList = [NSMutableArray array]; for (NSHTTPCookie *cookie in [cookieStorage cookies]) { if (![self p_checkCookie:cookie URL:URL]) { continue; } [cookieList addObject:cookie]; } if (cookieList.count > 0) { NSDictionary *cookieDic = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieList]; if ([cookieDic objectForKey:@"Cookie"]) { return cookieDic[@"Cookie"]; } } return nil; } - (BOOL)p_checkCookie:(NSHTTPCookie *)cookie URL:(NSURL *)URL { if (cookie.domain.length <= 0 || URL.host.length <= 0) { return NO; } if ([URL.host containsString:cookie.domain]) { return YES; } return NO; } - (NSMutableURLRequest *)createNewRequest:(NSURLRequest*)request { NSURL* originUrl = request.URL; NSString *cookie = [self cookieForURL:originUrl]; NSMutableURLRequest* mutableRequest = [request copy]; [mutableRequest setValue:cookie forHTTPHeaderField:@"Cookie"]; return [mutableRequest copy]; } /** * Cancels the request. */ - (void)stopLoading { if (_inputStream.streamStatus == NSStreamStatusOpen) { [self closeStream:_inputStream]; } [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"stop loading" code:-1 userInfo:nil]]; } /** * Forwards the request using CFHTTPMessage. */ - (void)startRequest { // The header information of the original request. NSDictionary *headFields = _curRequest.allHTTPHeaderFields; CFStringRef url = (__bridge CFStringRef) [_curRequest.URL absoluteString]; CFURLRef requestURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL); // The method of the original request, such as GET or POST. CFStringRef requestMethod = (__bridge_retained CFStringRef) _curRequest.HTTPMethod; // Creates a CFHTTPMessageRef object based on the URL, method, and version of the request. CFHTTPMessageRef cfrequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, requestURL, kCFHTTPVersion1_1); // Adds the data attached to the HTTP POST request. CFStringRef requestBody = CFSTR(""); CFDataRef bodyData = CFStringCreateExternalRepresentation(kCFAllocatorDefault, requestBody, kCFStringEncodingUTF8, 0); if (_curRequest.HTTPBody) { bodyData = (__bridge_retained CFDataRef) _curRequest.HTTPBody; } else if (_curRequest.HTTPBodyStream) { NSData *data = [self dataWithInputStream:_curRequest.HTTPBodyStream]; NSString *strBody = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"originStrBody: %@", strBody); CFDataRef body = (__bridge_retained CFDataRef) data; CFHTTPMessageSetBody(cfrequest, body); CFRelease(body); } else { CFHTTPMessageSetBody(cfrequest, bodyData); } // Copies the header information of the original request. for (NSString* header in headFields) { CFStringRef requestHeader = (__bridge CFStringRef) header; CFStringRef requestHeaderValue = (__bridge CFStringRef) [headFields valueForKey:header]; CFHTTPMessageSetHeaderFieldValue(cfrequest, requestHeader, requestHeaderValue); } // Creates an input stream for the CFHTTPMessage object. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CFReadStreamRef readStream = CFReadStreamCreateForHTTPRequest(kCFAllocatorDefault, cfrequest); #pragma clang diagnostic pop self.inputStream = (__bridge_transfer NSInputStream *) readStream; // Sets the Server Name Indication (SNI) host information. This is a key step. NSString *host = [_curRequest.allHTTPHeaderFields objectForKey:@"host"]; if (!host) { host = _curRequest.URL.host; } [_inputStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL forKey:NSStreamSocketSecurityLevelKey]; NSDictionary *sslProperties = [[NSDictionary alloc] initWithObjectsAndKeys: host, (__bridge id) kCFStreamSSLPeerName, nil]; [_inputStream setProperty:sslProperties forKey:(__bridge_transfer NSString *) kCFStreamPropertySSLSettings]; [_inputStream setDelegate:self]; if (!_curRunLoop) { // Saves the runloop of the current thread. This is critical for redirected requests. self.curRunLoop = [NSRunLoop currentRunLoop]; } // Schedules the stream on the current runloop. [_inputStream scheduleInRunLoop:_curRunLoop forMode:NSRunLoopCommonModes]; [_inputStream open]; CFRelease(cfrequest); CFRelease(requestURL); cfrequest = NULL; CFRelease(bodyData); CFRelease(requestBody); CFRelease(requestMethod); } - (NSData*)dataWithInputStream:(NSInputStream*)stream { NSMutableData *data = [NSMutableData data]; [stream open]; NSInteger result; uint8_t buffer[1024]; while ((result = [stream read:buffer maxLength:1024]) != 0) { if (result > 0) { // buffer contains result bytes of data to be handled [data appendBytes:buffer length:result]; } else if (result < 0) { // The stream had an error. You can get an NSError object using [iStream streamError] data = nil; break; } } [stream close]; return data; } #pragma mark - NSStreamDelegate /** * Handles stream events. */ - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode { if (eventCode == NSStreamEventHasBytesAvailable) { CFReadStreamRef readStream = (__bridge_retained CFReadStreamRef) aStream; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader); #pragma clang diagnostic pop if (CFHTTPMessageIsHeaderComplete(message)) { NSInputStream *inputstream = (NSInputStream *) aStream; NSNumber *alreadyAdded = objc_getAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded)); NSDictionary *headDict = (__bridge NSDictionary *) (CFHTTPMessageCopyAllHeaderFields(message)); if (!alreadyAdded || ![alreadyAdded boolValue]) { objc_setAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded), [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_COPY); // Notifies the client that the response is received. This notification is sent only once. CFStringRef httpVersion = CFHTTPMessageCopyVersion(message); // Obtains the status code from the response header. CFIndex statusCode = CFHTTPMessageGetResponseStatusCode(message); if (!self.responseIsHandle) { NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:_curRequest.URL statusCode:statusCode HTTPVersion:(__bridge NSString *) httpVersion headerFields:headDict]; [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; self.responseIsHandle = YES; } // Verifies the certificate. SecTrustRef trust = (__bridge SecTrustRef) [aStream propertyForKey:(__bridge NSString *) kCFStreamPropertySSLPeerTrust]; SecTrustResultType res = kSecTrustResultInvalid; NSMutableArray *policies = [NSMutableArray array]; NSString *domain = [[_curRequest allHTTPHeaderFields] valueForKey:@"host"]; if (domain) { [policies addObject:(__bridge_transfer id) SecPolicyCreateSSL(true, (__bridge CFStringRef) domain)]; } else { [policies addObject:(__bridge_transfer id) SecPolicyCreateBasicX509()]; } // Binds the verification policy to the server certificate. SecTrustSetPolicies(trust, (__bridge CFArrayRef) policies); if (SecTrustEvaluate(trust, &res) != errSecSuccess) { [self closeStream:aStream]; [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"can not evaluate the server trust" code:-1 userInfo:nil]]; return; } if (res != kSecTrustResultProceed && res != kSecTrustResultUnspecified) { // If the certificate fails verification, close the input stream. [self closeStream:aStream]; [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]]; } else { // The certificate is verified. if (statusCode >= 300 && statusCode < 400) { // Handles the redirection error code. [self closeStream:aStream]; [self handleRedirect:message]; } else { NSError *error = nil; NSData *data = [self readDataFromInputStream:inputstream headerDict:headDict stream:aStream error:&error]; if (error) { [self.client URLProtocol:self didFailWithError:error]; } else { [self.client URLProtocol:self didLoadData:data]; } } } } else { NSError *error = nil; NSData *data = [self readDataFromInputStream:inputstream headerDict:headDict stream:aStream error:&error]; if (error) { [self.client URLProtocol:self didFailWithError:error]; } else { [self.client URLProtocol:self didLoadData:data]; } } CFRelease((CFReadStreamRef)inputstream); CFRelease(message); } } else if (eventCode == NSStreamEventErrorOccurred) { [self closeStream:aStream]; inflateEnd(&_gzipStream); // Notifies the client that an error occurred. [self.client URLProtocol:self didFailWithError: [[NSError alloc] initWithDomain:@"NSStreamEventErrorOccurred" code:-1 userInfo:nil]]; } else if (eventCode == NSStreamEventEndEncountered) { CFReadStreamRef readStream = (__bridge_retained CFReadStreamRef) aStream; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CFHTTPMessageRef message = (CFHTTPMessageRef) CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPResponseHeader); #pragma clang diagnostic pop if (CFHTTPMessageIsHeaderComplete(message)) { NSNumber *alreadyAdded = objc_getAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded)); NSDictionary *headDict = (__bridge NSDictionary *) (CFHTTPMessageCopyAllHeaderFields(message)); if (!alreadyAdded || ![alreadyAdded boolValue]) { objc_setAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded), [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_COPY); // Notifies the client that the response is received. This notification is sent only once. if (!self.responseIsHandle) { CFStringRef httpVersion = CFHTTPMessageCopyVersion(message); // Obtains the status code from the response header. CFIndex statusCode = CFHTTPMessageGetResponseStatusCode(message); NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:_curRequest.URL statusCode:statusCode HTTPVersion:(__bridge NSString *) httpVersion headerFields:headDict]; [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; self.responseIsHandle = YES; } } } [self closeStream:_inputStream]; inflateEnd(&_gzipStream); [self.client URLProtocolDidFinishLoading:self]; } } - (NSData *)readDataFromInputStream:(NSInputStream *)inputStream headerDict:(NSDictionary *)headDict stream:(NSStream *)aStream error:(NSError **)error { // In case the response header is incomplete. UInt8 buffer[16 * 1024]; NSInteger length = [inputStream read:buffer maxLength:sizeof(buffer)]; if (length < 0) { *error = [[NSError alloc] initWithDomain:@"inputstream length is invalid" code:-2 userInfo:nil]; [aStream removeFromRunLoop:_curRunLoop forMode:NSRunLoopCommonModes]; [aStream setDelegate:nil]; [aStream close]; return nil; } NSData *data = [[NSData alloc] initWithBytes:buffer length:length]; if (headDict[@"Content-Encoding"] && [headDict[@"Content-Encoding"] containsString:@"gzip"]) { data = [self gzipUncompress:data]; if (!data) { *error = [[NSError alloc] initWithDomain:@"can't read any data" code:-3 userInfo:nil]; return nil; } } return data; } - (void)closeStream:(NSStream*)stream { [stream removeFromRunLoop:_curRunLoop forMode:NSRunLoopCommonModes]; [stream setDelegate:nil]; [stream close]; } - (void)handleRedirect:(CFHTTPMessageRef)messageRef { // Response header. CFDictionaryRef headerFieldsRef = CFHTTPMessageCopyAllHeaderFields(messageRef); NSDictionary *headDict = (__bridge_transfer NSDictionary *)headerFieldsRef; [self redirect:headDict]; } - (void)redirect:(NSDictionary *)headDict { // If cookies are required for redirection, handle them. NSString *location = headDict[@"Location"]; if (!location) location = headDict[@"location"]; NSURL *url = [[NSURL alloc] initWithString:location]; _curRequest.URL = url; if ([[_curRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) { // Per RFCs, a redirected POST request should be converted to a GET request. _curRequest.HTTPMethod = @"GET"; _curRequest.HTTPBody = nil; } [self startRequest]; } - (NSData *)gzipUncompress:(NSData *)gzippedData { if ([gzippedData length] == 0) { return gzippedData; } unsigned full_length = (unsigned) [gzippedData length]; unsigned half_length = (unsigned) [gzippedData length] / 2; NSMutableData *decompressed = [NSMutableData dataWithLength:full_length + half_length]; BOOL done = NO; int status; _gzipStream.next_in = (Bytef *)[gzippedData bytes]; _gzipStream.avail_in = (uInt)[gzippedData length]; _gzipStream.total_out = 0; while (_gzipStream.avail_in != 0 && !done) { if (_gzipStream.total_out >= [decompressed length]) { [decompressed increaseLengthBy:half_length]; } _gzipStream.next_out = (Bytef *)[decompressed mutableBytes] + _gzipStream.total_out; _gzipStream.avail_out = (uInt)([decompressed length] - _gzipStream.total_out); status = inflate(&_gzipStream, Z_SYNC_FLUSH); if (status == Z_STREAM_END) { done = YES; } else if (status == Z_BUF_ERROR) { // If Z_BUF_ERROR is caused by an insufficient output buffer, the input buffer is not fully processed and the output buffer is full. In this case, the loop must continue to expand the buffer. // The opposite condition indicates that the error is not caused by an insufficient output buffer. In this case, the loop must be terminated, which indicates an error. if (_gzipStream.avail_in == 0 || _gzipStream.avail_out != 0) { return nil; } } else if (status != Z_OK) { return nil; } } [decompressed setLength:_gzipStream.total_out]; return [NSData dataWithData:decompressed]; } @endImplement a custom BeforeSend.
- (void)setupHttpDNS:(NSString *)accountId { // Configure HTTPDNS. HttpDnsService *httpdns = [[HttpDnsService alloc] initWithAccountID:accountId]; [httpdns setHTTPSRequestEnabled:YES]; [httpdns setPersistentCacheIPEnabled:YES]; [httpdns setReuseExpiredIPEnabled:YES]; [httpdns setIPv6Enabled:YES]; NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:configuration.protocolClasses]; // Set the custom NSURLProtocol defined in the preceding step. [protocolsArray insertObject:[HttpDnsNSURLProtocolImpl class] atIndex:0]; [configuration setProtocolClasses:protocolsArray]; NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:nil]; [SLSURLSession setURLSession:session]; [SLSURLSession setBeforeSend:^NSMutableURLRequest * _Nonnull(NSMutableURLRequest * _Nonnull request) { NSURL *url = request.URL; // You can add a condition here to apply this logic only to specific URLs. HttpdnsResult *result = [httpdns resolveHostSync:url.host byIpType:HttpdnsQueryIPTypeAuto]; if (!result) { return request; } NSString *ipAddress = nil; if (result.hasIpv4Address) { ipAddress = result.firstIpv4Address; } else if(result.hasIpv6Address) { ipAddress = result.firstIpv6Address; } else { return request; } NSString *requestUrl = url.absoluteString; requestUrl = [requestUrl stringByReplacingOccurrencesOfString: url.host withString:ipAddress]; [request setURL:[NSURL URLWithString:requestUrl]]; [request setValue:url.host forHTTPHeaderField:@"host"]; return request; }]; }Complete the SLS SDK initialization.
@implementation AliyunSLS - (void)initSLS { // Set custom HTTPDNS for the SLS SDK. // Note: // This setting applies to all SLS SDK instances. [self setupHttpDNS:accountId]; [self initProducer]; } - (void)initProducer { // Initialize LogProducerConfig and LogProducerClient as usual. // ... } @end