commit 467d1c6edf8da205ac47086a7109a1d64d33416e Author: Sambo Chea Date: Sat May 27 01:06:18 2023 +0700 Add collect the logs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3cceda5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..994460d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +## 1.0.0 + +- Initial version. +- Add collect the logs. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9ae1268 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# vLogs SDK for Dart + +A simple way to collect logs and send to the server via simple SDK. + +- [x] Collect the logs +- [ ] Support local retries + +## Usages + +```dart +import 'package:vlogs/vlogs.dart'; + +void main() async { + final APP_ID = "xxx"; + final API_KEY = "vlogs_xxx"; + + final sdk = VLogs.create(APP_ID, API_KEY); + + var request = CollectorRequest.builder() + .message("Hello World") + .source(CollectorSource.mobile.name) + .type(CollectorType.log.name) + .build(); + + var response = await sdk.collect(request); + print("Response: ${response.toJson()}"); +} +``` + +### Contributors + +- Sambo Chea diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..dee8927 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/example/vlogs_example.dart b/example/vlogs_example.dart new file mode 100644 index 0000000..b38521e --- /dev/null +++ b/example/vlogs_example.dart @@ -0,0 +1,19 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:vlogs/vlogs.dart'; + +void main() async { + final APP_ID = "72bd14c306a91fa8a590330e3898ddcc"; + final API_KEY = "vlogs_gX9WwSdKatMNdpUClLU0IfCx575tvdoeQ"; + + final sdk = VLogs.create(APP_ID, API_KEY); + + var request = CollectorRequest.builder() + .message("Hello World") + .source(CollectorSource.mobile.name) + .type(CollectorType.log.name) + .build(); + + var response = await sdk.collect(request); + print("Response: ${response.toJson()}"); +} diff --git a/lib/src/base.dart b/lib/src/base.dart new file mode 100644 index 0000000..1cf37e2 --- /dev/null +++ b/lib/src/base.dart @@ -0,0 +1,88 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:logger/logger.dart'; +import 'package:vlogs/src/model.dart'; +import 'package:vlogs/src/service.dart'; +import 'package:vlogs/src/util.dart'; + +class VLogs { + static final _logger = Logger(); + static final String NAME = 'vlogs'; + static final String VERSION = '1.0.0'; + static final String VERSION_CODE = '1'; + static final String DEFAULT_VLOGS_URL = "https://vlogs-sg1.onrender.com"; + static final String APP_ID_HEADER_PREFIX = "x-app-id"; + static final String API_KEY_HEADER_PREFIX = "x-api-key"; + static final int DEFAULT_CONNECT_TIMEOUT = 60; // seconds + + late String _baseUrl; + late String _appId; + late String _apiKey; + + late VLogsService _service; + + VLogs(VLogsOptions options) { + _baseUrl = options.url ?? DEFAULT_VLOGS_URL; + if (options.appId == null || options.apiKey == null) { + throw Exception("AppId and ApiKey are required"); + } + + if (options.apiKey!.isEmpty || options.appId!.isEmpty) { + throw Exception("AppId and ApiKey are required"); + } + _appId = options.appId!; + _apiKey = options.apiKey!; + + _service = VLogsService(_baseUrl); + + _logger.i("VLogs: Initialized AppID: $_appId | SDK Version: $VERSION-$VERSION_CODE"); + } + + Future collect(CollectorRequest request) async { + var headers = { + APP_ID_HEADER_PREFIX: _appId, + API_KEY_HEADER_PREFIX: _apiKey, + "Content-Type": "application/json", + }; + + var hostname = Util.getSystemHostname(); + var sender = Util.getSystemUsername(); + var sdkInfo = SDKInfo.builder() + .hostname(hostname) + .sender(sender) + .name(VLogs.NAME) + .version(VLogs.VERSION) + .versionCode(VLogs.VERSION_CODE) + .build(); + + if (request.target == null) { + request.target = Target.builder().sdkInfo(sdkInfo).build(); + } else { + request.target!.sdkInfo = sdkInfo; + } + request.userAgent ??= "vlogs-dart-sdk/$VERSION-$VERSION_CODE ($hostname)"; + + var response = await _service.post(request.toJson(), headers: headers); + return response; + } + + void collectAsync(CollectorRequest request) async { + try { + var response = await collect(request); + _logger.i("VLogs: ${response.message}"); + } catch (e) { + _logger.e("VLogs: ${e.toString()}"); + } + } + + static VLogs createWithOptions(VLogsOptions options) { + return VLogs(options); + } + + static VLogs create(String appId, String apiKey) { + return createWithOptions(VLogsOptions( + appId: appId, + apiKey: apiKey, + )); + } +} diff --git a/lib/src/model.dart b/lib/src/model.dart new file mode 100644 index 0000000..a135b38 --- /dev/null +++ b/lib/src/model.dart @@ -0,0 +1,487 @@ +import 'dart:convert'; + +import 'package:uuid/uuid.dart'; + +enum CollectorType { error, event, metric, trace, log, span } + +enum CollectorSource { web, mobile, server, desktop, iot, other } + +enum TelegramParseMode { markdown, markdownV2, html } + +class Telegram { + String? token; + String? chatId; + TelegramParseMode? parseMode; + bool? disabled; + + Telegram({ + this.token, + this.chatId, + this.parseMode, + this.disabled, + }); + + Map toMap() { + return { + 'token': token, + 'chat_id': chatId, + 'parse_mode': parseMode, + 'disabled': disabled, + }; + } + + static TelegramBuilder builder() { + return TelegramBuilder(); + } +} + +class TelegramBuilder { + String? _token; + String? _chatId; + TelegramParseMode? _parseMode; + bool? _disabled; + + TelegramBuilder(); + + TelegramBuilder token(String? token) { + _token = token; + return this; + } + + TelegramBuilder chatId(String? chatId) { + _chatId = chatId; + return this; + } + + TelegramBuilder parseMode(TelegramParseMode? parseMode) { + _parseMode = parseMode; + return this; + } + + TelegramBuilder disabled(bool? disabled) { + _disabled = disabled; + return this; + } + + Telegram build() { + return Telegram( + token: _token, + chatId: _chatId, + parseMode: _parseMode, + disabled: _disabled, + ); + } +} + +class Discord { + String? webhookId; + String? webhookToken; + String? webhookUrl; + bool? disabled; + + Discord({ + this.webhookId, + this.webhookToken, + this.webhookUrl, + this.disabled, + }); + + Discord._builder(DiscordBuilder builder) + : webhookId = builder._webhookId, + webhookToken = builder._webhookToken, + webhookUrl = builder._webhookUrl, + disabled = builder._disabled; + + Map toMap() { + return { + 'webhook_id': webhookId, + 'webhook_token': webhookToken, + 'webhook_url': webhookUrl, + 'disabled': disabled, + }; + } + + static DiscordBuilder builder() { + return DiscordBuilder(); + } +} + +class DiscordBuilder { + String? _webhookId; + String? _webhookToken; + String? _webhookUrl; + bool? _disabled; + + DiscordBuilder(); + + DiscordBuilder webhookId(String? webhookId) { + _webhookId = webhookId; + return this; + } + + DiscordBuilder webhookToken(String? webhookToken) { + _webhookToken = webhookToken; + return this; + } + + DiscordBuilder webhookUrl(String? webhookUrl) { + _webhookUrl = webhookUrl; + return this; + } + + DiscordBuilder disabled(bool? disabled) { + _disabled = disabled; + return this; + } + + Discord build() { + return Discord._builder(this); + } +} + +class SDKInfo { + String? name; + String? version; + String? versionCode; + String? hostname; + String? sender; + + SDKInfo({ + this.name, + this.version, + this.versionCode, + this.hostname, + this.sender, + }); + + SDKInfo._builder(SDKInfoBuilder builder) + : name = builder._name, + version = builder._version, + versionCode = builder._versionCode, + hostname = builder._hostname, + sender = builder._sender; + + Map toMap() { + return { + 'name': name, + 'version': version, + 'version_code': versionCode, + 'hostname': hostname, + 'sender': sender, + }; + } + + static SDKInfoBuilder builder() { + return SDKInfoBuilder(); + } +} + +class SDKInfoBuilder { + String? _name; + String? _version; + String? _versionCode; + String? _hostname; + String? _sender; + + SDKInfoBuilder(); + + SDKInfoBuilder name(String? name) { + _name = name; + return this; + } + + SDKInfoBuilder version(String? version) { + _version = version; + return this; + } + + SDKInfoBuilder versionCode(String? versionCode) { + _versionCode = versionCode; + return this; + } + + SDKInfoBuilder hostname(String? hostname) { + _hostname = hostname; + return this; + } + + SDKInfoBuilder sender(String? sender) { + _sender = sender; + return this; + } + + SDKInfo build() { + return SDKInfo._builder(this); + } +} + +class Target { + Telegram? telegram; + Discord? discord; + SDKInfo? sdkInfo; + + Target({ + this.telegram, + this.discord, + this.sdkInfo, + }); + + Map toMap() { + return { + 'telegram': telegram?.toMap(), + 'discord': discord?.toMap(), + 'sdk_info': sdkInfo?.toMap(), + }; + } + + static TargetBuilder builder() { + return TargetBuilder(); + } +} + +class TargetBuilder { + Telegram? _telegram; + Discord? _discord; + SDKInfo? _sdkInfo; + + TargetBuilder(); + + TargetBuilder telegram(Telegram? telegram) { + _telegram = telegram; + return this; + } + + TargetBuilder discord(Discord? discord) { + _discord = discord; + return this; + } + + TargetBuilder sdkInfo(SDKInfo? sdkInfo) { + _sdkInfo = sdkInfo; + return this; + } + + Target build() { + return Target( + telegram: _telegram, + discord: _discord, + sdkInfo: _sdkInfo, + ); + } +} + +class CollectorRequest { + String? id; + String? type; + String? source; + String? message; + dynamic data; + String? userAgent; + int? timestamp; + Target? target; + List? tags; + + CollectorRequest( + {this.id, + this.type, + this.source, + this.message, + this.data, + this.userAgent, + this.timestamp, + this.target, + this.tags}); + + String? getId() { + id ??= Uuid().v4(); + return id; + } + + int? getTimestamp() { + timestamp ??= DateTime.now().millisecondsSinceEpoch; + return timestamp; + } + + Map toMap() { + return { + 'id': getId(), + 'type': type, + 'source': source, + 'message': message, + 'data': data, + 'user_agent': userAgent, + 'timestamp': getTimestamp(), + 'target': target?.toMap(), + 'tags': tags, + }; + } + + String toJson() => json.encode(toMap()); + + static CollectorRequestBuilder builder() { + return CollectorRequestBuilder(); + } +} + +class CollectorRequestBuilder { + String? _id; + String? _type; + String? _source; + String? _message; + dynamic _data; + String? _userAgent; + int? _timestamp; + Target? _target; + List? _tags; + + CollectorRequestBuilder(); + + CollectorRequestBuilder id(String? id) { + _id = id; + return this; + } + + CollectorRequestBuilder type(String? type) { + _type = type; + return this; + } + + CollectorRequestBuilder source(String? source) { + _source = source; + return this; + } + + CollectorRequestBuilder message(String? message) { + _message = message; + return this; + } + + CollectorRequestBuilder data(dynamic data) { + _data = data; + return this; + } + + CollectorRequestBuilder userAgent(String? userAgent) { + _userAgent = userAgent; + return this; + } + + CollectorRequestBuilder timestamp(int? timestamp) { + _timestamp = timestamp; + return this; + } + + CollectorRequestBuilder target(Target? target) { + _target = target; + return this; + } + + CollectorRequestBuilder tags(List? tags) { + _tags = tags; + return this; + } + + CollectorRequest build() { + return CollectorRequest( + id: _id, + type: _type, + source: _source, + message: _message, + data: _data, + userAgent: _userAgent, + timestamp: _timestamp, + target: _target, + tags: _tags, + ); + } +} + +class CollectorResponse { + String? message; + String? id; + + CollectorResponse({this.message, this.id}); + + bool get isSuccess => message == 'ok'; + + Map toMap() { + return { + 'id': id, + 'message': message, + }; + } + + String toJson() => json.encode(toMap()); + + factory CollectorResponse.fromMap(Map map) { + return CollectorResponse( + message: map['message'], + id: map['id'], + ); + } + + factory CollectorResponse.fromJson(String source) => + CollectorResponse.fromMap(json.decode(source)); +} + +class VLogsOptions { + String? url; + String? appId; + String? apiKey; + int? connectionTimeout; + bool? testConnection; + + VLogsOptions({ + this.url, + this.appId, + this.apiKey, + this.connectionTimeout, + this.testConnection, + }); + + VLogsOptions._builder(VLogsOptionsBuilder builder) + : url = builder._url, + appId = builder._appId, + apiKey = builder._apiKey, + connectionTimeout = builder._connectionTimeout, + testConnection = builder._testConnection; +} + +class VLogsOptionsBuilder { + String? _url; + String? _appId; + String? _apiKey; + int? _connectionTimeout; + bool? _testConnection; + + VLogsOptionsBuilder(); + + VLogsOptionsBuilder url(String? url) { + _url = url; + return this; + } + + VLogsOptionsBuilder appId(String? appId) { + _appId = appId; + return this; + } + + VLogsOptionsBuilder apiKey(String? apiKey) { + _apiKey = apiKey; + return this; + } + + VLogsOptionsBuilder connectionTimeout(int? connectionTimeout) { + _connectionTimeout = connectionTimeout; + return this; + } + + VLogsOptionsBuilder testConnection(bool? testConnection) { + _testConnection = testConnection; + return this; + } + + VLogsOptions build() { + return VLogsOptions._builder(this); + } +} diff --git a/lib/src/service.dart b/lib/src/service.dart new file mode 100644 index 0000000..fa8f22e --- /dev/null +++ b/lib/src/service.dart @@ -0,0 +1,32 @@ +import 'package:vlogs/src/model.dart'; +import 'package:http/http.dart' as http; + +class VLogsService { + late final String url; + + VLogsService(String baseUrl) { + url = '$baseUrl/api/v1/collector'; + } + + Future post(String body, {headers}) async { + var request = http.Request('POST', Uri.parse(url)); + request.body = body; + // print("Request Body: ${request.body}"); + + if (headers != null) { + request.headers.addAll(headers); + } + + var response = await request.send(); + + if (response.statusCode == 200 || + response.statusCode == 201 || + response.statusCode == 202) { + var json = await response.stream.bytesToString(); + return CollectorResponse.fromJson(json); + } else { + throw Exception( + 'Failed to post data to vlogs server with status code: ${response.statusCode}'); + } + } +} diff --git a/lib/src/util.dart b/lib/src/util.dart new file mode 100644 index 0000000..161046c --- /dev/null +++ b/lib/src/util.dart @@ -0,0 +1,26 @@ +import 'dart:convert'; +import 'dart:io'; + +class Util { + static String? toJSON(dynamic data) { + try { + return jsonEncode(data); + } catch (e) { + return null; + } + } + + static String getSystemHostname() { + String name = Platform.localHostname; + + if (name.isEmpty) { + name = 'unknown'; + } + + return name; + } + + static String getSystemUsername() { + return Platform.environment['USER'] ?? 'unknown'; + } +} diff --git a/lib/vlogs.dart b/lib/vlogs.dart new file mode 100644 index 0000000..94a9543 --- /dev/null +++ b/lib/vlogs.dart @@ -0,0 +1,4 @@ +library; + +export 'src/base.dart'; +export 'src/model.dart'; \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..0fa6543 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,16 @@ +name: vlogs +description: A simple way to collect logs and send to the server via simple SDK. +version: 1.0.0 +repository: https://github.com/CUBETIQ/vlogs_sdk_dart.git + +environment: + sdk: ^3.0.2 + +dependencies: + http: + uuid: + logger: + +dev_dependencies: + lints: ^2.0.0 + test: ^1.21.0 diff --git a/test/vlogs_test.dart b/test/vlogs_test.dart new file mode 100644 index 0000000..ec01288 --- /dev/null +++ b/test/vlogs_test.dart @@ -0,0 +1,29 @@ +// ignore_for_file: non_constant_identifier_names + +import 'package:test/test.dart'; +import 'package:vlogs/src/model.dart'; +import 'package:vlogs/vlogs.dart'; + +void main() { + final APP_ID = "72bd14c306a91fa8a590330e3898ddcc"; + final API_KEY = "vlogs_gX9WwSdKatMNdpUClLU0IfCx575tvdoeQ"; + + group('A group of tests', () { + final sdk = VLogs.create(APP_ID, API_KEY); + + setUp(() { + // Additional setup goes here. + }); + + test('Emit the logs to collector', () async { + var request = CollectorRequest.builder() + .message("Hello World") + .source(CollectorSource.mobile.name) + .type(CollectorType.log.name) + .build(); + + var response = await sdk.collect(request); + expect(response.id, request.getId()); + }); + }); +}