全部产品
Search
文档中心

Simple Log Service:Memulai dengan iOS SDK

更新时间:Aug 23, 2025

Topik ini menjelaskan cara menggunakan kit pengembangan perangkat lunak (SDK) Simple Log Service untuk iOS dalam mengumpulkan data log.

Prasyarat

Pastikan iOS SDK telah diinstal. Untuk detail lebih lanjut, lihat Instal iOS SDK.

Panduan Cepat

Inisialisasi SDK dan gunakan metode addLog untuk mengirim log.

Penting
  • iOS SDK mendukung inisialisasi beberapa instance. Instance LogProducerConfig dan LogProducerClient harus digunakan berpasangan.

  • Saat Anda mengirim log ke Simple Log Service, Anda harus menggunakan Pasangan Kunci Akses dari Akun Alibaba Cloud atau Pengguna Resource Access Management (RAM) untuk otentikasi dan proteksi anti-pemalsuan. Menyimpan Pasangan Kunci Akses di aplikasi seluler Anda menimbulkan risiko keamanan. Untuk menghindari risiko ini, gunakan layanan untuk mentransfer log langsung dari perangkat seluler dan konfigurasikan Pasangan Kunci Akses di sisi layanan. Untuk informasi lebih lanjut, lihat Bangun layanan untuk mengunggah log dari perangkat seluler ke Simple Log Service.

@interface ProducerExampleController ()
// Simpan secara global instans LogProducerConfig dan LogProducerClient.
@property(nonatomic, strong) LogProducerConfig *config;
@property(nonatomic, strong) LogProducerClient *client;
@end

@implementation ProducerExampleController


// Fungsi callback bersifat opsional. Jika Anda tidak perlu mengetahui apakah sebuah log telah dikirim, Anda tidak perlu mendaftarkan fungsi callback.
// Untuk mengonfigurasi ulang Pasangan Kunci Akses secara dinamis, atur fungsi callback dan perbarui Pasangan Kunci Akses saat fungsi callback dipanggil.
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:@"pengiriman berhasil, config : %s, hasil : %d, ukuran log : %d, ukuran terkompresi : %d, ID permintaan : %s", config_name, (result), (int)log_bytes, (int)compressed_bytes, req_id];
        SLSLogV("%@", success);
    } else {
        NSString *fail = [NSString stringWithFormat:@"pengiriman gagal   , config : %s, hasil : %d, ukuran log : %d, ukuran terkompresi : %d, ID permintaan : %s, pesan kesalahan : %s", config_name, (result), (int)log_bytes, (int)compressed_bytes, req_id, message];
        SLSLogV("%@", fail);
    }
}

- (void) initLogProducer {
    // Titik akhir Simple Log Service. Titik akhir harus dimulai dengan https:// atau http://.
    NSString *endpoint = @"titik akhir Anda";
    NSString *project = @"proyek Anda";
    NSString *logstore = @"penyimpanan log Anda";

    _config = [[LogProducerConfig alloc] initWithEndpoint:endpoint
                                                  project:project
                                                 logstore:logstore
    ];

    // Atur topik log.
    [_config SetTopic:@"contoh_topik"];
    // Atur tag. Tag tersebut dilampirkan pada setiap log.
    [_config AddTag:@"contoh" value:@"tag_contoh"];
    // Tentukan apakah akan membuang log yang kedaluwarsa. Nilai 0 menunjukkan bahwa log yang kedaluwarsa tidak dibuang dan waktu log diperbarui ke waktu saat ini. Nilai 1 menunjukkan bahwa log yang kedaluwarsa dibuang. Nilai default: 1.
    [_config SetDropDelayLog:1];
    // Tentukan apakah akan membuang log yang gagal dalam otentikasi. Nilai 0 menunjukkan bahwa log tersebut tidak dibuang. Nilai 1 menunjukkan bahwa log tersebut dibuang. Nilai default: 0.
    [_config SetDropUnauthorizedLog:0];    

    // Jika Anda ingin memeriksa apakah sebuah log telah dikirim, lewatkan fungsi callback sebagai parameter kedua.
    _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done];
}

// Minta informasi Pasangan Kunci Akses.
- (void) requestAccessKey {
    // Pertama, gunakan layanan untuk transfer log langsung dari perangkat seluler untuk mengonfigurasi informasi Pasangan Kunci Akses.
    // ...

    // Setelah Anda mendapatkan informasi Pasangan Kunci Akses, perbarui informasinya.
    [self updateAccessKey:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken];
}

// Perbarui informasi Pasangan Kunci Akses.
- (void) updateAccessKey:(NSString *)accessKeyId accessKeySecret:(NSString *)accessKeySecret securityToken:(NSString *)securityToken {
    
    // Jika Anda mendapatkan Pasangan Kunci Akses menggunakan Layanan Token Keamanan (STS), Pasangan Kunci Akses tersebut berisi token keamanan. Dalam hal ini, Anda harus memperbarui Pasangan Kunci Akses dengan cara berikut.
    if (securityToken.length > 0) {
        if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
            [_config ResetSecurityToken:accessKeyId
                        accessKeySecret:accessKeySecret
                          securityToken:securityToken
            ];
        }
    } else {
        // Jika Anda tidak mendapatkan Pasangan Kunci Akses menggunakan STS, perbarui Pasangan Kunci Akses dengan cara berikut.
        if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
            [_config setAccessKeyId: accessKeyId];
            [_config setAccessKeySecret: accessKeySecret];
        }
    }
}
// Laporkan sebuah log.
- (void) addLog {
    Log *log = [Log log];
    // Sesuaikan bidang yang akan dilaporkan sesuai kebutuhan.
    [log putContent:@"content_key_1" intValue:123456];
    [log putContent:@"content_key_2" floatValue:23.34f];
    [log putContent:@"content_key_3" value:@"Karakter Tiongkok"];
    
    [_client AddLog:log];
}
@end

Penggunaan lanjutan

Konfigurasi Parameter Dinamis

iOS SDK mendukung konfigurasi dinamis parameter seperti ProjectName, Logstore, Endpoint, dan Pasangan Kunci Akses. Untuk informasi tentang cara mendapatkan titik akhir, lihat Titik Akhir. Untuk informasi tentang cara mendapatkan Pasangan Kunci Akses, lihat Pasangan Kunci Akses.

  • Konfigurasi dinamis Endpoint, ProjectName, dan Logstore.

    // Anda dapat mengonfigurasi parameter Endpoint, ProjectName, dan Logstore secara independen atau bersamaan.
    // Perbarui titik akhir.
    [_config setEndpoint:@"your new-endpoint"];
    // Perbarui ProjectName.
    [_config setProject:@"your new-project"];
    // Perbarui Logstore.
    [_config setLogstore:@"your new-logstore"];
  • Konfigurasi dinamis Pasangan Kunci Akses.

    Saat Anda mengonfigurasi Pasangan Kunci Akses secara dinamis, Anda juga harus menggunakan fungsi callback.

    // Jika Anda telah menginisialisasi fungsi callback saat Anda menginisialisasi LogProducerClient, Anda dapat mengabaikan kode berikut.
    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 adalah referensi ke instans kelas saat ini.
        }
    }
    
    // Jika Anda ingin memeriksa apakah sebuah log telah dikirim, lewatkan fungsi callback sebagai parameter kedua.
    _client = [[LogProducerClient alloc] initWithLogProducerConfig:_config callback:_on_log_send_done];
    
    // Minta informasi Pasangan Kunci Akses.
    - (void) requestAccessKey {
        // Pertama, gunakan layanan untuk transfer log langsung dari perangkat seluler untuk mengonfigurasi informasi Pasangan Kunci Akses.
        // ...
    
        // Setelah Anda mendapatkan informasi Pasangan Kunci Akses, perbarui informasinya.
        [self updateAccessKey:accessKeyId accessKeySecret:accessKeySecret securityToken:securityToken];
    }
    
    // Perbarui informasi Pasangan Kunci Akses.
    - (void) updateAccessKey:(NSString *)accessKeyId accessKeySecret:(NSString *)accessKeySecret securityToken:(NSString *)securityToken {
        
        // Jika Anda mendapatkan Pasangan Kunci Akses menggunakan STS, Pasangan Kunci Akses tersebut berisi token keamanan. Dalam hal ini, Anda harus memperbarui Pasangan Kunci Akses dengan cara berikut.
        if (securityToken.length > 0) {
            if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
                [_config ResetSecurityToken:accessKeyId
                            accessKeySecret:accessKeySecret
                              securityToken:securityToken
                ];
            }
        } else {
            // Jika Anda tidak mendapatkan Pasangan Kunci Akses menggunakan STS, perbarui Pasangan Kunci Akses dengan cara berikut.
            if (accessKeyId.length > 0 && accessKeySecret.length > 0) {
                [_config setAccessKeyId: accessKeyId];
                [_config setAccessKeySecret: accessKeySecret];
            }
        }
    }
  • Konfigurasi dinamis sumber, topik, dan tag.

    Penting

    Pengaturan ini berlaku secara global dan memengaruhi semua log berikutnya, termasuk log apa pun yang saat ini ada di buffer ulang. Jika logika bisnis Anda memerlukan pelacakan jenis log tertentu dengan parameter ini, perilaku global ini dapat menyebabkan hasil yang tidak terduga. Anda dapat menambahkan bidang kustom ke setiap log untuk mengidentifikasi jenisnya.

    // Atur topik log.
    [_config SetTopic:@"your new-topic"];
    // Atur sumber log.
    [_config SetSource:@"your new-source"];
    // Atur tag. Tag tersebut dilampirkan pada setiap log.
    [_config AddTag:@"test" value:@"your new-tag"];

Unggah yang dapat dilanjutkan

iOS SDK mendukung unggah yang dapat dilanjutkan. Saat fitur ini diaktifkan, log yang dikirim menggunakan metode addLog pertama kali disimpan ke file log biner lokal (binlog). Data lokal hanya dihapus setelah log berhasil dikirim. Ini memastikan semantik setidaknya sekali untuk pengunggahan log.

Untuk mengimplementasikan unggah yang dapat dilanjutkan, Anda dapat menambahkan kode berikut selama inisialisasi SDK.

Penting
  • Saat Anda menginisialisasi beberapa instans LogProducerConfig, Anda harus memberikan jalur file unik ke metode setPersistentFilePath untuk setiap instans LogProducerConfig.

  • Jika aplikasi memiliki beberapa proses dan fitur unggah yang dapat dilanjutkan diaktifkan, inisialisasi SDK hanya di proses utama. Jika proses anak juga perlu mengumpulkan data, pastikan jalur file yang diberikan ke metode SetPersistentFilePath unik. Jika tidak, data log mungkin menjadi tidak teratur atau hilang.

  • Perhatikan potensi masalah dengan inisialisasi LogProducerConfig berulang yang disebabkan oleh beberapa thread.

- (void) initLogProducer {
    // Nilai 1 mengaktifkan unggah yang dapat dilanjutkan. Nilai 0 menonaktifkan fitur ini. Nilai default: 0.
    [_config SetPersistent:1];
    
    // Nama file persistensi. Pastikan folder tempat file disimpan telah dibuat.
    NSArray  *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *Path = [[paths lastObject] stringByAppendingString:@"/log.dat"];
    [_config SetPersistentFilePath:Path];
    // Jumlah file persisten untuk rotasi. Atur parameter ini ke 10.
    [_config SetPersistentMaxFileCount:10];
    // Ukuran setiap file persisten dalam byte. Nilainya dihitung menggunakan rumus N × 1024 × 1024. Atur N ke nilai dari 1 hingga 10.
    [_config SetPersistentMaxFileSize:N*1024*1024];
    // Jumlah maksimum log yang dapat disimpan secara lokal. Jangan atur parameter ini ke nilai lebih besar dari 1.048.576. Nilai default adalah 65.536.
    [_config SetPersistentMaxLogCount:65536];
}

Parameter konfigurasi

Anda dapat mengatur semua parameter konfigurasi di kelas LogProducerConfig. Tabel berikut menjelaskan parameter-parameter ini.

Parameter

Tipe data

Deskripsi

SetTopic

String

Atur nilai bidang topik. Nilai default adalah string kosong.

AddTag

String

Atur tag dalam format tag:xxxx. Nilai default adalah string kosong.

SetSource

String

Atur nilai bidang sumber. Nilai default: iOS.

SetPacketLogBytes

Int

Ukuran maksimum paket log yang di-cache. Jika batas terlampaui, log segera dikirim.

Nilai valid: 1 hingga 5.242.880. Nilai default: 1024 × 1024. Unit: byte.

SetPacketLogCount

Int

Jumlah maksimum log dalam paket log yang di-cache. Jika batas terlampaui, log segera dikirim.

Nilai valid: 1 hingga 4.096. Nilai default: 1024.

SetPacketTimeout

Int

Periode timeout untuk mengirim log yang di-cache. Jika periode timeout berakhir, log segera dikirim.

Nilai default: 3.000. Unit: milidetik.

SetMaxBufferLimit

Int

Memori maksimum yang dapat digunakan oleh satu instance Klien Produser. Jika batas terlampaui, antarmuka add_log segera mengembalikan kegagalan.

Nilai default: 64 × 1024 × 1024.

SetPersistent

Int

Menentukan apakah akan mengaktifkan unggah yang dapat dilanjutkan.

  • 1: mengaktifkan fitur.

  • 0 (default): menonaktifkan fitur.

SetPersistentFilePath

String

Nama file persistensi. Pastikan folder tempat file disimpan telah dibuat. Saat Anda mengonfigurasi beberapa instance LogProducerConfig, pastikan nama tersebut unik.

Nilai default adalah kosong.

SetPersistentForceFlush

Int

Menentukan apakah akan mengaktifkan fitur flush paksa untuk setiap pemanggilan AddLog.

  • 1: mengaktifkan fitur. Aktifkan fitur ini dengan hati-hati karena memengaruhi kinerja.

  • 0 (default): menonaktifkan fitur.

Aktifkan fitur ini dalam skenario dengan reliabilitas tinggi.

SetPersistentMaxFileCount

Int

Jumlah file persisten untuk rotasi. Nilai yang direkomendasikan: 1 hingga 10. Nilai default: 0.

SetPersistentMaxFileSize

Int

Ukuran setiap file persisten dalam byte. Nilainya dihitung menggunakan rumus N × 1024 × 1024. Atur N ke nilai dari 1 hingga 10.

SetPersistentMaxLogCount

Int

Jumlah maksimum log yang dapat disimpan secara lokal. Jangan atur parameter ini ke nilai lebih besar dari 1.048.576. Nilai default adalah 65.536.

SetConnectTimeoutSec

IntInt

Periode timeout koneksi. Nilai default: 10. Unit: detik.

SetSendTimeoutSec

Int

Periode timeout untuk mengirim log. Nilai default: 15. Unit: detik.

SetDestroyFlusherWaitSec

Int

Waktu tunggu maksimum untuk thread flusher dihancurkan. Nilai default: 1. Unit: detik.

SetDestroySenderWaitSec

Int

Waktu tunggu maksimum untuk kolam thread pengirim dihancurkan. Nilai default: 1. Unit: detik.

SetCompressType

Int

Tipe kompresi untuk pengunggahan data.

  • 0: tanpa kompresi.

  • 1 (default): kompresi LZ4.

SetNtpTimeOffset

Int

Perbedaan antara waktu perangkat dan waktu standar. Nilainya dihitung menggunakan rumus: Waktu standar - Waktu perangkat. Perbedaan ini biasanya terjadi karena waktu perangkat klien tidak disinkronkan. Nilai default: 0. Unit: detik.

SetMaxLogDelayTime

Int

Perbedaan antara waktu log dan waktu lokal. Jika perbedaan melebihi nilai ini, SDK memproses log berdasarkan opsi setDropDelayLog. Unit: detik. Nilai default: 7 × 24 × 3600, yaitu 7 hari.

SetDropDelayLog

Int

Menentukan apakah akan membuang log yang kedaluwarsa yang melebihi nilai setMaxLogDelayTime.

  • 0: tidak membuang log dan memperbarui waktu log ke waktu saat ini.

  • 1 (default): membuang log.

SetDropUnauthorizedLog

Int

Menentukan apakah akan membuang log yang gagal otentikasi.

  • 0 (default): tidak membuang log.

  • 1: membuang log.

Kode Kesalahan

Semua kode kesalahan didefinisikan dalam log_producer_result. Untuk informasi lebih lanjut, lihat tabel berikut.

Kode Kesalahan

Nilai

Deskripsi

Solusi

LOG_PRODUCER_OK

0

Berhasil.

Tidak ada.

LOG_PRODUCER_INVALID

1

SDK dihancurkan atau tidak valid.

  1. Periksa apakah SDK diinisialisasi dengan benar.

  2. Periksa apakah metode destroy() dipanggil.

LOG_PRODUCER_WRITE_ERROR

2

Kesalahan penulisan data terjadi. Penyebabnya mungkin karena lalu lintas tulis proyek telah mencapai batas atas.

Atur ulang batas atas pada lalu lintas tulis proyek. Untuk informasi lebih lanjut, lihat Sesuaikan kuota sumber daya.

LOG_PRODUCER_DROP_ERROR

3

Cache disk atau memori penuh, dan log tidak dapat ditulis.

Atur ulang nilai parameter maxBufferLimit, persistentMaxLogCount, dan persistentMaxFileSize dan coba lagi.

LOG_PRODUCER_SEND_NETWORK_ERROR

4

Kesalahan jaringan terjadi.

Periksa konfigurasi Endpoint, Project, dan Logstore.

LOG_PRODUCER_SEND_QUOTA_ERROR

5

Lalu lintas tulis Proyek telah mencapai batas atas.

Atur batas atas lalu lintas tulis proyek. Untuk informasi lebih lanjut, lihat Adjust resource quotas.

LOG_PRODUCER_SEND_UNAUTHORIZED

6

Pasangan Kunci Akses kedaluwarsa atau tidak valid, atau kebijakan akses dikonfigurasi secara salah.

Periksa pasangan AccessKey.

Pengguna RAM harus memiliki izin untuk mengelola sumber daya Simple Log Service. Untuk informasi lebih lanjut, lihat Berikan izin kepada Pengguna RAM.

LOG_PRODUCER_SEND_SERVER_ERROR

7

Kesalahan layanan terjadi.

Ajukan tiket untuk menghubungi dukungan teknis.

LOG_PRODUCER_SEND_DISCARD_ERROR

8

Data dibuang. Ini biasanya karena waktu perangkat tidak disinkronkan dengan waktu server.

SDK secara otomatis mengirim ulang data tersebut.

LOG_PRODUCER_SEND_TIME_ERROR

9

Waktu tidak disinkronkan dengan waktu server.

SDK secara otomatis memperbaiki masalah ini.

LOG_PRODUCER_SEND_EXIT_BUFFERED

10

Data yang di-cache tidak dikirim saat SDK dihancurkan.

Aktifkan unggah yang dapat dilanjutkan untuk mencegah kehilangan data.

LOG_PRODUCER_PARAMETERS_INVALID

11

Kesalahan terjadi pada parameter inisialisasi SDK.

Periksa konfigurasi parameter seperti AccessKey, Endpoint, Project, dan Logstore.

LOG_PRODUCER_PERSISTENT_ERROR

99

Gagal menulis data yang di-cache ke disk.

1. Periksa apakah jalur file cache dikonfigurasi dengan benar.

2. Periksa apakah file cache penuh.

3. Periksa apakah disk sistem memiliki ruang yang cukup.

FAQ

Mengapa ada log duplikat?

iOS SDK mengirim log secara asinkron. Karena kondisi jaringan, sebuah log mungkin gagal dikirim dan kemudian dikirim ulang. SDK menganggap log berhasil dikirim hanya ketika menerima kode status 200. Ini dapat mengakibatkan log duplikat. Anda dapat menggunakan pernyataan SQL selama kueri dan analisis untuk menghapus data duplikat.

Jika Anda mengalami tingkat duplikasi log yang tinggi, periksa kesalahan dalam inisialisasi SDK. Daftar berikut menjelaskan penyebab umum dan solusinya.

  • Konfigurasi salah untuk unggah yang dapat dilanjutkan

    Periksa apakah jalur file yang diberikan ke metode SetPersistentFilePath bersifat unik secara global.

  • Inisialisasi SDK berulang

    • Penyebab umum dari inisialisasi SDK berulang adalah kegagalan untuk benar-benar menerapkan pola singleton untuk inisialisasi SDK. Inisialisasi SDK seperti yang ditunjukkan dalam contoh berikut.

      // 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 {
          // Ganti kode berikut dengan kode inisialisasi Anda.
          _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];
      }
      
      @end
    • Penyebab lain dari inisialisasi SDK berulang adalah penggunaan beberapa proses. Inisialisasi SDK hanya di proses utama. Jika Anda harus menginisialisasi SDK di proses yang berbeda, berikan nilai unik ke SetPersistentFilePath untuk setiap proses.

  • Optimasi konfigurasi untuk lingkungan jaringan lemah

    Jika aplikasi Anda digunakan dalam lingkungan dengan koneksi jaringan yang lemah, Anda dapat mengoptimalkan parameter konfigurasi SDK seperti yang ditunjukkan dalam contoh berikut.

    // Inisialisasi SDK.
    - (void) initProducer() {
        // Sesuaikan periode timeout untuk koneksi HTTP dan pengiriman untuk mengurangi tingkat duplikasi log.
        // Sesuaikan periode timeout spesifik sesuai kebutuhan.
        [_config SetConnectTimeoutSec:20];
        [_config SetSendTimeoutSec:20];
      
        // Parameter inisialisasi lainnya.
        // ...
    }

Apa yang harus saya lakukan jika log hilang?

Pengiriman log adalah proses asinkron. Jika aplikasi ditutup sebelum log dikirim, log mungkin hilang. Untuk mencegah hal ini, aktifkan unggah yang dapat dilanjutkan. Untuk informasi lebih lanjut, lihat Unggah yang Dapat Dilanjutkan.

Apa yang harus saya lakukan jika pelaporan log tertunda?

SDK mengirim log secara asinkron. Log mungkin tidak segera dikirim karena lingkungan jaringan atau skenario aplikasi tertentu. Jika pengiriman log tertunda hanya pada beberapa perangkat, ini normal. Jika tidak, Anda dapat mendiagnosis masalah menggunakan kode kesalahan dalam tabel berikut.

Kode Kesalahan

Deskripsi

LOG_PRODUCER_SEND_NETWORK_ERROR

Periksa apakah parameter Endpoint, Project, dan Logstore dikonfigurasi dengan benar.

LOG_PRODUCER_SEND_UNAUTHORIZED

Periksa apakah Pasangan Kunci Akses kedaluwarsa atau valid, atau apakah kebijakan akses dikonfigurasi secara salah.

LOG_PRODUCER_SEND_QUOTA_ERROR

Lalu lintas tulis proyek telah mencapai batas atas. Atur ulang batas atas pada lalu lintas tulis proyek. Untuk informasi lebih lanjut, lihat Sesuaikan kuota sumber daya.

Apakah iOS SDK mendukung praresolusi DNS dan kebijakan cache?

Contoh berikut menunjukkan cara menggunakan iOS SDK dengan HTTPDNS SDK untuk mengimplementasikan praresolusi DNS dan kebijakan cache.

Catatan: Jika titik akhir Simple Log Service menggunakan nama domain HTTPS, lihat Solusi untuk Skenario HTTPS dan SNI.
  1. Implementasikan NSURLProtocol kustom.

    #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;
    }
    
    /**
     *  Menentukan apakah akan mencegat dan memproses permintaan yang ditentukan.
     *
     *  @param request Permintaan yang ditentukan.
     *  @return Mengembalikan YES untuk mencegat dan memproses permintaan, atau NO untuk tidak mencegat permintaan.
     */
    + (BOOL)canInitWithRequest:(NSURLRequest *)request {
      if([[request.URL absoluteString] isEqual:@"about:blank"]) {
        return NO;
      }
    
      // Mencegah loop tak terbatas. Permintaan mungkin diinisiasi ulang selama intersepsi. Jika ini tidak ditangani, loop tak terbatas terjadi.
      if ([NSURLProtocol propertyForKey:hasBeenInterceptedCustomLabelKey inRequest:request]) {
        return NO;
      }
    
      NSString * url = request.URL.absoluteString;
      NSString * domain = request.URL.host;
    
      // Cegat hanya permintaan HTTPS.
      if (![url hasPrefix:@"https"]) {
        return NO;
      }
    
      // Anda dapat menambahkan lebih banyak kondisi sesuai kebutuhan, seperti mengonfigurasi array host untuk mencegat hanya host-host dalam array.
    
      // Cegat hanya permintaan yang host-nya diganti dengan alamat IP.
      if (![self isPlainIpAddress:domain]) {
        return NO;
      }
      return YES;
    }
    
    + (BOOL)isPlainIpAddress:(NSString *)hostStr {
      if (!hostStr) {
        return NO;
      }
    
      // Memeriksa apakah alamat tersebut adalah alamat IPv4.
      const char *utf8 = [hostStr UTF8String];
      int success = 0;
      struct in_addr dst;
      success = inet_pton(AF_INET, utf8, &dst);
      if (success == 1) {
        return YES;
      }
    
      // Memeriksa apakah alamat tersebut adalah alamat IPv6.
      struct in6_addr dst6;
      success = inet_pton(AF_INET6, utf8, &dst6);
      if (success == 1) {
        return YES;
      }
    
      return NO;
    }
    
    // Untuk mengarahkan ulang permintaan atau menambahkan header, lakukan operasi dalam metode ini.
    + (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
      return request;
    }
    
    // Mulai memuat permintaan.
    - (void)startLoading {
      NSMutableURLRequest *request = [self.request mutableCopy];
      // Menunjukkan bahwa permintaan telah diproses. Ini mencegah loop tak terbatas.
      [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];
    }
    
    /**
     * Batalkan permintaan.
     */
    - (void)stopLoading {
      if (_inputStream.streamStatus == NSStreamStatusOpen) {
        [self closeStream:_inputStream];
      }
      [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"stop loading" code:-1 userInfo:nil]];
    }
    
    /**
     * Teruskan permintaan menggunakan CFHTTPMessage.
     */
    - (void)startRequest {
      // Informasi header permintaan asli.
      NSDictionary *headFields = _curRequest.allHTTPHeaderFields;
      CFStringRef url = (__bridge CFStringRef) [_curRequest.URL absoluteString];
      CFURLRef requestURL = CFURLCreateWithString(kCFAllocatorDefault, url, NULL);
    
      // Metode permintaan asli, seperti GET atau POST.
      CFStringRef requestMethod = (__bridge_retained CFStringRef) _curRequest.HTTPMethod;
    
      // Buat objek CFHTTPMessageRef berdasarkan URL, metode, dan versi permintaan.
      CFHTTPMessageRef cfrequest = CFHTTPMessageCreateRequest(kCFAllocatorDefault, requestMethod, requestURL, kCFHTTPVersion1_1);
    
      // Tambahkan data yang dilampirkan pada permintaan HTTP POST.
      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);
      }
    
      // Salin informasi header permintaan asli.
      for (NSString* header in headFields) {
        CFStringRef requestHeader = (__bridge CFStringRef) header;
        CFStringRef requestHeaderValue = (__bridge CFStringRef) [headFields valueForKey:header];
        CFHTTPMessageSetHeaderFieldValue(cfrequest, requestHeader, requestHeaderValue);
      }
    
      // Buat input stream untuk objek CFHTTPMessage.
      #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;
    
      // Tetapkan informasi host Indikasi Nama Server (SNI). Ini adalah langkah kunci.
      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) {
        // Simpan runloop thread saat ini. Ini penting untuk permintaan yang dialihkan.
        self.curRunLoop = [NSRunLoop currentRunLoop];
      }
      // Tambahkan permintaan ke antrian event runloop saat ini.
      [_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 berisi data dengan panjang result yang harus ditangani
          [data appendBytes:buffer length:result];
        } else if (result < 0) {
          // Stream memiliki kesalahan. Anda bisa mendapatkan objek NSError menggunakan [iStream streamError]
          data = nil;
          break;
        }
      }
      [stream close];
      return data;
    }
    
    #pragma mark - NSStreamDelegate
    /**
     * Fungsi callback setelah input stream menerima header lengkap.
     */
    - (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);
            // Memberi tahu klien bahwa respons diterima. Pemberitahuan ini hanya dikirim sekali.
    
            CFStringRef httpVersion = CFHTTPMessageCopyVersion(message);
            // Dapatkan kode status dari header respons.
            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;
            }
    
            // Verifikasi sertifikat.
            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()];
            }
    
            // Ikat kebijakan verifikasi ke sertifikat server.
            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) {
              // Jika sertifikat gagal diverifikasi, tutup input stream.
              [self closeStream:aStream];
              [self.client URLProtocol:self didFailWithError:[[NSError alloc] initWithDomain:@"fail to evaluate the server trust" code:-1 userInfo:nil]];
            } else {
              // Sertifikat diverifikasi.
              if (statusCode >= 300 && statusCode < 400) {
                // Tangani kode kesalahan pengalihan.
                [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);
        // Memberi tahu klien bahwa kesalahan terjadi.
        [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_transfer NSDictionary *) (CFHTTPMessageCopyAllHeaderFields(message));
    
          if (!alreadyAdded || ![alreadyAdded boolValue]) {
            objc_setAssociatedObject(aStream, (__bridge const void *)(kAnchorAlreadyAdded), [NSNumber numberWithBool:YES], OBJC_ASSOCIATION_COPY);
            // Memberi tahu klien bahwa respons diterima. Pemberitahuan ini hanya dikirim sekali.
    
            if (!self.responseIsHandle) {
              CFStringRef httpVersion = CFHTTPMessageCopyVersion(message);
              // Dapatkan kode status dari header respons.
              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 {
      // Jika header respons tidak lengkap.
      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 {
      // Header respons.
      CFDictionaryRef headerFieldsRef = CFHTTPMessageCopyAllHeaderFields(messageRef);
      NSDictionary *headDict = (__bridge_transfer NSDictionary *)headerFieldsRef;
      [self redirect:headDict];
    }
    
    - (void)redirect:(NSDictionary *)headDict {
      // Jika cookie diperlukan untuk pengalihan, tangani.
      NSString *location = headDict[@"Location"];
      if (!location)
        location = headDict[@"location"];
      NSURL *url = [[NSURL alloc] initWithString:location];
      _curRequest.URL = url;
      if ([[_curRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) {
        // Menurut dokumentasi RFC, saat permintaan POST dialihkan, itu harus diubah menjadi permintaan GET.
        _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) {
          // Jika Z_BUF_ERROR disebabkan oleh buffer output yang tidak mencukupi, buffer input belum sepenuhnya diproses dan buffer output penuh. Dalam hal ini, loop harus dilanjutkan untuk memperluas buffer.
          // Kondisi sebaliknya menunjukkan bahwa kesalahan tersebut bukan disebabkan oleh buffer output yang tidak mencukupi. Dalam hal ini, loop harus dihentikan, yang menunjukkan kesalahan.
          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];
    }
    
    @end
    
  2. Implementasikan BeforeSend kustom.

    - (void)setupHttpDNS:(NSString *)accountId {
      // Konfigurasikan 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];
      // Tetapkan NSURLProtocol kustom yang didefinisikan di atas.
      [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;
        // Anda dapat menambahkan kondisi di sini untuk menerapkan ini hanya pada URL yang diperlukan.
        
        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;
      }];
    }
  3. Lengkapi inisialisasi SDK SLS.

    @implementation AliyunSLS
    
    - (void)initSLS {
      // Tetapkan HTTPDNS kustom untuk SDK SLS.
      // !!!Catatan!!!
      // Pengaturan ini berlaku untuk semua instance SDK SLS.
      [self setupHttpDNS:accountId];
      [self initProducer];
    }
    
    - (void)initProducer {
        // Inisialisasi LogProducerConfig dan LogProducerClient seperti biasa.
        // ...
    }
    
    @end